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的类型合并。

相关推荐
大师兄66685 天前
HarmonyOS 卡片 UI 三种玩法:普通卡片、动效卡片、Canvas 卡片
harmonyos·arkts·formkit·动效卡片·canvas卡片
UnicornDev16 天前
【HarmonyOS 6】底部悬浮导航的迷你栏适配(API23)
华为·harmonyos·arkts·鸿蒙
笔触狂放17 天前
【项目】基于ArkTS的老年人智能应用开发(1)
harmonyos·arkts·鸿蒙
UnicornDev18 天前
【HarmonyOS 6】底部悬浮导航的沉浸光感适配(API23)
华为·harmonyos·arkts·鸿蒙·鸿蒙系统
UnicornDev23 天前
【HarmonyOS 6】设置页面 UI 设计
ui·华为·harmonyos·arkts·鸿蒙
UnicornDev1 个月前
【HarmonyOS 6】基于API23的底部悬浮导航
华为·harmonyos·arkts·鸿蒙·鸿蒙系统
积水成渊,蛟龙生焉1 个月前
鸿蒙手势处理篇(滑动冲突、基础手势、组合手势)
华为·arkts·鸿蒙·滑动冲突·手势冲突·基础手势·组合手势
纯爱掌门人1 个月前
聊聊 HarmonyOS 上的应用内通知授权弹窗
前端·harmonyos·arkts
UnicornDev1 个月前
【HarmonyOS 6】练习记录页面 UI 设计
ui·华为·harmonyos·arkts·鸿蒙
哈__1 个月前
新手入门harmonyOS开发:手把手教你用ArkTS实现一个天气应用
harmonyos·arkts