Harmony os——ArkTS 语言笔记(四):类、对象、接口和抽象类

Harmony os------ArkTS 语言笔记(四):类、对象、接口和抽象类

鸿蒙第四期活动

这一篇主要整理 ArkTS 里的 类 / 对象 / 接口 / 抽象类

写的时候我尽量按"实际写代码时会遇到的问题"来组织


一、从一个最普通的类开始

最标准的 ArkTS 类声明长这样:

ts 复制代码
class Person {
  name: string = '';
  surname: string = '';

  constructor(n: string, sn: string) {
    this.name = n;
    this.surname = sn;
  }

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

let p = new Person('John', 'Smith');
console.info(p.fullName());

几个要点:

  • class 引入一个新类型Person
  • 里面可以有:
    • 字段(name, surname
    • 构造函数(constructor
    • 方法(fullName()
  • 使用 new 创建实例:new Person(...)

这部分和 TypeScript 基本是一致的。


二、字段:实例字段、静态字段和"必须初始化"这件事

2.1 实例字段 vs 静态字段

实例字段:每个对象各有一份。

复制代码
class Person {
  name: string = '';
  age: number = 0;

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

  getName(): string {
    return this.name;
  }
}

let p1 = new Person('Alice', 25); // p1 有自己的 name/age
let p2 = new Person('Bob', 28);   // p2 有自己的 name/age

静态字段:属于类本身,所有实例共享一份。

复制代码
class Person {
  static numberOfPersons = 0;

  constructor() {
    Person.numberOfPersons++;
  }
}

console.info(Person.numberOfPersons);

访问方式:

  • 实例字段:实例.字段
  • 静态字段:类名.字段

个人习惯:

  • "统计类信息、全局配置、缓存"放静态字段
  • "跟具体对象相关的属性"用实例字段

2.2 ArkTS 为什么强制"字段必须初始化"?

ArkTS 的要求是:

字段要么在声明时 初始化,要么在构造函数里初始化。

不这么做,会怎样?看这个例子(错误写法):

复制代码
class Person {
  name: string; // 这里其实是 undefined

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

  getName(): string {
    return this.name;  // 这里假装一定是 string
  }
}

let jack = new Person();
// 从头到尾都没给 jack.name 赋过值
jack.getName().length; // 运行时直接炸:name is undefined

问题关键点:

  • name 实际上可能是 undefined
  • 但类型标成了 string,骗过了类型系统;
  • 最终在 getName().length 这里炸掉。

ArkTS 的思路是:尽量在编译期就暴露问题,不要把炸弹留到运行时。

改成 ArkTS 推荐写法:

复制代码
class Person {
  name: string = '';  // 明确初始化为 ''

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

  getName(): string { // 现在真的保证是 string,不是 undefined
    return this.name;
  }
}

let jack = new Person();
jack.getName().length; // 0,正常执行

如果字段确实有可能缺失,就把这个可能性写在类型上:

复制代码
class Person {
  name?: string;  // 类型:string | undefined

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

  // 错误示例:返回值不能只写 string
  getNameWrong(): string {
    return this.name;  // 编译器会报错
  }

  getName(): string | undefined {
    return this.name;
  }
}

let jack = new Person();

// 编译期就会阻止这种写法:
jack.getName().length; // ❌ 编译失败

// 正确写法:加上可选链或其他判断
jack.getName()?.length; // ✅ 编译通过,没有运行时异常

我自己的理解:

ArkTS 不想让你偷懒。

要么一开始就给个安全默认值,要么在类型上诚实地承认:"它可能是 undefined"。


2.3 getter / setter:做"受控属性"

有些字段不希望被随便改,希望在赋值时做校验,这就是 getter/setter 的场景。

复制代码
class Person {
  name: string = '';
  private _age: number = 0;

  get age(): number {
    return this._age;
  }

  set age(x: number) {
    if (x < 0) {
      throw Error('Invalid age argument');
    }
    this._age = x;
  }
}

let p = new Person();
console.info(p.age); // 0
p.age = -42;         // 抛异常

特点:

  • 对外访问方式还是 p.age / p.age = ...
  • 但内部可以加校验、日志、派发事件等逻辑

三、方法:实例方法、静态方法、继承和重写

3.1 实例方法

实例方法依赖具体对象的状态来计算结果。

复制代码
class RectangleSize {
  private height: number = 0;
  private width: number = 0;

  constructor(height: number, width: number) {
    this.height = height;
    this.width = width;
  }

  calculateArea(): number {
    return this.height * this.width;
  }
}

let square = new RectangleSize(10, 10);
square.calculateArea(); // 100

3.2 静态方法

静态方法属于类本身,一般用于"与具体实例无关的工具行为"。

复制代码
class Cl {
  static staticMethod(): string {
    return 'this is a static method.';
  }
}

console.info(Cl.staticMethod());

特点:

  • 通过类名调用:Cl.staticMethod()
  • 静态方法里只能直接访问静态成员,不访问某个具体实例的字段

3.3 继承:extends + implements

类的定义可以同时:

  • extends 一个基类;

  • implements 若干接口。

    class Person {
    name: string = '';
    private _age = 0;

    复制代码
    get age(): number {
      return this._age;
    }

    }

    class Employee extends Person {
    salary: number = 0;

    复制代码
    calculateTaxes(): number {
      return this.salary * 0.42;
    }

    }

实现接口:

复制代码
interface DateInterface {
  now(): string;
}

class MyDate implements DateInterface {
  now(): string {
    return 'now';
  }
}

小总结:

  • 继承(extends):继承"实现 + 状态",可以复用代码。
  • 实现(implements):只继承"约定",必须自己实现方法。

3.4 super:访问父类构造和方法

复制代码
class RectangleSize {
  protected height: number = 0;
  protected width: number = 0;

  constructor(h: number, w: number) {
    this.height = h;
    this.width = w;
  }

  draw() {
    /* 画边框 */
  }
}

class FilledRectangle extends RectangleSize {
  color = '';

  constructor(h: number, w: number, c: string) {
    super(h, w);  // 调用父类构造函数
    this.color = c;
  }

  draw() {
    super.draw();  // 先画边框
    /* 再填充颜色 */
  }
}

注意:

  • 在派生类构造函数中,super(...) 必须是第一条语句
  • 重写方法时,如果想保留父类部分逻辑,就调用 super.xxx()

3.5 方法重写(override 的思想)

子类可以用同名方法覆盖父类实现,只要:

  • 参数列表一致;

  • 返回类型相同或是父类返回类型的子类型。

    class RectangleSize {
    area(): number {
    return 0;
    }
    }

    class Square extends RectangleSize {
    private side: number = 0;

    复制代码
    area(): number {
      return this.side * this.side;
    }

    }


3.6 方法重载签名(类里的"多态调用")

和函数重载一样,方法也可以写多个重载签名 + 一个实现:

复制代码
class C {
  foo(x: number): void;            /* 第一个签名 */
  foo(x: string): void;            /* 第二个签名 */
  foo(x: number | string): void {  /* 实现签名 */
    // ...
  }
}

let c = new C();
c.foo(123);     // 使用第一个签名
c.foo('aa');    // 使用第二个签名

注意:

  • 重载签名的参数列表不能完全相同
  • 实现签名必须能覆盖所有前面的重载情况(通常用联合类型)。

四、构造函数:初始化对象状态

4.1 基本构造函数

复制代码
class Point {
  x: number = 0;
  y: number = 0;
}

let p = new Point();  // 没有自定义构造函数时,系统给你一个默认的

如果你自己写了构造函数,就可以定制初始化逻辑:

复制代码
class RectangleSize {
  width: number = 0;
  height: number = 0;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
}

4.2 派生类构造函数:必须先 super(...)

复制代码
class RectangleSize {
  constructor(width: number, height: number) {
    // ...
  }
}

class Square extends RectangleSize {
  constructor(side: number) {
    super(side, side);
  }
}

这块和 TS/ES 的规则一样:
子类构造函数里,在访问 this 之前一定要先调用 super(...)


4.3 构造函数重载签名

写法和方法重载很像:

复制代码
class C {
  constructor(x: number)               /* 第一个签名 */
  constructor(x: string)               /* 第二个签名 */
  constructor(x: number | string) {    /* 实现签名 */
    // ...
  }
}

let c1 = new C(123);
let c2 = new C('abc');

同样的规则:签名参数列表不能一模一样。


五、可见性修饰符:public / private / protected

默认可见性是 public。手动标时有三种:

5.1 public(公有)

  • 任何地方都可以访问(只要能访问到这个类)。

5.2 private(私有)

  • 只能在类内部访问,外部和子类都不行。

    class C {
    public x: string = '';
    private y: string = '';

    复制代码
    set_y(new_y: string) {
      this.y = new_y;        // ✅ 类内部访问私有字段
    }

    }

    let c = new C();
    c.x = 'a'; // ✅
    c.y = 'b'; // ❌ 编译错误:'y' 是私有的

5.3 protected(受保护)

  • private 类似,但子类中可以访问

    class Base {
    protected x: string = '';
    private y: string = '';
    }

    class Derived extends Base {
    foo() {
    this.x = 'a'; // ✅ protected 子类可访问
    this.y = 'b'; // ❌ 编译错误:private 只能在 Base 内部访问
    }
    }

简单记法:

  • 只给自己用:private
  • 自己和子类用:protected
  • 对外开放:public

六、对象字面量、Record 和"结构化数据"

6.1 用对象字面量初始化类类型

复制代码
class C {
  n: number = 0;
  s: string = '';
}

let c: C = { n: 42, s: 'foo' };

这里有一个前提:上下文要能推断出类型是 C,否则 ArkTS 不知道这个字面量应该匹配谁。

还可以这样用:

复制代码
class C {
  n: number = 0;
  s: string = '';
}

function foo(c: C) {}

let c: C;
c = { n: 42, s: 'foo' };       // 利用变量类型
foo({ n: 42, s: 'foo' });      // 利用参数类型

function bar(): C {
  return { n: 42, s: 'foo' };  // 利用返回类型
}

数组中也一样:

复制代码
class C {
  n: number = 0;
  s: string = '';
}

let cc: C[] = [
  { n: 1, s: 'a' },
  { n: 2, s: 'b' },
];

归纳一下:
对象字面量本身是"长得像某个类型"的一坨数据,ArkTS 需要一个上下文来知道它应该被当成哪种类型。


6.2 Record<K, V>:用键值映射来表达表格数据

Record<K, V> 本质就是:键类型为 K,值类型为 V 的一个对象类型

复制代码
let map: Record<string, number> = {
  'John': 25,
  'Mary': 21,
};

map['John']; // 25
  • K 可以是 stringnumber(不包括 BigInt)
  • V 可以是任意类型

结合复杂类型:

复制代码
interface PersonInfo {
  age: number;
  salary: number;
}

let map: Record<string, PersonInfo> = {
  'John': { age: 25, salary: 10 },
  'Mary': { age: 21, salary: 20 },
};

这在做"名字 → 配置""ID → 对象信息"等映射时非常自然。


七、抽象类:不能直接 new,只能被继承

7.1 抽象类基本概念

abstract 的类就是抽象类,不能直接实例化

复制代码
abstract class X {
  field: number;

  constructor(p: number) {
    this.field = p;
  }
}

let x = new X(666);  // ❌ 编译错误:不能创建抽象类实例

但可以继承它:

复制代码
abstract class Base {
  field: number;

  constructor(p: number) {
    this.field = p;
  }
}

class Derived extends Base {
  constructor(p: number) {
    super(p);
  }
}

let x = new Derived(666);  // ✅

7.2 抽象方法

抽象方法只有声明,没有实现,只能放在抽象类里。

复制代码
class Y {
  abstract method(p: string);  
  // ❌ 编译错误:抽象方法只能在 abstract class 里
}

正确写法:

复制代码
abstract class Base {
  abstract method(p: string): void;
}

class Derived extends Base {
  method(p: string): void {
    console.info(p);
  }
}

八、接口:行为的"协议"

接口是对"应该有什么属性/方法"的约定。

复制代码
interface Style {
  color: string;  // 属性
}

interface AreaSize {
  calculateAreaSize(): number; // 方法声明
  someMethod(): void;
}

实现接口的类:

复制代码
interface AreaSize {
  calculateAreaSize(): number;
  someMethod(): void;
}

class RectangleSize implements AreaSize {
  private width: number = 0;
  private height: number = 0;

  someMethod(): void {
    console.info('someMethod called');
  }

  calculateAreaSize(): number {
    this.someMethod();
    return this.width * this.height;
  }
}

8.1 接口属性:字段 vs getter/setter

接口里的属性可以写成:

复制代码
interface Style {
  color: string;
}

也可以拆成 getter + setter:

复制代码
interface Style {
  get color(): string;
  set color(x: string);
}

类实现接口时也有两种写法:

复制代码
interface Style {
  color: string;
}

class StyledRectangle implements Style {
  color: string = '';
}

或者用 getter/setter 形式:

复制代码
interface Style {
  color: string;
}

class StyledRectangle implements Style {
  private _color: string = '';

  get color(): string {
    return this._color;
  }

  set color(x: string) {
    this._color = x;
  }
}

8.2 接口继承接口

接口可以继承其他接口:

复制代码
interface Style {
  color: string;
}

interface ExtendedStyle extends Style {
  width: number;
}

ExtendedStyle 拥有 color + width 两个属性。

九、抽象类 vs 接口:什么时候用谁?

官方给出的差异可以总结成这几条(我顺便加一点自己的理解):

  1. 继承数量

    • 一个类只能 extends 一个抽象类;

    • 但可以 implements 多个接口:

      class Bird extends Animal implements CanFly, CanSwim {
      // ...
      }

  2. 静态成员

    • 接口里不能有静态方法和静态代码块;

    • 抽象类里可以:

      interface MyInterface {
      static staticMethod(): void; // ❌ 不允许
      static { console.info('static'); }; // ❌ 不允许
      }

      abstract class MyAbstractClass {
      static staticMethod(): void { console.info('static'); } // ✅
      static { console.info('static initialization block'); } // ✅
      }

  3. 是否允许方法实现

    • 接口:完全抽象,只能有方法声明,不能写实现;

    • 抽象类:既可以有抽象方法,也可以有写好实现的普通方法。

      abstract class MyAbstractClass {
      func(): void { console.info('func'); } // ✅ 可以有实现
      }

      interface MyInterface {
      func(): void { console.info('func'); } // ❌ 接口不能有实现
      }

  4. 构造函数

    • 抽象类可以有构造函数;

    • 接口不能有构造函数。

      abstract class MyAbstractClass {
      constructor() {} // ✅
      }

      interface MyInterface {
      constructor(); // ❌ 不允许
      }

我自己的经验总结:

  • 如果你需要共享一部分实现 + 统一抽象 + 需要构造/静态成员 → 用抽象类
  • 如果你只是想定义一组"行为约定",然后让不同类自由地各自实现 → 用接口
  • 很多时候,两者是配合使用的:
    • "一个抽象父类 + 若干接口 + 多个具体子类"。
相关推荐
拿破轮1 小时前
使用通义灵码解决复杂正则表达式替换字符串的问题.
java·服务器·前端
j***51891 小时前
Java进阶,时间与日期,包装类,正则表达式
java·mysql·正则表达式
程序员东岸1 小时前
《数据结构——排序(中)》选择与交换的艺术:从直接选择到堆排序的性能跃迁
数据结构·笔记·算法·leetcode·排序算法
WZTTMoon1 小时前
Spring Boot 启动全解析:4 大关键动作 + 底层逻辑
java·spring boot·后端
章鱼哥7301 小时前
[特殊字符] SpringBoot 自定义系统健康检测:数据库、Redis、表统计、更新时长、系统性能全链路监控
java·数据库·redis
Ccjf酷儿2 小时前
操作系统 蒋炎岩 4.数学视角的操作系统
笔记
深圳佛手2 小时前
Sharding-JDBC 和 Sharding-Proxy 区别
java
yinchao1632 小时前
EMC设计经验-笔记
笔记
kk哥88992 小时前
inout参数传递机制的底层原理是什么?
java·开发语言