4、ArkTS语言基础(下):深入理解高级类型、泛型、模块化编程和状态管理的基本概念

好,咱们接着聊。上一节我们把 ArkTS 的基础类型和语法过了一遍,算是把地基打牢了。这一节,咱们要往上盖楼了——高级类型、泛型、模块化,还有状态管理。这几个概念,说白了就是让你写的代码更灵活、更安全、更好维护。

我个人习惯,学新东西一定要先理解它「解决了什么问题」。不然你光记语法,转头就忘。咱们一个一个来。

4.1 高级类型:不只是简单的 string 和 number

你想想看,实际开发中,一个变量可能不止一种类型。比如一个接口返回的数据,成功时是一个对象,失败时是一个错误信息。这时候怎么办?

4.1.1 联合类型(Union Types)

联合类型用 | 符号表示,意思是「可以是这个,也可以是那个」。我在项目中遇到过不少这样的场景,比如用户输入框,可能接收字符串,也可能接收数字。

let input: string | number;
input = "Hello";
input = 42; // 这样也行

嗯,这里要注意:当你使用联合类型时,TypeScript 只会允许你调用所有类型共有的方法。比如 string | number,你只能调用 toString(),因为两者都有。如果你要调用 length,那就得先做类型收窄。

4.1.2 交叉类型(Intersection Types)

交叉类型用 & 符号,意思是「同时满足多个类型」。说白了就是把多个类型合并成一个。我经常用它来组合多个接口。

interface Person {
  name: string;
  age: number;
}

interface Employee {
  employeeId: number;
  department: string;
}

type Staff = Person & Employee;

const staff: Staff = {
  name: "张三",
  age: 30,
  employeeId: 1001,
  department: "研发部"
};

你看,Staff 类型同时拥有了 PersonEmployee 的所有属性。这在做多设备协同开发时特别有用——比如一个设备信息对象,可能同时包含基础信息和设备专属信息。

4.1.3 类型别名(Type Aliases)

类型别名就是给一个类型起个新名字。用 type 关键字。我个人习惯用它来简化复杂的类型定义。

type ID = string | number;
type Point = { x: number; y: number };

let userId: ID = "abc123";
let position: Point = { x: 10, y: 20 };
小技巧: 类型别名和接口(interface)很像,但接口可以扩展(extends),类型别名不行。如果你需要定义对象的结构,优先用接口;如果你需要定义联合类型或交叉类型,用类型别名。

4.2 泛型:让代码更通用

泛型,说白了就是「类型参数化」。你写一个函数或类,不把类型写死,而是让调用者来决定。我刚开始学的时候觉得这玩意儿有点绕,但用多了就发现,它真的太香了。

4.2.1 泛型函数

举个例子,你想写一个函数,返回传入的数组的第一个元素。如果不使用泛型,你得为每种类型写一个重载。有了泛型,一行搞定。

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = first([1, 2, 3]); // num 的类型是 number | undefined
const str = first(["a", "b"]); // str 的类型是 string | undefined

你看,T 就像一个占位符,调用时自动推断出具体类型。我在鸿蒙的多设备数据同步模块中,就用泛型写了一个通用的缓存工具,不管存什么类型的数据,都能保证类型安全。

4.2.2 泛型约束

有时候,你不能让 T 太「自由」。比如你想写一个函数,获取对象的 length 属性。但并不是所有类型都有 length。这时候就需要约束。

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello"); // 5
logLength([1, 2, 3]); // 3
// logLength(123); // 报错:number 没有 length 属性
注意: 泛型约束不是万能的。我曾经在项目中遇到过一个坑:用泛型约束了接口,但传入的对象在运行时缺少某些属性,导致崩溃。所以,泛型约束只保证编译时的类型安全,运行时还是要做防御性检查。

4.2.3 泛型类与泛型接口

泛型不仅可以用在函数上,类、接口也都可以。比如鸿蒙中的 ArrayList<T> 就是一个典型的泛型类。

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2

4.3 模块化编程:把代码拆开,再拼起来

模块化,说白了就是「分而治之」。一个文件只做一件事,然后通过 importexport 把它们串起来。鸿蒙的 ArkTS 完全支持 ES Module 标准。

4.3.1 导出与导入

你可以导出变量、函数、类、接口等。我个人习惯用命名导出,因为 IDE 的自动补全支持得更好。

// mathUtils.ts
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

// main.ts
import { add, PI } from './mathUtils';
console.log(add(PI, 2)); // 5.14159

4.3.2 默认导出

每个模块只能有一个默认导出。我一般只在主组件或主类上使用默认导出。

// Logger.ts
export default class Logger {
  log(msg: string) {
    console.log(msg);
  }
}

// app.ts
import Logger from './Logger';
const logger = new Logger();
logger.log("Hello");

4.3.3 模块化在鸿蒙中的实践

在鸿蒙多设备协同开发中,模块化尤其重要。比如你可以把网络请求、数据模型、UI组件分别放在不同模块中。这样,手机端和平板端可以共享数据模型和网络层,只替换 UI 层。

核心思想: 模块化不只是为了代码整洁,更是为了「复用」和「隔离」。复用是指同一份代码可以在多个设备上跑;隔离是指修改一个模块不会影响其他模块。

4.4 状态管理:让数据流动起来

状态管理,说白了就是「数据怎么存、怎么改、怎么通知 UI 更新」。在鸿蒙中,ArkTS 提供了 @State@Prop@Link 等装饰器来管理状态。

4.4.1 @State:组件的本地状态

@State 装饰的变量,一旦发生变化,UI 会自动重新渲染。我刚开始用的时候觉得这很神奇,后来理解了它的原理——其实就是观察者模式。

@Entry
@Component
struct Counter {
  @State count: number = 0;

  build() {
    Column() {
      Text(`计数: ${this.count}`)
        .fontSize(30)
      Button('增加')
        .onClick(() => {
          this.count++;
        })
    }
  }
}

你看,点击按钮后 count 增加,UI 自动更新。不需要手动操作 DOM,这就是声明式编程的魅力。

4.4.2 @Prop:父子组件传值

@Prop 用于父组件向子组件传递数据。子组件可以读取,但不能修改父组件的值。

@Component
struct ChildComponent {
  @Prop message: string;

  build() {
    Text(this.message)
      .fontSize(20)
  }
}

@Entry
@Component
struct ParentComponent {
  build() {
    ChildComponent({ message: "来自父组件的问候" })
  }
}

4.4.3 @Link:双向绑定

@Link 用于父子组件之间的双向数据同步。子组件修改了数据,父组件也会同步更新。

@Component
struct ChildComponent {
  @Link count: number;

  build() {
    Button('子组件增加')
      .onClick(() => {
        this.count++;
      })
  }
}

@Entry
@Component
struct ParentComponent {
  @State count: number = 0;

  build() {
    Column() {
      Text(`父组件计数: ${this.count}`)
      ChildComponent({ count: this.count })
    }
  }
}
避坑指南: 我曾经在项目中使用 @Link 时,不小心在子组件中直接赋值(this.count = 10),导致父组件的状态被意外覆盖。记住:@Link 是引用传递,修改时要谨慎。

4.5 本章知识体系总览

为了让你更直观地理解这一章的知识结构,我画了一张图。你可以看到,高级类型、泛型、模块化和状态管理,它们之间是层层递进的关系。

ArkTS 语言基础(下) 高级类型 联合类型 | 交叉类型 | 类型别名 泛型 泛型函数 | 泛型约束 | 泛型类/接口 模块化编程 导出/导入 | 默认导出 | 模块拆分 状态管理 @State | @Prop | @Link 目标:写出类型安全、可复用、易维护的 ArkTS 代码

这一节的内容确实不少,但都是硬核干货。高级类型让你的类型定义更灵活;泛型让你的代码更通用;模块化让你的项目结构更清晰;状态管理让你的 UI 和数据同步更简单。把这四个点吃透了,你在鸿蒙开发中就能写出更优雅、更健壮的代码。


专注资料整理