ArkTS基础语法 |(4)泛型类型和函数、空安全、模块、关键字、注解

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 导出了实体XY

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 ,且数组仅能由以下类型组成 :numberbooleanstring 、枚举 、上述类型的数组。

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. 注解的导入与导出

注解支持跨模块导入导出,但有专属的规则,与普通实体的导入导出不同:

  1. 导出:仅支持 export @interface 注解名 的形式

  2. 导入:仅支持 import {}import * as不允许重命名 、不允许使用 import type

  3. 仅导入注解不会触发模块的副作用(如模块中的 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的类型合并。

相关推荐
ITUnicorn1 天前
【HarmonyOS 6】数据可视化:实现热力图时间块展示
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn3 天前
【HarmonyOS 6】HarmonyOS 自定义时间选择器实现
华为·harmonyos·arkts·鸿蒙·harmonyos6
Lee_xiaoming4 天前
ArkTS基础语法 |(3)类和接口
arkts
Lee_xiaoming6 天前
ArkTS基础语法 |(2)函数
arkts
全栈探索者6 天前
useState 换个名字叫 @State,仅此而已
react·harmonyos·arkts·前端开发·deveco studio·状态管理·鸿蒙next
全栈探索者6 天前
useContext 退场,@Provide + @Consume 登场
react.js·harmonyos·arkts·状态管理·前端转型
Lee_xiaoming7 天前
ArkTS基础语法 | (1)基本知识
arkts
全栈探索者8 天前
列表渲染不用 map,用 ForEach!—— React 开发者的鸿蒙入门指南(第 4 期)
react.js·harmonyos·arkts·foreach·列表渲染
ITUnicorn9 天前
【HarmonyOS6】ArkTS 自定义组件封装实战:动画水杯组件
华为·harmonyos·arkts·鸿蒙·harmonyos6