【HarmomyOS6】ArkTS入门(三)

上篇文章:https://blog.csdn.net/qq_37566395/article/details/157254007

接下来我们讲一下接口和泛型类型。

一、接口

接下来,让我们深入了解ArkTS中一个非常重要的概念------接口

什么是接口?

想象一下生活中的"插座标准":无论什么品牌的电器,只要插头符合标准,就能插入插座使用。接口就是代码世界的"标准",它定义了一组规则,告诉类:"你必须实现这些方法和属性,我才能认可你"。

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

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

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

示例1:基础接口声明

typescript 复制代码
// 定义一个"样式"接口
interface Style {
  color: string; // 属性声明
}

// 定义一个"计算面积"接口
interface AreaCalculator {
  calculateArea(): number; // 方法声明
  showInfo(): void;        // 另一个方法声明
}

示例2:实现接口的类

typescript 复制代码
// 接口定义
interface AreaCalculator {
  calculateArea(): number;
  showInfo(): void;
}

// 实现接口的矩形类
class Rectangle implements AreaCalculator {
  private width: number = 0;
  private height: number = 0;
  
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
  
  showInfo(): void {
    console.info(`矩形尺寸:${this.width} × ${this.height}`);
  }
  
  calculateArea(): number {
    this.showInfo(); // 调用另一个方法
    return this.width * this.height;
  }
}

// 使用示例
const rect = new Rectangle(10, 5);
console.log(`面积:${rect.calculateArea()}`); // 输出:面积:50

1.1 接口属性

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

示例1:基础属性声明

typescript 复制代码
// 接口定义颜色属性
interface VehicleStyle {
  color: string;  // 简单属性
}

示例2:getter/setter形式的等价写法

typescript 复制代码
// 以下两种接口定义是等价的

// 方式1:简写形式
interface VehicleStyle {
  color: string;
}

// 方式2:完整getter/setter形式
interface VehicleStyle {
  get color(): string;
  set color(value: string);
}

属性字段只是getter/setter对的便捷写法。以下表达方式是等价的:

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

示例3:实现接口的不同方式

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

class StyledRectangle implements Style {
  color: string = '';
}
typescript 复制代码
class Car implements VehicleStyle {
  color: string = '红色';
}

// 使用私有属性和getter/setter
class Motorcycle implements VehicleStyle {
  private _color: string = '黑色';
  
  get color(): string { 
    return this._color; 
  }
  
  set color(value: string) { 
    console.log(`摩托车颜色更改为:${value}`);
    this._color = value; 
  }
}

// 使用示例
const myCar = new Car();
myCar.color = '蓝色'; // 直接赋值

const myBike = new Motorcycle();
myBike.color = '银色'; // 通过setter赋值,会输出日志

1.2 接口继承

接口可以继承其他接口,就像孩子继承父母的特征一样。

示例:接口继承的实际应用

typescript 复制代码
// 基础接口:基本的车辆特征
interface Vehicle {
  brand: string;
  startEngine(): void;
}

// 扩展接口:添加更多特征
interface Car extends Vehicle {
  numberOfDoors: number;
  honk(): void;
}

// 扩展接口:另一种类型的车辆
interface Motorcycle extends Vehicle {
  hasSidecar: boolean;
  wheelie(): void;
}

// 实现扩展接口的类
class Sedan implements Car {
  brand: string = 'Aito';
  numberOfDoors: number = 4;
  
  startEngine(): void {
    console.log(`${this.brand}轿车引擎启动`);
  }
  
  honk(): void {
    console.log('滴滴!');
  }
}

// 使用示例
const mySedan = new Sedan();
mySedan.startEngine(); // 输出:Aito轿车引擎启动
mySedan.honk();        // 输出:滴滴!

继承接口包含被继承接口的所有属性和方法,还可以添加自己的属性和方法。

1.3 抽象类和接口

关键区别对比:

特性 抽象类 接口
继承数量 只能继承一个 可以实现多个
方法实现 可以有具体实现 只能有声明,不能有实现
构造函数 可以有 不能有
静态成员 可以有静态方法和代码块 不能有静态成员

抽象类与接口都无法实例化。抽象类是类的抽象,抽象类用来捕捉子类的通用特性,接口是行为的抽象。在ArkTS语法中抽象类与接口的区别如下:

区别一:继承数量限制

  • 抽象类:一个类只能继承一个抽象类(单继承)
  • 接口:一个类可以实现多个接口(多实现)
typescript 复制代码
// 定义抽象类
abstract class Animal {
  abstract makeSound(): void;
}

// 定义接口
interface CanFly {
  fly(): void;
}

interface CanSwim {
  swim(): void;
}

// Bird类:继承一个抽象类,实现多个接口
class Bird extends Animal implements CanFly, CanSwim {
  makeSound(): void {
    console.info('叽叽喳喳');
  }
  
  fly(): void {
    console.info('展翅高飞');
  }
  
  swim(): void {
    console.info('水中嬉戏');
  }
}

// 使用示例
const sparrow = new Bird();
sparrow.makeSound();  // 输出:叽叽喳喳
sparrow.fly();        // 输出:展翅高飞
sparrow.swim();       // 输出:水中嬉戏

通俗解释

  • 一个孩子只能有一个亲生父亲(抽象类:单继承)
  • 但一个孩子可以学习多种技能,如唱歌、跳舞、画画(接口:多实现)

区别二:静态成员的支持

  • 抽象类:可以有静态方法和静态代码块
  • 接口:不能包含静态成员
typescript 复制代码
// 错误示例:接口中不能包含静态成员
interface Logger {
  // 以下两行都会报错,接口不能包含静态成员
  
  // static log(message: string): void;  // 错误:接口不能有静态方法声明
  
  // static {                           // 错误:接口不能有静态代码块
  //   console.info('初始化日志接口');
  // }
}

// 正确示例:抽象类可以有静态成员
abstract class Logger {
  // 静态方法
  static log(message: string): void {
    console.info(`[日志] ${message}`);
  }
  
  // 静态代码块(在类加载时执行)
  static {
    console.info('日志系统初始化完成');
  }
  
  // 抽象方法
  abstract saveLog(): void;
}

// 继承抽象类
class FileLogger extends Logger {
  saveLog(): void {
    console.info('日志已保存到文件');
  }
}

// 使用示例
Logger.log('应用程序启动');  // 输出:[日志] 应用程序启动
const logger = new FileLogger();
logger.saveLog();           // 输出:日志已保存到文件

区别三:方法的实现

  • 抽象类:可以包含具体实现的方法
  • 接口:只能声明方法,不能包含实现
typescript 复制代码
// 错误示例:接口不能包含方法实现
interface Calculator {
  // 以下写法是错误的,接口只能声明不能实现
  // add(a: number, b: number): number {
  //   return a + b;  // 错误:接口不能有方法体
  // }
  
  // 正确的接口声明(只有方法签名)
  add(a: number, b: number): number;
}

// 正确示例:抽象类可以包含方法实现
abstract class Calculator {
  // 具体实现的方法
  add(a: number, b: number): number {
    return a + b;
  }
  
  // 抽象方法(子类必须实现)
  abstract multiply(a: number, b: number): number;
}

// 实现抽象类
class AdvancedCalculator extends Calculator {
  multiply(a: number, b: number): number {
    return a * b;
  }
}

// 使用示例
const calc = new AdvancedCalculator();
console.log(calc.add(5, 3));       // 输出:8
console.log(calc.multiply(5, 3));  // 输出:15

区别四:构造函数

  • 抽象类:可以有构造函数
  • 接口:不能有构造函数
typescript 复制代码
// 错误示例:接口不能有构造函数
interface Person {
  // constructor(name: string);  // 错误:接口不能声明构造函数
  
  getName(): string;
}

// 正确示例:抽象类可以有构造函数
abstract class Person {
  protected name: string;
  
  // 抽象类的构造函数
  constructor(name: string) {
    this.name = name;
    console.info(`创建Person实例:${name}`);
  }
  
  abstract introduce(): void;
  
  getName(): string {
    return this.name;
  }
}

// 继承抽象类
class Student extends Person {
  private grade: string;
  
  // 调用父类构造函数
  constructor(name: string, grade: string) {
    super(name);  // 必须调用父类构造函数
    this.grade = grade;
  }
  
  introduce(): void {
    console.info(`我是${this.name},${this.grade}年级学生`);
  }
}

// 使用示例
const student = new Student('小明', '三');
// 输出:创建Person实例:小明
student.introduce();  // 输出:我是小明,三年级学生

二、泛型类型和函数

在ArkTS中,泛型是一种强大的特性,它允许我们编写可以适应多种数据类型的代码,同时保持类型安全。可以把泛型想象成一种"代码模板"------写一次代码,就能适用于多种类型。

2.1 泛型类和接口

什么是泛型?

泛型就像是一个"万能容器"。现实生活中,一个普通的杯子只能装水,但一个"万能杯子"可以装水、咖啡、果汁等各种饮料,而且你还能明确知道里面装的是什么。

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

typescript 复制代码
// 定义一个泛型类CustomStack,Element是类型参数(就像标签)
class CustomStack<Element> {
  private items: Element[] = [];  // 内部用数组存储元素
  
  // 入栈方法:接收一个Element类型的参数
  public push(e: Element): void {
    this.items.push(e);
    console.log(`入栈:${e}`);
  }
  
  // 出栈方法:返回Element类型
  public pop(): Element | undefined {
    return this.items.pop();
  }
}

// 使用这个泛型类时,必须指定具体的类型(给标签写上具体内容)
let stringStack = new CustomStack<string>();  // 创建一个字符串栈
stringStack.push('hello');    // 正确:可以放入字符串
stringStack.push('world');    // 正确:可以放入字符串

let numberStack = new CustomStack<number>();  // 创建一个数字栈
numberStack.push(100);        // 正确:可以放入数字
numberStack.push(200);        // 正确:可以放入数字

// 类型安全检查:编译器会检查类型是否匹配
let s = new CustomStack<string>();
s.push('hello');              // 正确:类型匹配
// s.push(55);                // 错误:类型不匹配!55是number,不是string

2.2 泛型约束

有时候我们不是什么东西都往盒子里放,而是有条件的

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

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

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

2.3 泛型函数

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

示例:

typescript 复制代码
// 问题:我们经常需要写很多类似的函数
function lastNumber(x: number[]): number {
  return x[x.length - 1];
}

function lastString(x: string[]): string {
  return x[x.length - 1];
}

function lastBoolean(x: boolean[]): boolean {
  return x[x.length - 1];
}

// 每次都要为新类型写一个新函数,太麻烦了!

// 解决方案:使用泛型函数
function last<T>(x: T[]): T {
  return x[x.length - 1];
}

// 现在一个函数就能处理所有类型!

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

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

typescript 复制代码
// 泛型函数定义
function last<T>(x: T[]): T {
  return x[x.length - 1];
}

// 使用方法1:显式指定类型参数(明确告诉函数是什么类型)
let res1: string = last<string>(['aa', 'bb']);
console.log(res1);  // 输出:bb

let res2: number = last<number>([1, 2, 3]);
console.log(res2);  // 输出:3

// 使用方法2:隐式类型推断(让编译器自己猜类型)
let res3 = last(['hello', 'world']);   // 编译器推断T是string
console.log(res3);  // 输出:world

let res4 = last([true, false]);        // 编译器推断T是boolean
console.log(res4);  // 输出:false

let res5 = last([1, 2, 3, 4, 5]);      // 编译器推断T是number
console.log(res5);  // 输出:5

2.4 泛型默认值

为什么需要默认值?

有时候我们希望泛型有一个"备选方案",当用户不指定类型时,就使用默认类型。

泛型类型的类型参数可以设置默认值,这样无需指定实际类型实参,直接使用泛型类型名称即可。以下示例展示了类和函数的这一特性。

示例1:****泛型类的默认值

typescript 复制代码
// 带有默认类型的泛型类
class Container<T = string> {
  private value: T;
  
  constructor(initialValue: T) {
    this.value = initialValue;
  }
  
  getValue(): T {
    return this.value;
  }
  
  setValue(newValue: T): void {
    this.value = newValue;
    console.log(`值更新为:${newValue}`);
  }
}

// 使用默认类型(string)
const defaultContainer = new Container('初始文本');
console.log(defaultContainer.getValue());  // 输出:初始文本

// 指定不同类型
const numberContainer = new Container<number>(100);
console.log(numberContainer.getValue());   // 输出:100

const booleanContainer = new Container<boolean>(true);
console.log(booleanContainer.getValue());  // 输出:true

示例2:泛型函数的默认值

typescript 复制代码
// 带有默认类型的泛型函数
function makeArray<T = number>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

// 使用默认类型(number)
const defaultArray = makeArray(3, 0);
console.log(defaultArray);  // 输出:[0, 0, 0]

// 指定不同类型
const stringArray = makeArray<string>(2, 'hello');
console.log(stringArray);   // 输出:['hello', 'hello']

const booleanArray = makeArray<boolean>(4, true);
console.log(booleanArray);  // 输出:[true, true, true, true]

三、空安全

ArkTS通过严格的空安全机制,在编译阶段就尽可能发现潜在的空指针问题,而不是等到应用运行时才崩溃,这大大提升了应用的稳定性。

默认情况下,ArkTS中的所有类型都不允许为空,这类似于TypeScript的(strictNullChecks)模式,但规则更严格。

简单来说,ArkTS中,类型默认不可为空 指的是类型注解本身不允许包含 **<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">null</font>**,但我们可以通过联合类型显式声明可空 ,并且初始化赋值为 **<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">null</font>**是允许的

在下面的示例中,所有行都会导致编译时错误:

typescript 复制代码
let x: number = null;    // 编译时错误
let y: string = null;    // 编译时错误
let z: number[] = null;  // 编译时错误

// 正确做法:明确声明可为空的类型
let age: number | null = null;   // 明确表示age可以是数字或null
let name: string | null = "张三"; // 先赋值为字符串,后续可以改为null

可以为空值的变量定义为联合类型T | null。

typescript 复制代码
let x: number | null = null;
x = 1;    // ok
x = null; // ok
if (x != null) { /* do something */ }

具体应用示例:

typescript 复制代码
// 用户信息示例
class User {
  id: number;
  nickname: string | null = null;
  
  constructor(id: number) {
    this.id = id;
  }
  
  // 显示用户显示名:如果有昵称显示昵称,否则显示"用户"+ID
  getDisplayName(): string {
    if (this.nickname != null) {
      return this.nickname;
    }
    return `用户${this.id}`;
  }
}

// 使用示例
let currentUser: User = new User(1001);
currentUser.nickname = "小明";  // 可以设置昵称
currentUser.nickname = null;    // 也可以清空昵称

3.1 非空断言运算符

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

当应用于可空类型的值时,编译时类型会变为非空类型。例如,类型从T | null变为T:

typescript 复制代码
class A {
  value: number = 0;
}


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

注意:

  • <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">!</font>运算符告诉编译器:"我知道这里不为空,你不用担心"
  • 如果用错了(实际上值为空),应用会在运行时崩溃
  • 只在确定不为空的情况下使用,或者配合空检查后使用

3.2 空值合并运算符

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

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

在以下示例中,getNick方法返回已设置的昵称。如果未设置,则返回空字符串。

typescript 复制代码
class Person {
  // ...
  nick: string | null = null;
  getNick(): string {
    return this.nick ?? '';
  }
}

可选链

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

typescript 复制代码
// 公司组织架构示例
class Employee {
  name: string;
  manager?: Employee;  // 可选属性:可能没有上级
  
  constructor(name: string) {
    this.name = name;
  }
  
  // 获取上级的姓名(如果存在)
  getManagerName(): string | undefined {
    return this.manager?.name;  // 如果manager为null/undefined,返回undefined
  }
  
  // 多层可选链
  getDepartmentManager(): string | undefined {
    // 安全地访问多层级属性
    return this.manager?.department?.director?.name;
  }
}

// 使用示例
let alice = new Employee("Alice");
let bob = new Employee("Bob");
bob.manager = alice;

console.log(bob.getManagerName());      // 输出:"Alice"
console.log(alice.getManagerName());    // 输出:undefined

// 与普通链式调用的对比
console.log(bob.manager?.name);         // 安全:输出"Alice"
console.log(bob.manager!.name);         // 安全:但需要确定manager存在
// console.log(bob.manager.name);       // 编译错误:manager可能为undefined

如果在学习过程中遇到任何问题,欢迎在评论区留言交流。

后续我将持续更新更多HarmonyOS开发教程,涵盖从基础到进阶的各个知识点,敬请关注!

相关推荐
[H*]2 小时前
Flutter框架跨平台鸿蒙开发——ListView Widget基础用法
flutter·华为·harmonyos
A懿轩A2 小时前
【2026 最新】Kuikly 编译开发 OpenHarmony 项目逐步详细教程带图操作Android Studio编译(Windows)
windows·harmonyos·鸿蒙·openharmony·kuikly
[H*]2 小时前
Flutter框架跨平台鸿蒙开发——Button样式定制
flutter·华为·harmonyos
不会写代码0002 小时前
Flutter 框架跨平台鸿蒙开发 - 全国图书馆查询:探索知识的殿堂
flutter·华为·harmonyos
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——每日谚语APP的开发流程
flutter·华为·harmonyos·鸿蒙
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——脑筋急转弯小游戏的开发流程
flutter·华为·harmonyos·鸿蒙
m0_685535082 小时前
Zemax光学设计偶次非球面优化技巧
华为·光学·光学设计·光学工程·镜头设计
不爱吃糖的程序媛3 小时前
React Native 0.77.1 适配鸿蒙(RN-OH)信息总览
react native·react.js·harmonyos
时光慢煮3 小时前
打造跨端浮动操作按钮:Flutter × OpenHarmony 实战
flutter·华为·开源·openharmony