Harmony os ——ArkTS 语言笔记(五):泛型、空安全与可选链
本文介绍了ArkTS语言中的泛型、空安全与可选链特性。泛型通过类型参数实现代码复用,支持类、接口和函数的泛型化,并提供约束和默认值功能。空安全机制默认禁止null/undefined赋值,需显式声明可空类型。文章还讲解了三个实用运算符:非空断言(!)用于强制类型转换,空值合并(??)提供默认值,可选链(?.)安全访问可能为null的属性。这些特性共同增强了类型安全性,帮助开发者在编译期捕获潜在错误
ArkTS 语言笔记(五):泛型、空安全与可选链
这一篇主要记三件事:
- 泛型类型 / 泛型函数
- 空安全(null 安全)规则
- 几个和空值相关的小工具:!、??、?.
这些东西在 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 判断了
可选链运算符 ?. 的作用是:
在访问对象属性时,如果前面那一级是
null或undefined,就直接返回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:
spouse是undefined→ 整个表达式结果是undefined - 情况 2:
spouse.nick是null→ 结果是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,不抛异常。
- 非空断言:
更多推荐


所有评论(0)