ArkTS 泛型編程指南:提升代碼複用性與類型安全

引言

在 HarmonyOS ArkTS 開發中,泛型是一種強大的編程工具,它允許我們編寫可重用的代碼,同時保持類型安全。通過泛型,我們可以創建適用於多種數據類型的組件、函數和類,而不必為每種類型重複編寫相似的代碼。本文將深入探討 ArkTS 中泛型的概念、用法和最佳實踐,幫助開發者構建更加靈活和健壯的應用程序。

官方參考資料:

  • HarmonyOS ArkTS 語言參考
  • TypeScript 泛型文檔

泛型基礎概念

什麼是泛型?

泛型是一種編程語言特性,它允許在定義函數、接口或類時不預先指定具體的類型,而在使用時再指定類型的一種特性。這種方式可以讓代碼更加靈活,提高代碼的複用性。

在 ArkTS 中,泛型使用尖括號 <T> 來表示類型參數,其中 T 是一個類型變量,可以在使用時被具體的類型替代。

為什麼需要泛型?

在沒有泛型的情況下,如果我們想要創建一個適用於多種類型的函數,通常有以下幾種方法:

  1. 使用 any 類型:失去類型檢查的好處
  2. 為每種類型重載函數:代碼重複,維護困難
  3. 使用聯合類型:限制了類型的範圍

泛型解決了這些問題,它允許我們在保持類型安全的同時編寫通用的代碼。

ArkTS 泛型語法

泛型函數

泛型函數是最基本的泛型用法,它允許我們創建可以處理多種類型數據的函數。

// 泛型函數基本語法
function identity<T>(arg: T): T {
  return arg;
}

// 使用泛型函數
let output1 = identity<string>("Hello ArkTS"); // 明確指定類型
let output2 = identity(42); // 類型推斷

泛型類

泛型類允許我們在類的定義中使用類型參數,使得類可以處理不同類型的數據。

// 泛型類基本語法
class GenericClass<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }

  setValue(value: T): void {
    this.value = value;
  }
}

// 使用泛型類
let numberClass = new GenericClass<number>(100);
let stringClass = new GenericClass<string>("Hello");

泛型接口

泛型接口定義了可以接受類型參數的接口,使得接口更加靈活。

// 泛型接口基本語法
interface GenericInterface<T> {
  data: T;
  getData(): T;
}

// 實現泛型接口
class StringImpl implements GenericInterface<string> {
  data: string;

  constructor(data: string) {
    this.data = data;
  }

  getData(): string {
    return this.data;
  }
}

泛型的高級用法

多個類型參數

ArkTS 支持在一個泛型定義中使用多個類型參數,用逗號分隔。

// 多個類型參數的泛型函數
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

// 使用
let result = pair<string, number>("count", 42);

泛型約束

泛型約束用於限制泛型參數必須滿足的條件,確保泛型類型具有某些特定的屬性或方法。

// 定義一個接口作為約束
interface Lengthwise {
  length: number;
}

// 使用約束的泛型函數
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 現在我們知道它有一個 .length 屬性,所以不會報錯
  return arg;
}

// 有效使用
loggingIdentity("Hello"); // 字符串有 length 屬性
loggingIdentity([1, 2, 3]); // 數組有 length 屬性

// 無效使用
// loggingIdentity(42);  // 數字沒有 length 屬性,會報錯

泛型默認類型

在 ArkTS 中,我們可以為泛型參數指定默認類型,這樣在不明確指定類型時會使用默認類型。

// 帶有默認類型的泛型函數
function createArray<T = number>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

// 使用默認類型
let numbers = createArray(5, 0); // number[]

// 明確指定類型
let strings = createArray<string>(3, "default"); // string[]

泛型工具類型

ArkTS 提供了一些內置的泛型工具類型,用於常見的類型轉換操作。

Partial

將類型 T 的所有屬性變為可選。

interface User {
  id: number;
  name: string;
  age: number;
}

// 所有屬性變為可選
let partialUser: Partial<User> = { name: "張三" };
Required

將類型 T 的所有屬性變為必需。

interface PartialConfig {
  host?: string;
  port?: number;
}

// 所有屬性變為必需
let requiredConfig: Required<PartialConfig> = {
  host: "localhost",
  port: 8080,
};
Readonly

將類型 T 的所有屬性變為只讀。

interface Config {
  apiKey: string;
  timeout: number;
}

// 所有屬性變為只讀
let readonlyConfig: Readonly<Config> = {
  apiKey: "abc123",
  timeout: 5000,
};

// 嘗試修改會報錯
// readonlyConfig.timeout = 10000;  // 錯誤
Record<K, T>

創建一個類型,其屬性鍵為 K,屬性值為 T。

// 創建一個字符串鍵、數字值的對象類型
let scores: Record<string, number> = {
  Alice: 95,
  Bob: 87,
  Charlie: 92,
};
Pick<T, K>

從類型 T 中選擇一組屬性 K。

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

// 只選擇 name 和 age 屬性
let personInfo: Pick<Person, "name" | "age"> = {
  name: "李四",
  age: 25,
};
Omit<T, K>

從類型 T 中排除一組屬性 K。

// 排除 id 和 address 屬性
let personBasic: Omit<Person, "id" | "address"> = {
  name: "王五",
  age: 30,
};

ArkTS 中的泛型組件

泛型組件基礎

在 ArkTS 中,我們可以創建接受類型參數的組件,使其能夠適應不同類型的數據。

@Component
struct GenericList<T> {
  @State items: T[] = [];

  build() {
    Column() {
      ForEach(this.items, (item: T, index: number) => {
        Text(`Item ${index}: ${JSON.stringify(item)}`)
          .fontSize(16)
          .margin(10)
      }, (item: T, index: number) => JSON.stringify(item) + index)
    }
  }
}

// 使用泛型組件
@Component
struct GenericComponentDemo {
  build() {
    Column() {
      GenericList<string>({ items: ["Apple", "Banana", "Orange"] })
      GenericList<number>({ items: [1, 2, 3, 4, 5] })
    }
  }
}

帶約束的泛型組件

我們可以為泛型組件添加約束,確保傳入的類型滿足特定條件。

interface Displayable {
  name: string;
  toString(): string;
}

@Component
struct DisplayList<T extends Displayable> {
  @State items: T[] = [];

  build() {
    Column() {
      ForEach(this.items, (item: T, index: number) => {
        Text(`${index + 1}. ${item.name}`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .margin(8)
      }, (item: T, index: number) => item.name + index)
    }
  }
}

// 使用帶約束的泛型組件
class Product implements Displayable {
  name: string;
  price: number;

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  toString(): string {
    return `${this.name} - ¥${this.price}`;
  }
}

@Component
struct ProductList {
  build() {
    DisplayList<Product>({
      items: [
        new Product("手機", 3999),
        new Product("電腦", 7999),
        new Product("平板", 2999)
      ]
    })
  }
}

泛型組件中的事件處理

泛型組件可以定義泛型事件,使事件回調能夠處理特定類型的數據。

@Component
struct SelectableList<T> {
  @State items: T[] = [];
  onSelect: (item: T) => void;

  build() {
    Column() {
      ForEach(this.items, (item: T, index: number) => {
        Text(JSON.stringify(item))
          .fontSize(16)
          .margin(10)
          .padding(10)
          .backgroundColor(0xF0F0F0)
          .onClick(() => {
            this.onSelect(item);
          })
      }, (item: T, index: number) => JSON.stringify(item) + index)
    }
  }
}

@Component
struct EventDemo {
  @State selectedItem: any = null;

  build() {
    Column() {
      SelectableList<{ id: number; name: string }>({
        items: [
          { id: 1, name: "選項1" },
          { id: 2, name: "選項2" },
          { id: 3, name: "選項3" }
        ],
        onSelect: (item) => {
          this.selectedItem = item;
          console.log(`選中了: ${item.name}`);
        }
      })

      if (this.selectedItem) {
        Text(`當前選中: ${this.selectedItem.name}`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .marginTop(20)
      }
    }
  }
}

泛型在數據結構中的應用

泛型棧實現

使用泛型實現一個棧數據結構,可以存儲任意類型的元素。

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

  // 添加元素到棧頂
  push(item: T): void {
    this.items.push(item);
  }

  // 移除並返回棧頂元素
  pop(): T | undefined {
    return this.items.pop();
  }

  // 返回棧頂元素但不移除
  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  // 判斷棧是否為空
  isEmpty(): boolean {
    return this.items.length === 0;
  }

  // 獲取棧的大小
  size(): number {
    return this.items.length;
  }

  // 清空棧
  clear(): void {
    this.items = [];
  }
}

// 使用泛型棧
let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop()); // 輸出: 3

let stringStack = new Stack<string>();
stringStack.push("Hello");
stringStack.push("ArkTS");
console.log(stringStack.peek()); // 輸出: ArkTS

泛型隊列實現

使用泛型實現一個隊列數據結構。

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

  // 入隊
  enqueue(item: T): void {
    this.items.push(item);
  }

  // 出隊
  dequeue(): T | undefined {
    return this.items.shift();
  }

  // 返回隊首元素
  front(): T | undefined {
    return this.items[0];
  }

  // 判斷隊列是否為空
  isEmpty(): boolean {
    return this.items.length === 0;
  }

  // 獲取隊列大小
  size(): number {
    return this.items.length;
  }

  // 清空隊列
  clear(): void {
    this.items = [];
  }
}

// 使用泛型隊列
let messageQueue = new Queue<{ id: number; content: string }>();
messageQueue.enqueue({ id: 1, content: "消息1" });
messageQueue.enqueue({ id: 2, content: "消息2" });
console.log(messageQueue.dequeue()?.content); // 輸出: 消息1

泛型映射實現

使用泛型實現一個簡單的映射(字典)數據結構。

class GenericMap<K, V> {
  private map: Map<K, V> = new Map<K, V>();

  // 添加鍵值對
  set(key: K, value: V): void {
    this.map.set(key, value);
  }

  // 獲取值
  get(key: K): V | undefined {
    return this.map.get(key);
  }

  // 刪除鍵值對
  delete(key: K): boolean {
    return this.map.delete(key);
  }

  // 判斷鍵是否存在
  has(key: K): boolean {
    return this.map.has(key);
  }

  // 獲取所有鍵
  keys(): IterableIterator<K> {
    return this.map.keys();
  }

  // 獲取所有值
  values(): IterableIterator<V> {
    return this.map.values();
  }

  // 獲取所有鍵值對
  entries(): IterableIterator<[K, V]> {
    return this.map.entries();
  }

  // 獲取映射大小
  size(): number {
    return this.map.size;
  }

  // 清空映射
  clear(): void {
    this.map.clear();
  }
}

// 使用泛型映射
let userMap = new GenericMap<string, { name: string; age: number }>();
userMap.set("user1", { name: "張三", age: 28 });
userMap.set("user2", { name: "李四", age: 32 });
console.log(userMap.get("user1")?.name); // 輸出: 張三

泛型與異步操作

泛型 Promise 處理

泛型可以與 Promise 結合使用,使異步操作的返回類型更加明確。

// 定義返回特定類型的異步函數
async function fetchData<T>(url: string): Promise<T> {
  // 模擬網絡請求
  return new Promise<T>((resolve, reject) => {
    setTimeout(() => {
      // 模擬返回數據
      resolve({} as T);
    }, 1000);
  });
}

// 定義數據接口
interface UserData {
  id: number;
  name: string;
  email: string;
}

// 使用泛型異步函數
async function processUserData() {
  try {
    const userData: UserData = await fetchData<UserData>(
      "https://api.example.com/user"
    );
    console.log(userData.name); // 類型安全
  } catch (error) {
    console.error("獲取數據失敗:", error);
  }
}

泛型與 Future/Promise 工具函數

創建泛型工具函數來處理 Promise 操作,提高代碼的複用性。

// 泛型工具函數:延遲執行
function delay<T>(ms: number, value: T): Promise<T> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

// 泛型工具函數:重試異步操作
async function retry<T>(
  operation: () => Promise<T>,
  maxAttempts: number = 3,
  delayMs: number = 1000
): Promise<T> {
  let lastError: Error;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;
      console.log(`嘗試 ${attempt} 失敗,${delayMs}ms 後重試...`);
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }

  throw lastError!;
}

// 使用示例
async function fetchWithRetry() {
  try {
    const result = await retry(() =>
      fetchData<{ success: boolean }>("https://api.example.com/retry")
    );
    console.log("獲取成功:", result);
  } catch (error) {
    console.error("多次嘗試後仍然失敗:", error);
  }
}

泛型最佳實踐

命名約定

為了提高代碼的可讀性,建議遵循以下泛型參數命名約定:

  1. 單個字符的泛型參數用於簡單情況:
  • T: 通用類型
  • K: 鍵類型
  • V: 值類型
  • E: 元素類型
  1. 描述性的泛型參數名用於複雜情況:
  • TItem: 項目類型
  • TUser: 用户類型
  • TProduct: 產品類型

避免過度泛型化

雖然泛型提供了靈活性,但過度使用會使代碼變得複雜難以理解。建議:

  • 僅在確實需要處理多種類型時使用泛型
  • 如果一個函數只處理特定類型,不要使用泛型
  • 保持泛型定義簡單明瞭

合理使用泛型約束

使用泛型約束可以提高代碼的類型安全性:

  • 總是為泛型參數添加必要的約束,確保它們具有所需的屬性和方法
  • 使用接口來定義約束,使代碼更加清晰
  • 避免使用過於寬鬆的約束,如 anyobject

類型推斷的使用

利用 ArkTS 的類型推斷功能可以簡化代碼:

// 不必顯式指定類型
function identity<T>(arg: T): T {
  return arg;
}

// 類型推斷,不需要顯式指定 string 類型
let result = identity("Hello"); // 類型推斷為 string

泛型與具體類型的平衡

在設計 API 時,需要平衡泛型的靈活性和具體類型的明確性:

  • 公共 API 通常應該提供具體的類型版本,提高易用性
  • 內部工具函數可以使用更多的泛型,提高複用性
  • 考慮為複雜的泛型 API 提供更簡單的包裝器

實際應用案例

通用表單驗證器

使用泛型創建一個通用的表單驗證器,可以驗證不同結構的表單數據。

// 驗證規則接口
interface ValidationRule<T> {
  validate: (value: T) => boolean;
  errorMessage: string;
}

// 泛型表單驗證器
class FormValidator<T extends Record<string, any>> {
  private rules: Map<keyof T, ValidationRule<any>[]> = new Map();

  // 添加驗證規則
  addRule<K extends keyof T>(field: K, rule: ValidationRule<T[K]>): void {
    if (!this.rules.has(field)) {
      this.rules.set(field, []);
    }
    this.rules.get(field)!.push(rule);
  }

  // 驗證表單數據
  validate(data: T): { isValid: boolean; errors: Record<string, string[]> } {
    const errors: Record<string, string[]> = {};

    // 遍歷所有規則
    this.rules.forEach((fieldRules, field) => {
      const value = data[field];
      const fieldErrors: string[] = [];

      // 驗證每個規則
      fieldRules.forEach((rule) => {
        if (!rule.validate(value)) {
          fieldErrors.push(rule.errorMessage);
        }
      });

      if (fieldErrors.length > 0) {
        errors[field as string] = fieldErrors;
      }
    });

    return {
      isValid: Object.keys(errors).length === 0,
      errors,
    };
  }
}

// 示例:用户註冊表單
interface UserRegistration {
  username: string;
  email: string;
  password: string;
}

// 創建驗證器
const userValidator = new FormValidator<UserRegistration>();

// 添加驗證規則
userValidator.addRule("username", {
  validate: (value) => value.length >= 3 && value.length <= 20,
  errorMessage: "用户名長度必須在3-20個字符之間",
});

userValidator.addRule("email", {
  validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  errorMessage: "請輸入有效的郵箱地址",
});

userValidator.addRule("password", {
  validate: (value) => value.length >= 8,
  errorMessage: "密碼長度至少為8個字符",
});

// 驗證數據
const formData: UserRegistration = {
  username: "user123",
  email: "invalid-email",
  password: "123",
};

const result = userValidator.validate(formData);
console.log(result);
// 輸出:
// {
//   isValid: false,
//   errors: {
//     email: ["請輸入有效的郵箱地址"],
//     password: ["密碼長度至少為8個字符"]
//   }
// }

通用狀態管理

使用泛型創建一個簡單的狀態管理工具,可以管理不同類型的狀態。

// 泛型狀態管理類
class StateManager<T> {
  private state: T;
  private listeners: Set<(state: T) => void> = new Set();

  constructor(initialState: T) {
    this.state = initialState;
  }

  // 獲取當前狀態
  getState(): T {
    return { ...this.state } as T; // 返回副本避免直接修改
  }

  // 更新狀態
  setState(newState: Partial<T>): void {
    this.state = { ...this.state, ...newState } as T;
    this.notifyListeners();
  }

  // 替換整個狀態
  replaceState(newState: T): void {
    this.state = newState;
    this.notifyListeners();
  }

  // 添加狀態變化監聽器
  subscribe(listener: (state: T) => void): () => void {
    this.listeners.add(listener);
    // 立即調用監聽器,提供當前狀態
    listener(this.getState());

    // 返回取消訂閲函數
    return () => {
      this.listeners.delete(listener);
    };
  }

  // 通知所有監聽器
  private notifyListeners(): void {
    const stateCopy = this.getState();
    this.listeners.forEach((listener) => {
      listener(stateCopy);
    });
  }
}

// 示例:用户狀態管理
interface UserState {
  isLoggedIn: boolean;
  userInfo: { name: string; id: string } | null;
  loading: boolean;
}

// 創建狀態管理器
const userStateManager = new StateManager<UserState>({
  isLoggedIn: false,
  userInfo: null,
  loading: false,
});

// 訂閲狀態變化
const unsubscribe = userStateManager.subscribe((state) => {
  console.log("用户狀態更新:", state);
});

// 更新狀態
userStateManager.setState({ loading: true });
// 模擬登錄
setTimeout(() => {
  userStateManager.setState({
    isLoggedIn: true,
    userInfo: { name: "張三", id: "user123" },
    loading: false,
  });
}, 1000);

// 不再需要時取消訂閲
// unsubscribe();

總結

泛型是 ArkTS 中一個強大的特性,它允許我們編寫更加靈活、可複用且類型安全的代碼。通過本文的學習,我們瞭解了:

  1. 泛型的基本概念和語法
  2. 如何在函數、類、接口中使用泛型
  3. 泛型的高級用法,如約束、默認類型和工具類型
  4. 泛型在組件開發和數據結構實現中的應用
  5. 泛型與異步操作的結合
  6. 泛型編程的最佳實踐

合理使用泛型可以顯著提高代碼的質量和可維護性。在 HarmonyOS ArkTS 應用開發中,掌握泛型編程技巧將幫助你構建更加健壯和靈活的應用程序。

擴展閲讀

  • HarmonyOS ArkTS 類型系統
  • TypeScript 泛型深入講解
  • 設計模式與泛型結合


需要參加鴻蒙認證的請點擊 鴻蒙認證鏈接