上篇文章: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开发教程,涵盖从基础到进阶的各个知识点,敬请关注!