ArkTS基础语法 |(4)泛型类型和函数、空安全、模块、关键字、注解
在学习HarmonyOS开发的核心语言ArkTS时,整理了一份基础语法笔记,方便日后回顾。
一、泛型类型和函数
泛型的核心价值是让代码以类型安全的方式操作多种数据类型 ,避免为每种类型编写重复逻辑,同时编译器会做类型校验,兼顾代码通用性和类型安全性。
1. 泛型类和接口
在类/接口的定义中添加类型参数,使用时需为类型参数指定具体的类型实参,编译器会严格校验传入的类型是否匹配。
TypeScript
// 泛型类定义
class CustomStack<Element> {
public push(e: Element): void {
// 入栈逻辑
}
}
// 正确使用:指定类型实参为string
let s = new CustomStack<string>();
s.push('hello'); // 编译通过
// 错误使用:传入非string类型
s.push(55); // 编译时错误 类型不匹配
2. 泛型约束
通过 extends 关键字限制泛型的类型参数只能取特定值(实现指定接口/继承指定类),让泛型拥有特定的方法/属性,满足业务逻辑的类型要求。
TypeScript
// 定义约束接口
interface Hashable {
hash(): number; // 要求被约束的类型必须实现hash方法
}
// 泛型类:Key必须实现Hashable接口 Value无约束
class MyHashMap<Key extends Hashable, Value> {
public set(k: Key, v: Value) {
let h = k.hash(); // 可安全调用hash方法 因Key受泛型约束
// 后续逻辑
}
}
3. 泛型函数
为函数定义类型参数,让函数支持任意类型的入参和返回值,调用时可显式 或隐式指定类型实参(编译器可根据入参自动推导)。
TypeScript
// 泛型函数定义:返回数组最后一个元素
function last<T>(x: T[]): T {
return x[x.length - 1];
}
// 显式指定类型实参
let res1: string = last<string>(['aa', 'bb']); // 结果:'bb'
let res2: number = last<number>([1, 2, 3]); // 结果:3
// 隐式指定:编译器根据入参自动推导类型
let res3: number = last([1, 2, 3]); // 编译通过 推导T为number
4. 泛型默认值
为泛型的类型参数设置默认值,使用时若不指定类型实参,编译器会自动使用默认值,简化泛型的使用。
类、接口、函数均支持泛型默认值。
TypeScript
class SomeType {}
// 接口和类设置泛型默认值为SomeType
interface Interface <T1 = SomeType> { }
class Base <T2 = SomeType> { }
// 不指定类型实参 自动使用默认值
class Derived1 extends Base implements Interface { }
// 等价于显式指定默认值
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }
// 函数设置泛型默认值为number
function foo<T = number>(): void {
// 函数逻辑
}
foo(); // 等价于foo<number>()
二、空安全
ArkTS默认开启严格的空安全校验 ,规则比TypeScript的strictNullChecks模式更严苛,所有基础类型默认不允许赋值为null/undefined ,从编译阶段避免空指针异常,是ArkTS的核心特性之一。
1. 可空类型定义
若变量需要支持空值,需通过 联合类型T | null 显式声明,声明后变量可赋值为T类型或null。
TypeScript
// 错误:默认类型不允许为空
let x: number = null; // 编译时错误
let y: string = null; // 编译时错误
// 正确:显式声明可空类型
let x: number | null = null;
x = 1; // 正确 赋值为number类型
x = null; // 正确 赋值为null
2. 非空断言运算符 !
后缀运算符 ! 用于断言可空类型的变量为非空 ,编译时会将 T | null 转为非空的 T 类型,可直接访问其属性/方法。
注意:非空断言仅为编译期校验,若运行时变量实际为空,会触发运行时异常,使用前需确保变量非空。
TypeScript
class A {
value: number = 0;
}
function foo(a: A | null) {
a.value; // 编译时错误:无法访问可空值的属性
a!.value; // 编译通过 断言a非空
}
3. 空值合并运算符 ??
二元运算符 ?? 用于判断左侧表达式是否为 null/undefined ,是空值兜底的常用方式,比三元运算符更简洁。
-
左侧为
null/undefined:返回右侧表达式结果 -
左侧非空:返回左侧表达式结果
TypeScript
// 等价于:(a != null && a != undefined) ? a : b
// 空值合并运算符写法
a ?? b
// 三元运算符写法
(a !== null && a !== undefined) ? a : b
// if-else 写法
if (a !== null && a !== undefined) {
a; // 返回 a
} else {
b; // 返回 b
}
// ---------------------------------------------------------------------------------------------------
// 示例1
const a = 0; // a有有效值
const b = 10;
console.log(a ?? b); // 输出 0(0不是null/undefined 故返回a)
console.log((a !== null && a !== undefined) ? a : b); // 输出 0
const a = null; // a是null
const b = 10;
console.log(a ?? b); // 输出 10(a是null 故返回b)
console.log((a !== null && a !== undefined) ? a : b); // 输出 10
const a = undefined; // a是undefined
const b = 10;
console.log(a ?? b); // 输出 10(a是undefined 故返回b)
console.log((a !== null && a !== undefined) ? a : b); // 输出 10
// 示例2
// 获取昵称 未设置则返回空字符串
class Person {
nick: string | null = null;
getNick(): string {
return this.nick ?? ''; // nick为null时返回''
}
}
4. 可选链运算符 ?.
访问对象的嵌套属性/方法时,使用 ?. 可自动判断左侧对象是否为 null/undefined ,若为空则直接返回 undefined ,避免层层判空的冗余代码。
-
可选链可多层嵌套(
obj?.a?.b?.c) -
可选链也支持方法调用(
obj?.fn())
TypeScript
class Person {
nick: string | null = null;
spouse?: Person; // 可选属性 默认undefined
constructor(nick: string) {
this.nick = nick;
this.spouse = undefined;
}
}
let p: Person = new Person('Alice');
p.spouse?.nick; // spouse为undefined 直接返回 不报错
// 多层可选链
p.spouse?.spouse?.nick; // 编译通过 任意一层为空则返回undefined
三、模块
ArkTS支持将程序拆分为多个模块(编译单元) ,每个模块拥有独立的作用域,模块内的声明(变量、函数、类等)默认私有,需显式导出后才能被其他模块导入使用,有效实现代码的模块化和解耦。
1. 导出 export
使用 export 关键字导出模块的顶层声明,支持命名导出 和默认导出两种方式:
-
命名导出:导出多个实体,导入时需匹配名称。
-
默认导出:每个模块仅能有一个默认导出,导入时可自定义名称。
TypeScript
// 命名导出
export const num = 10; // 导出单个实体
// 导出多个实体
export class Demo{
constructor(){}
}
// 默认导出:一个模块仅一个
export default new Demo();
2. 导入 import
使用 import 关键字导入其他模块导出的实体,分为静态导入 和动态导入,静态导入是开发中的常用方式。
静态导入(三种形式)
假设模块路径为 ./utils 导出了实体X和Y:
TypeScript
// 形式1:导入所有实体,绑定到别名,通过「别名.实体名」访问。
import * as Utils from './utils';
Utils.X; Utils.Y;
// 形式2:按需导入指定实体,直接使用实体名。
import { X, Y } from './utils';
X; Y;
// 形式3:导入并为实体重命名,避免命名冲突。
import { X as Z, Y } from './utils';
Z; // 对应原X
Y;
X; // 编译错误 未导入
动态导入
通过 import() 实现条件/按需导入,返回一个Promise对象,适用于需要根据业务逻辑动态加载模块的场景(如懒加载)。
TypeScript
// 动态导入示例:点击按钮后加载模块
button.onClick(() => {
import('./utils').then((module) => {
module.X; // 使用导入的模块实体
}).catch((err) => {
// 处理导入失败
});
});
3. HarmonyOS SDK 开放能力导入
HarmonyOS SDK的接口支持直接导入模块 和导入Kit 两种方式. 从NEXT Developer Preview 1开始推荐导入Kit(SDK对同Kit下的接口做了封装,更简洁). Kit导入有三种方式,推荐按需导入避免包体积过大。
TypeScript
// 方式1:导入Kit下单个模块的接口
import { UIAbility } from '@kit.AbilityKit';
// 方式2:导入Kit下多个模块的接口(推荐,按需导入)
import { UIAbility, Ability, Context } from '@kit.AbilityKit';
// 方式3:导入Kit下所有接口(谨慎使用,会增大HAP包体积)
import * as module from '@kit.AbilityKit';
module.UIAbility;
4. 顶层语句
模块最外层、未被任何函数/类/块级作用域包裹的语句(变量声明、函数声明、表达式等),即为顶层语句 ,模块加载时会自动执行。
四、关键字this
ArkTS中 this 的使用有严格的场景限制 ,仅能指向类的实例对象,核心作用是在类的实例方法中访问实例的属性和方法. 相比JavaScript,ArkTS对 this 做了更多编译期校验,避免误用。
1. 合法使用场景
仅能在类的实例方法 中使用 this ,指向调用该方法的实例对象 或正在构造的实例对象。
TypeScript
class A {
count: string = 'a';
// 实例方法中使用this,访问实例属性
m(i: string): void {
this.count = i; // 合法,this指向A的实例
}
}
2. 禁止使用场景
ArkTS严格限制 this 的使用,以下场景会触发编译错误:
(1)不支持 this 类型( 不能将 this 作为参数/返回值类型 )
(2)不允许在函数 中使用 this
(3)不允许在类的静态方法 中使用 this(静态方法属于类而非实例)
TypeScript
class A {
n: number = 0;
f1(arg1: this) {} // 编译错误:不支持this类型
static f2(arg1: number) {
this.n = arg1; // 编译错误:静态方法中不能使用this
}
}
// 编译错误:普通函数中不能使用this
function foo(arg1: number) {
this.n = arg1;
}
五、注解(Annotation)
注解是ArkTS的专属特性(TypeScript不支持),通过为声明添加元数据 来修改应用声明的语义,可理解为给类/方法打"标签",用于实现元编程、AOP等功能,仅能在.ets/.d.ets文件中使用。
1. 注解的基础声明与使用
-
使用
@interface声明注解,注解需定义在顶层作用域。 -
使用注解时需加前缀
@,@与注解名之间无空格、换行。 -
注解可带参数,参数需为常量表达式,无参数时可省略括号。
-
多个注解可应用于同一个声明,顺序不影响效果。
TypeScript
// 1. 声明注解:带一个string类型的参数
@interface ClassAuthor {
authorName: string
}
// 2. 声明无参注解
@interface MyAnno {}
// 3. 使用注解:类上添加注解,带参数。
@ClassAuthor({authorName: "Bob"})
class MyClass { }
// 4. 无参注解的使用:可省略括号
@MyAnno
class MyClass2 { }
// 5. 多个注解叠加使用
@ClassAuthor({authorName: "Bob"})
@MyAnno
class MyClass3 { }
2. 用户自定义注解
从API version 20开始支持用户自定义注解,有严格的语法约束,违反会触发编译错误。
(1)注解字段类型限制
仅支持以下类型,不支持 BigInt ,且数组仅能由以下类型组成 :number 、boolean 、string 、枚举 、上述类型的数组。
TypeScript
// 合法的注解声明
@interface MyAnno1 {
num: number;
bool: boolean;
str: string;
arr: string[];
}
// 编译错误:字段类型为BigInt
@interface MyAnno2 {
big: bigint;
}
(2)注解字段默认值约束
字段默认值必须是常量表达式,仅支持 :数字字面量 / 布尔字面量 / 字符串字面量 / 编译期可确定的枚举值 / 上述常量的数组。
(3)注解的合法使用对象
当前仅允许在类声明class declarations 和方法声明method declarations上使用注解,不支持在抽象类、抽象方法、类的getter/setter方法上使用。
TypeScript
@interface MyAnno {}
// 编译错误:抽象类不能加注解
@MyAnno
abstract class C {
// 编译错误:抽象方法不能加注解
@MyAnno
abstract foo(): void;
}
// 编译错误:getter方法不能加注解
class D {
@MyAnno
get num() { return 10; }
}
3. 注解的导入与导出
注解支持跨模块导入导出,但有专属的规则,与普通实体的导入导出不同:
-
导出:仅支持
export @interface 注解名的形式 -
导入:仅支持
import {}和import * as,不允许重命名 、不允许使用import type。 -
仅导入注解不会触发模块的副作用(如模块中的
console、变量赋值等)
TypeScript
// a.ets:导出注解
export @interface MyAnno {}
export @interface ClassAuthor {}
console.info('hello'); // 模块副作用
// b.ets:导入注解
import { MyAnno } from './a'; // 合法
import * as ns from './a'; // 合法
// import { MyAnno as Anno } from './a'; // 编译错误:不允许重命名
// import type { MyAnno } from './a'; // 编译错误:不允许使用import type
@MyAnno
@ns.ClassAuthor
class C { }
// 仅导入注解,a.ets中的console.info('hello')不会执行
4. .d.ets文件中的注解
注解可出现在 .d.ets 声明文件中,通过环境声明( declare )定义注解,仅提供注解的类型信息,不实际定义注解,注解的具体实现需在其他源代码文件中完成,且环境声明与实际实现必须完全一致(字段类型、默认值)。
TypeScript
// a.d.ets:环境声明注解
export declare @interface ClassAuthor {
authorName: string;
revision: number = 1;
}
// b.ets:实现注解(需与环境声明一致)
export @interface ClassAuthor {
authorName: string;
revision: number = 1;
}
// c.ets:导入并使用
import { ClassAuthor } from './a';
@ClassAuthor({authorName: "Bob"})
class C { }
5. 注解的其他核心注意事项
(1)禁止重复注解:同一个实体不能重复使用同一个注解,否则编译错误。
(2)注解不继承:子类不会继承基类的注解,子类方法也不会继承基类方法的注解。
(3)混淆兼容 :release模式下构建JS HAR 并开启混淆时,注解会被移除(JS无注解实现),需避免使用;若需使用,应构建字节码HAR。
(4)注解非类型:不能将注解当作类型使用(如类型别名),也不支持TypeScript的类型合并。