链接上文:ArkTS入门(一)

接口

接口声明引入新类型。接口是定义代码协定的常见方式。

任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。

接口通常包含属性和方法的声明。

// 定义接口
interface Animal {
    makeSound(): void;
}

// 实现接口
class Dog implements Animal {
    makeSound(): void {
        console.log("Woof!");
    }
}

class Cat implements Animal {
    makeSound(): void {
        console.log("Meow!");
    }
}

class Cow implements Animal {
    makeSound(): void {
        console.log("Moo!");
    }
}

// 使用接口多态
function makeAnimalSound(animal: Animal): void {
    animal.makeSound();
}

const dog = new Dog();
const cat = new Cat();
const cow = new Cow();

makeAnimalSound(dog); // 输出: Woof!
makeAnimalSound(cat); // 输出: Meow!
makeAnimalSound(cow); // 输出: Moo!
// 接口定义
interface Style {
  color: string; // 属性
}
interface AreaSize {
  calculateAreaSize(): number; // 方法的声明
  someMethod(): void;     // 方法的声明
}

// 接口实现
class RectangleSize implements AreaSize {
  private width: number = 0;
  private height: number = 0;
  someMethod(): void {
    console.log('someMethod called');
  }
  calculateAreaSize(): number {
    this.someMethod(); // 调用另一个方法并返回结果
    return this.width * this.height;
  }
}

接口属性

接口属性可以是字段、getter、setter或getter和setter组合的形式。

// 属性字段只是getter/setter对的便捷写法。
interface Style {
	color : string;
}
interface Style {
	get color(): string;
	set color(x: string);
}

// 实现接口类1
interface Style {
	color: string;
}
interface StyledRectangle implements Style {
	color: string = '';
}

// 实现接口类2
interface Style {
	color: string;
}
class StyledRectangle implements Style {
	private _color: string = '';
	get color(): string { return this._color; }
	set color(x: string) { this._color = x }
}

接口继承

继承接口包含被继承接口的所有属性和方法,还可以添加自己的属性和方法,通过接口继承,可以创建更复杂的类型层次结构,提高代码的复用性和可维护性。

interface Style {
	color: string;
}

interface ExtendedStyle extends Style {
	width: number;
}

泛型类型和函数

泛型类型和函数允许创建的代码在各种类型上运行,而不仅支持单一类型。

泛型类和接口

类和接口可以定义为泛型,将参数添加到类型定义中,如以下示例中的类型参数Element:

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

要使用类型CustomStack,必须为每个类型参数指定类型实参:

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

编译器在使用泛型类型和函数时会确保类型安全。参见以下示例:

let s = new CustomStack<string>();
s.push(55); // 将会产生编译时错误

泛型约束

泛型类型的类型参数可以被限制只能取某些特定的值。例如,MyHashMap<Key, Value>这个类中的Key类型参数必须具有hash方法。

interface Hashable {
  hash(): number;
}
class MyHashMap<Key extends Hashable, Value> {
  public set(k: Key, v: Value) {
    let h = k.hash();
    // ...其他代码...
  }
}

// Key类型扩展了Hashable,Hashable接口的所有方法都可以为key调用。

泛型函数

使用泛型函数可编写更通用的代码。

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

如果需要为任何数组定义相同的函数,使用类型参数将该函数定义为泛型:

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

现在,该函数可以与任何数组一起使用。在函数调用中,类型实参可以显式或隐式设置:

// 显式设置的类型实参
last<string>(['aa', 'bb']);
last<number>([1, 2, 3]);

// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
last([1, 2, 3]);

泛型默认值

泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。

// 定义一个简单空类
class SomeType {}
// 定义一个泛型接口,默认参数T1为SomeType
interface Interface <T1 = SomeType> { }
// 定义一个泛型类,默认参数T2为SomeType
class Base <T2 = SomeType> { }


// Derived1 继承自Base类并实现Interface接口。
class Derived1 extends Base implements Interface { }
// 继承自 Base<SomeType> 并实现 Interface<SomeType>。显式指定了类型参数 SomeType。
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }
//  Base 和 Interface 的默认类型参数都是 SomeType,所以Derived1在语义上等价于Derived2


// 定义了一个泛型函数 foo, 默认类型参数 T 为 number。
function foo<T = number>(): T {
  // ...
}
// 调用 foo 函数时, 如果没有显式指定类型参数, TypeScript 会使用默认类型参数 number。
foo();
// 显式指定类型参数 number, 与 foo() 的效果相同。
foo<number>();

空安全

默认情况下,ArkTS中的所有类型都是不可为空的,因此类型的值不能为空。这类似于TypeScript的严格空值检查模(strictNullChecks),但规则更严格。

let x: number = null;    // 编译时错误
let y: string = null;    // 编译时错误
let z: number[] = null;  // 编译时错误

// 可以为空值的变量定义为联合类型T | null
let x: number | null = null;
x = 1;    // ok
x = null; // ok
if (x != null) { /* do something */ }

非空断言运算符

后缀运算符 ! 可用于断言其操作数为非空。

class A {
  value: number = 0;
}

function foo(a: A | null) {
  a.value;   // 编译时错误:无法访问可空值的属性
  a!.value;  // 编译通过,如果运行时a的值非空,可以访问到a的属性;如果运行时a的值为空,则发生运行时异常
}

空值合并运算符

空值合并二元运算符??用于检查左侧表达式的求值是否等于null或者undefined。如果是,则表达式的结果为右侧表达式;否则,结果为左侧表达式。

换句话说,a ?? b等价于三元运算符(a != null && a != undefined) ? a : b。

// getNick方法如果设置了昵称,则返回昵称;否则,返回空字符串
class Person {
  // ...
  nick: string | null = null;
  getNick(): string {
    return this.nick ?? '';
  }
}

可选链

在访问对象属性时,如果该属性是undefined或者null,可选链运算符会返回undefined。

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,因为该方法可能返回null或者undefined

可选链可以任意长,可以包含任意数量的?.运算符。

// Person的实例有不为空的spouse属性,且spouse有不为空的nick属性,则输出spouse.nick。否则,输出undefined
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; // undefined

模块

程序可划分为多组编译单元或模块。

每个模块都有其自己的作用域,即,在模块中创建的任何声明(变量、函数、类等)在该模块之外都不可见,除非它们被显式导出。

与此相对,从另一个模块导出的变量、函数、类、接口等必须首先导入到模块中。

导出

可以使用关键字export导出顶层的声明。未导出的声明名称被视为私有名称,只能在声明该名称的模块中使用。

export class Point {
  x: number = 0;
  y: number = 0;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
export let Origin = new Point(0, 0);
export function Distance(p1: Point, p2: Point): number {
  return Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));
}

// 通过export方式导出,在导入时要加{}。

导入

静态导入

导入声明用于导入从其他模块导出的实体,并在当前模块中提供其绑定。导入声明由两部分组成:

  • 导入路径,用于指定导入的模块;
  • 导入绑定,用于定义导入的模块中的可用实体集和使用形式(限定或不限定使用)。

导入绑定可以有几种形式。

// 假设模块具有路径“./utils”和导出实体“X”和“Y”。

// 导入绑定* as A表示绑定名称“A”,通过A.name可访问从导入路径指定的模块导出的所有实体
import * as Utils from './utils'
Utils.X // 表示来自Utils的X
Utils.Y // 表示来自Utils的Y

// 导入绑定{ ident1, ..., identN }表示将导出的实体与指定名称绑定,该名称可以用作简单名称
import { X, Y } from './utils'
X // 表示来自utils的X
Y // 表示来自utils的Y

// 如果标识符列表定义了ident as alias,则实体ident将绑定在名称alias下
import { X as Z, Y } from './utils'
Z // 表示来自Utils的X
Y // 表示来自Utils的Y
X // 编译时错误:'X'不可见

动态导入

应用开发的有些场景中,如果希望根据条件导入模块或者按需导入模块,可以使用动态导入代替静态导入。

import()语法通常称为动态导入dynamic import,是一种类似函数的表达式,用来动态导入模块。以这种方式调用,将返回一个promise。

// import(modulePath)可以加载模块并返回一个promise,该promise resolve为一个包含其所有导出的模块对象。该表达式可以在代码中的任意位置调用
let modulePath = prompt("Which module to load?");
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)

// 如果在异步函数中,可以使用let module = await import(modulePath)
// say.ts
export function hi() {
  console.log('Hello');
}
export function bye() {
  console.log('Bye');
}

// 可以像下面这样进行动态导入
async function test() {
  let ns = await import('./say');
  let hi = ns.hi;
  let bye = ns.bye;
  hi();
  bye();
}

更多的使用动态import的业务场景和使用实例见动态import

导入HarmonyOS SDK的开放能力

HarmonyOS SDK提供的开放能力(接口)也需要在导入声明后使用。

// 可直接导入接口模块来使用该模块内的所有接口能力
import UIAbility from '@ohos.app.ability.UIAbility';

从HarmonyOS NEXT Developer Preview 1版本开始引入Kit概念。SDK对同一个Kit下的接口模块进行了封装,开发者在示例代码中可通过导入Kit的方式来使用Kit所包含的接口能力。其中,Kit封装的接口模块可查看SDK目录下Kit子目录中各Kit的定义。

通过导入Kit方式使用开放能力有三种方式:

// 方式一:导入Kit下单个模块的接口能力
import { UIAbility } from '@kit.AbilityKit';

// 方式二:	导入Kit下多个模块的接口能力
import { UIAbility, Ability, Context } from '@kit.AbilityKit';

// 方式三:	导入Kit包含的所有模块的接口能力
import * as module from '@kit.AbilityKit';

// module 为别名,可自定义,然后通过该名称调用模块的接口
// 方式三可能会导入过多无需使用的模块,导致编译后的HAP包太大,占用过多资源,请谨慎使用。

顶层语句

顶层语句是指在模块的最外层直接编写的语句,这些语句不被包裹在任何函数、类、块级作用域中。顶层语句包括变量声明、函数声明、表达式等。

关键词

this

关键字this只能在类的实例方法中使用

class A {
  count: string = 'a';
  m(i: string): void {
    this.count = i;
  }
}

使用限制:

  • 不支持this类型
  • 不支持在函数和类的静态方法中使用this
class A {
  n: number = 0;
  f1(arg1: this) {} // 编译时错误,不支持this类型
  static f2(arg1: number) {
    this.n = arg1;  // 编译时错误,不支持在类的静态方法中使用this
  }
}

function foo(arg1: number) {
  this.n = i;       // 编译时错误,不支持在函数中使用this
}

关键字this的指向:

  • 调用实例方法的对象
  • 正在构造的对象
Logo

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

更多推荐