ArkTS 语言笔记(五):泛型、空安全与可选链

这一篇主要记三件事:

  1. 泛型类型 / 泛型函数
  2. 空安全(null 安全)规则
  3. 几个和空值相关的小工具:!、??、?.

这些东西在 ArkTS 里都挺“严格”的,但写多了你会发现——它们是在帮你提前挡 bug。


一、为什么要泛型?

一句话:

泛型 = 在“保持类型安全”的前提下,让一份代码适配多种类型。

不泛型会怎样?比如写一个栈:

  • 一个 number
  • 一个 string
  • 一个 User 版……

逻辑几乎一样,只是类型不同,重复很烦。
泛型就是为了解决这个:逻辑写一次,类型“占个位”,用的时候再填具体类型进去。


二、泛型类和接口

2.1 最基础的泛型类

比如一个自定义栈:

class CustomStack<Element> {
  public push(e: Element): void {
    // ...
  }
}

这里的 Element 就是一个类型参数(type parameter),你可以理解成:

“我先不说这个栈里装的是什么类型,等你用的时候再告诉我。”

使用时,需要给它一个实际类型(type argument):

let s = new CustomStack<string>();
s.push('hello');

如果你乱来:

let s = new CustomStack<string>();
s.push(55);  // 编译期直接报错

ArkTS 会在编译期间就拦住这类不匹配的调用。


2.2 泛型约束:不是所有类型都行

有些场景并不是“任何类型都可以”,比如一个 MyHashMap<Key, Value>,我们希望 Key 至少要能被“哈希”一下。

就可以用 extends 做泛型约束

interface Hashable {
  hash(): number;
}

class MyHashMap<Key extends Hashable, Value> {
  public set(k: Key, v: Value) {
    let h = k.hash();
    // ... 用 h 做一些事情 ...
  }
}

这里:

  • Key extends Hashable 表示:Key 必须是 Hashable 的子类型(或本身实现了这个接口)。
  • 在方法里就可以放心地调用 k.hash(),不会再担心 Key 没这个方法。

简单记法:
“泛型 + extends 接口/类” = “类型参数必须至少长成某个样子”。


三、泛型函数:写一次,通吃多种类型

先看一个非泛型的版本:返回数组最后一个元素:

function last(x: number[]): number {
  return x[x.length - 1];
}

last([1, 2, 3]); // 3

问题是:如果数组不是 number[] 呢?
我们不可能为 string[]Person[] 再各写一遍。

改造成泛型函数:

function last<T>(x: T[]): T {
  return x[x.length - 1];
}

现在,这个函数可以适配任何类型的数组了。

3.1 显式指定类型参数

let res1: string = last<string>(['aa', 'bb']);
let res2: number = last<number>([1, 2, 3]);

3.2 让编译器“猜”(类型推断)

一般情况下,可以直接这样写:

let res3: number = last([1, 2, 3]);

编译器会根据传入的数组类型自动推断出 T 的具体类型。

实战经验:
大多数时候不用显式写 <T>,除非遇到复杂场景推断不出来,再手动补上。


四、泛型默认值:懒人福利

有时候泛型类型/函数的“默认类型”非常明确,就可以给类型参数一个默认值

4.1 泛型类/接口上的默认类型参数

class SomeType {}

interface Interface<T1 = SomeType> { }

class Base<T2 = SomeType> { }

class Derived1 extends Base implements Interface { }
// 等价于:
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }

也就是说,不写时就默认为 SomeType

4.2 泛型函数的默认类型参数

function foo<T = number>(): void {
  // ...
}

foo();         // 实际等价于 foo<number>()
foo<number>(); // 显式写出来的形式

这个功能对库设计者特别友好:
普通人用默认的就行,高级玩家可以手动指定。


五、空安全:ArkTS 比 TS 更“较真”

ArkTS 的默认模式可以理解成:

所有类型默认都是“非空”的:不能为 null、也不能为 undefined

这一点和 TypeScript 的 strictNullChecks 有点像,但 ArkTS 要求更严格。

看这个例子,全都会编译报错:

let x: number = null;    // ❌
let y: string = null;    // ❌
let z: number[] = null;  // ❌

5.1 真的需要“可以为空”怎么办?

用联合类型显式写出来:

let x: number | null = null;

x = 1;    // ok
x = null; // ok

if (x != null) {
  // 这里 x 会自动缩小为 number 类型
}

这其实挺合理的——只要你承认“它可能为空”,就得在类型上写清楚,以便编译器帮你做检查。


六、非空断言运算符 !

有时候,你比编译器更清楚:“这里绝不会是 null”,但类型上又不得不写成 T | null
这个时候可以用 非空断言运算符 !,告诉编译器“我负责”。

class A {
  value: number = 0;
}

function foo(a: A | null) {
  a.value;   // ❌ 编译时错误:a 可能为 null
  a!.value;  // ✅ 编译通过
}

行为是:

  • 编译时:类型从 A | null 变成 A
  • 运行时:
    • 如果 a 真的是 null → 会抛异常;
    • 如果 a 不为 null → 正常访问。

换句话说:
! 是在对编译器说:“你别管,我自己兜底,出事算我的。”
能不用就不用,多用显式判断会安全很多。


七、空值合并运算符 ??

?? 解决的是这样一类需求:

“如果左边是 null/undefined,就用右边的默认值,否则就用左边。”

语义等价于:

a ?? b  // 等价于:
(a != null && a != undefined) ? a : b

一个典型例子:返回昵称,如果没设置就返回空字符串:

class Person {
  nick: string | null = null;

  getNick(): string {
    return this.nick ?? '';
  }
}

好处:

  • if/else 简洁;
  • 把“空值处理逻辑”写得很集中、一眼能看明白。

八、可选链 ?.:不要一层层 if 判断了

可选链运算符 ?. 的作用是:

在访问对象属性时,如果前面那一级是 nullundefined,就直接返回 undefined,而不是抛异常。

8.1 基本示例

class Person {
  nick: string | null = null;
  spouse?: Person;

  setSpouse(spouse: Person): void {
    this.spouse = spouse;
  }

  getSpouseNick(): string | null | undefined {
    return this.spouse?.nick;
  }

  constructor(nick: string) {
    this.nick = nick;
    this.spouse = undefined;
  }
}

这里 getSpouseNick() 的返回类型写成 string | null | undefined 是有道理的:

  • 情况 1:spouseundefined → 整个表达式结果是 undefined
  • 情况 2:spouse.nicknull → 结果是 null
  • 情况 3:spouse.nick 是正常字符串 → 结果是 string

所以要把这三种可能都写进返回类型里。

8.2 可选链可以“接力用”

class Person {
  nick: string | null = null;
  spouse?: Person;

  constructor(nick: string) {
    this.nick = nick;
    this.spouse = undefined;
  }
}

let p: Person = new Person('Alice');
p.spouse?.nick;  // 如果 spouse 是 undefined,则整个结果是 undefined

现实代码里可以写得更长,比如:

p.spouse?.company?.address?.city;

每一级都可能短路为 undefined,而不会抛 "cannot read property 'xxx' of undefined" 这种经典错误。


九、小结:这一篇的关键词

这篇主要围绕 ArkTS 里的几个“类型安全增强工具”:

  • 泛型类型 / 函数
    • 泛型类 / 接口:class CustomStack<Element> { ... }
    • 泛型约束:Key extends Hashable
    • 泛型函数:function last<T>(x: T[]): T
    • 类型实参的显式 / 隐式传入
    • 泛型默认值:<T = number>
  • 空安全机制
    • 默认所有类型都“不接受 null”
    • 需要可空时用联合类型:T | null
    • 用泛型时,同样可以写成 T | null 这种形式
  • 围绕“空”的三个语法糖:
    • 非空断言:a!
      → 编译期把 T | null 强行当 T,出事你负责。
    • 空值合并:a ?? b
      → 空就用默认值,不空就用本身。
    • 可选链:obj?.field?.subField
      → 任何一层为 null/undefined 都直接变成 undefined,不抛异常。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐