鸿蒙开发——ArkTS基础知识 - 类、接口及泛型

1.前言

本文我们学习鸿蒙开发中使用ARKTS的类、接口、泛型、字段、方法、继承以及模块导出和导入的概念,为鸿蒙应用开发者提供了基础理论支持。

2 . 类

类声明引入一个新类型,并定义其字段、方法和构造函数。

有两种方法创建实例:

  1. 通过new创建实例

  2. 通过对象字面量创建实例

    //声明一个Person类
    class Person {
      //name属性
      name: string = ''
      age: number
     //构造函数
      constructor(n: string, sn: number) {
        this.name = n
        this.age = sn
      }
      //方法
      fullName(): string {
        return this.name + ' ' + this.age
      }
    }

    // 1.通过 new 创建实例
    let person = new Person('John', 18)
    console.log(person.fullName())

    // 2.使用对象字面量创建实例
    class Point {
      x: number = 0
      y: number = 0
    }
    let p: Point = {x: 20, y: 32};
    console.log(p.x+" "+p.y);
2.1字段

字段是直接在类中声明的某种类型的变量。

类可以具有实例字段或者静态字段。

2.1.1 实例字段

实例字段存在于类的每个实例上。每个实例都有自己的实例字段集合。 实例字段就是上面我们类实例中的属性,以上面的代码为例:

// 其中person.name为实例字段
let person = new Person('John', 18)
console.log(person.fullName())
2.1.2 静态字段

使用关键字static将字段声明为静态。静态字段属于类本身,类的所有实例共享一个静态字段。

要访问静态字段,需要使用类名:

class Student{
  name: string=''
  static sex: number=18
}
console.log("年龄"+Student.sex)

让我们看一下运行结果: 

2.1.3 字段初始化

为了减少运行时的错误和获得更好的执行性能,ArkTS要求所有字段在声明时或者构造函数中显式初始化。这和标准TS中的strictPropertyInitialization模式一样。

class Person {
  // 可能为`undefined`
  name ?: string

  setName(n:string): void {
    this.name = n
  }

  // 编译时错误:name可以是"undefined",所以将这个API的返回值类型标记为string
  // 演示成功样例时需要将该方法注释掉
  getNameWrong(): string {
    return this.name
  }

  // 返回类型匹配name的类型
  getName(): string | undefined {
    return this.name
  }
}

function main(){
  let jack = new Person()
  // 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"

  // 编译时错误:编译器认为下一行代码有可能会访问undefined的属性,报错
  // 演示成功样例时需要将该方法注释掉
  let length1 = jack.getName().length
  // 编译成功,没有运行时错误,?.表示可以为undefined 
  let length2 = jack.getName()?.length
  console.log(""+length2);
}

错误演示: 

成功样例演示: 

2.1.4 getter和setter
setter和getter可用于提供对对象属性的受控访问。

在以下示例中,setter用于禁止将age属性设置为无效值:

class Person {
  name: string = ''
  private _age: number = 0
  get age(): number { return this._age }
  set age(x: number) {
    if (x < 0) {
      throw Error('数值非法')
    }
    this._age = x
  }
}

let p = new Person()
console.log (''+p.age) // 将打印输出0
p.age = -42 // 设置无效age值会抛出错误

运行结果如下: 

2.2 方法

方法属于类。类可以定义实例方法或者静态方法。静态方法属于类本身,只能访问静态字段。而实例方法既可以访问静态字段,也可以访问实例字段,包括类的私有字段

2.2.1 实例方法

上面Person类中fullName()方法就是实例方法,必须通过类的实例调用实例方法

let person = new Person('John', 18)
// 通过类的实例调用实例方法
person.fullName()
2.2.2 静态方法

使用关键字static将方法声明为静态。静态方法属于类本身,只能访问静态字段。

静态方法定义了类作为一个整体的公共行为。所有实例都可以访问静态方法。必须通过类名调用静态方法

class Test{
  static test(): string{
    return "我是一个静态方法";
  }
}
console.log(''+Test.test());

2.3 继承

一个类可以继承另一个类(称为基类),可以实现多个接口。单继承类,多实现接口。 继承类可以继承基类的字段和方法,但不继承构造函数。继承类可以新增定义字段和方法,也可以覆盖其基类定义的方法。

基类也称为“父类”或“超类”。继承类也称为“派生类”或“子类”。

包含implements子句的类必须实现列出的接口中定义的所有方法,但使用默认实现定义的方法除外。

interface Introduce {
  call(): string;
}

class Person {
  name: string = ''
  age: number = 0

  constructor(n: string, sn: number) {
    this.name = n
    this.age = sn
  }

  fullName(): string {
    return this.name + ' ' + this.age
  }
}

class Employee extends Person implements Introduce {

  // 实现接口的now方法
  call(): string {
    return "我是员工一"
  }
}
let e1= new Employee('张三',18);
console.log(''+e1.fullName());
console.log(''+e1.call());

2.3.1 父类调用
父类属性和父类方法都通过super调用
interface Introduce {
  call(): string;
}

class Person {
  name: string = ''
  age: number = 0

  constructor(n: string, sn: number) {
    this.name = n
    this.age = sn
  }

  fullName(): string {
    return this.name + ' ' + this.age
  }
}

class Employee extends Person implements Introduce {
    id: number=0
  constructor(n: string, sn: number) {
    // 构造函数也通过super直接调用父类构造函数,
    // 如果构造函数函数体不以父类构造函数的显式调用开始,
    // 则构造函数函数体隐式地以父类构造函数调用super()开始。
    super(n, sn)
  }
  sessionId(): string {
    // 通过super访问父类属性
    return super.name + this.id
  }
  // 方法重写
  fullName(): string {
    // 通过super访问父类方法
    return super.fullName();
  }
  // 实现接口的now方法
  call(): string {
    return "我是员工一"
  }
}
let e1= new Employee('张三',18);
console.log(''+e1.fullName());
console.log(''+e1.call());
2.3.2 方法重写

子类可以重写其父类中定义的方法的实现。重写的方法必须具有与原始方法相同的参数类型和相同或派生的返回类型。

class Employee extends Person implements Introduce {
    id: number=0
  // 方法重写
  fullName(): string {
    // 通过super访问父类方法
    return super.fullName();
  }
  // 实现接口的now方法
  call(): string {
    return "我是员工一"
  }
}
2.3.3 方法重载

通过重载签名,指定方法的不同调用。具体方法为,为同一个方法写入多个同名但签名不同的方法头,方法实现紧随其后。构造方法重载也一样。

注意:如果两个重载签名的名称和参数列表均相同,会报错。

class C {
  foo(): void;            /* 第一个签名 */
  foo(x: string): void;   /* 第二个签名 */
  foo(x?: string): void { /* 实现签名 */
    console.log(x)
  }
}
let c = new C()
c.foo()     // OK,使用第一个签名
c.foo('aa') // OK,使用第二个签名

运行结果如下: 

3 接口

接口声明引入新类型。接口是定义代码协定的常见方式。 任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。

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

interface Style {
  color: string
}

// 继承接口包含被继承接口的所有属性和方法
// 还可以添加自己的属性和方法。
interface Area extends Style {
  width: number
  height: number
  // 方法的声明
  someMethod(): void;
}

class Rectangle implements Area {
  // 接口的属性必须实现
  // 1.接口属性通过直接声明实现
  width: number
  height: number
  // 构造函数,用于初始化属性
  constructor(width: number = 0, height: number = 0, color: string = '') {
    this.width = width
    this.height = height
    this._color = color
  }
  // 2.接口属性通过getter、setter方法实现,等价于上面方法1
  private _color: string = ''
  get color(): string {
    return this._color
  }
  set color(x: string) {
    this._color = x
  }

  // 接口方法实现
  someMethod(): void {
    console.log('实现Area接口方法')
  }

}
let r = new Rectangle();
r.someMethod();

4 泛型

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

4.1 泛型类和接口

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

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

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

let s = new Stack<string>
s.push('hello')
typescript
let s = new Stack<string>
s.push('hello')

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

let s = new Stack<string>
s.push(55) // 将会产生编译时错误
typescript
let s = new Stack<string>
s.push(55) // 将会产生编译时错误
4.2 泛型约束

泛型类型的类型参数可以绑定。例如,HashMap<Key, Value> 容器中的Key类型参数必须具有哈希方法,即它应该是可哈希的。

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

在上面的例子中,Key类型扩展了Hashable,Hashable接口的所有方法都可以为key调用

4.3 泛型函数

使用泛型函数可编写更通用的代码。比如返回数组最后一个元素的函数:

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

运行结果如下: 

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

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

现在,该函数可以与任何数组一起使用。

在函数调用中,类型实参可以显式或隐式设置:

function last<T>(x: T[]): T {
  return x[x.length - 1]
}
// 显式设置的类型实参
console.log(""+last<string>(['aa', 'bb']))
console.log(""+last<number>([1, 2, 3]))

// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
console.log(""+last([1, 2, 3]))
// 显式设置的类型实参
console.log(last<string>(['aa', 'bb']))
console.log(""+last<number>([1, 2, 3]))

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

运行结果如下: 

4.4 泛型默认值

泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。下面的示例展示了类和函数的这一点。

class SomeType {}
interface Interface <T1 = SomeType> { }
class Base <T2 = SomeType> { }
class Derived1 extends Base implements Interface { }
// Derived1在语义上等价于Derived2
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }

function foo<T = number>(): T {
  // ...
}
foo()
// 此函数在语义上等价于下面的调用
foo<number>()
typescript
class SomeType {}
interface Interface <T1 = SomeType> { }
class Base <T2 = SomeType> { }
class Derived1 extends Base implements Interface { }
// Derived1在语义上等价于Derived2
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }

function foo<T = number>(): T {
  // ...
}
foo()
// 此函数在语义上等价于下面的调用
foo<number>()

5 包

5.1 模块

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

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

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

5.2 导出

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

注意:通过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))
}

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

  • 导入路径,用于指定导入的模块;

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

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

  • 假设模块具有路径“./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'不可见

6.总结

学完本章,ArkTS的基础语法基本上就是学完了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值