ArkTS语法适配最佳实践之“泛型编程”

ArkTS语法适配最佳实践之“泛型编程”

HarmonyOS
2024-06-11 20:14:38
浏览
收藏 0
回答 1
待解决
回答 1
按赞同
/
按时间
hmogy

一:什么是泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而是

在使用的时候再指定类型的一种特性

泛型   --->   广泛的类型

二:泛型的好处

1.

代码复用性

组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型。

不同的类型处理逻辑可以复用同一段代码,而不用复制粘贴

2.

类型安全性(与any的区别)

设计泛型的关键目的是在

成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型能帮助我们在灵活适用不同类型的同时,不丢失必要的类型约束。

三:泛型的语法

function identity <T>(value: T) : T { 
  return value; 
} 
  
console.log(identity<Number>(1)) // 1

对于刚接触 TypeScript 泛型的读者来说,首次看到 <T> 语法会感到陌生。但这没什么可担心的,

就像传递参数一样,我们传递了我们想要用于特定函数调用的类型

参考上面的图片,当我们调用 identity<Number>(1) ,

Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。

其中 T 代表

Type,在定义泛型时通常用作第一个类型变量名称。但

实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  •  K(Key):表示对象中的键类型;
  •  V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

function identity <T, U>(value: T, message: U) : T { 
  console.log(message); 
  return value; 
} 
  
console.log(identity<Number, string>(68, "Semlinker"));

除了为类型变量显式设定值之外,一种更常见的做法是

使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

function identity <T, U>(value: T, message: U) : T { 
  console.log(message); 
  return value; 
} 
  
console.log(identity(68, "Semlinker"));

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。

泛型参数默认类型与普通函数默认值类似,对应的语法很简单,即 <T=Default Type>,对应的使用示例如下:

interface A<T=string> { 
  name: T; 
} 
  
const strA: A = { name: "Semlinker" }; 
const numB: A<number> = { name: 101 };

四:整改示例

1. 使用泛型解决参数类型不确定的 any 问题

public getArray(array: Array<any>): Array<any> { 
    let itemList: Array<any> = array; 
    if (ArrayUtil.isEmpty(itemList)) { 
      itemList = [{}]; 
    } 
    itemList = itemList.map((item, index: number) => { 
      let result: any = { 
        i: index + 1, 
        item: item 
      } 
      return result; 
    }) 
    return itemList; 
  }

这段代码用于修改数组元素的结构,使之附带索引值。为了适用于不同类型的数组,原来使用了 any。但这样就丢失了类型约束,比如我们传入 number 类型的数组,返回数组的元素类型变为 string,也不会收到报错提示。

基于我们这段代码的逻辑,我们实际的需求是返回数组元素的类型跟输入数组元素的类型一定是相同,只是元素的结构变化了。

为了达到这一约束,我们可以通过泛型改造如下:

1) 我们首先为新的元素结构定义一个新的 interface,因为这个结构也是类型无关的,所以也可以用到泛型

interface IndexedItem<T>{ 
       i?: number, 
       item?: T 
}

这个 interface 表示我们的类型可能带有 number类型的索引,以及字段名为 item 的值字段,其类型待确定。只需要加入 ? 可选标识是为了适配空数组的情况

2) 基于 IndexedItem 改造 getArray 方法

public getArray<T>(array: Array<T>): Array<IndexedItem<T>> { 
    let itemList: Array<IndexedItem<T>> = []; 
    if (ArrayUtil.isEmpty(itemList)) { 
      itemList = [{}]; 
    } 
    itemList = itemList.map((item: T, index: number) => { 
      let result: IndexedItem<T>= { 
        i: index + 1, 
        item: item 
      } 
      return result; 
    }) 
    return itemList; 
}

2.  多泛型参数与 extend

export class LimitRefreshDataUtil { 
  private taskId: number = -1; 
  private page: number = 0; 
  private task: string = ""; 
  private actionData: any = {}; 
  private resultCallBack: (resultPage: number, limit: number, result: any) => boolean = () => false; 
  private prepareCallBack: () => void = () => {}; 
  
  private isForeground: boolean = true; 
  private currentPage: number = 1; 
  private refreshedPages: Array<number> = []; 
  private paused: boolean = false; 
  
  constructor(task: string, resultCallBack: (resultPage: number, limit: number, result: any) => boolean, actionData?: any) { 
  } 
  
  private queryPage(page: number, limit: number, callback?: (isEnd: boolean) => void) { 
    let actionData: any = {}; 
    HiLog.i(TAG, this.task + ", queryPage, page:" + page + ",limit:" + limit); 
  
    if (this.actionData) { 
      Object.assign(actionData, this.actionData); 
    } 
    actionData.page = page; 
    actionData.limit = limit; 
    } 
}

这里有2个问题:

1) 这里涉及多个 any 参数,查询入参 actionData 与结果回调里的 result 明显是无关的两个类型,所以我们需要分别指定泛型参数

2) queryPage 方法里,我们会给 actionData 设置 page 跟 limit 属性,但泛型类型是待定的任意类型,我们怎么要求它支持 page 跟 limit 呢? 这就是 extends 这个关键字的作用,我们在申明 actionData 的类型时,就申明了它应该满足的约束条件,具体看下面的泛型改造代码

export interface PageLimit { 
  page?: number, 
  limit?: number 
} 
  
  
export class LimitRefreshDataUtil<T extends PageLimit, R> { 
  private taskId: number = -1; 
  private page: number = 0; 
  private task: string = ""; 
  private actionData?: T; 
  private resultCallBack: (resultPage: number, limit: number, result: R) => boolean = () => false; 
  private prepareCallBack: () => void = () => { 
  }; 
  private isForeground: boolean = true; 
  private currentPage: number = 1; 
  private refreshedPages: Array<number> = []; 
  private paused: boolean = false; 
  
  constructor(task: string, resultCallBack: (resultPage: number, limit: number, result: R) => boolean, actionData?: T) { 
  } 
  
  private queryPage(page: number, limit: number, callback?: (isEnd: boolean) => void) { 
    const actionData: PageLimit | T = { 
      page, 
      limit 
    }; 
    if (this.actionData) { 
      ObjectUtil.assign(actionData, this.actionData); 
    } 
    HiLog.i(TAG, this.task + ", queryPage, page:" + page + ",limit:" + limit); 
    const reqData: ReqData<PageLimit | T> = { 
      actionData: actionData 
    } 
}

有时候,我们希望类型变量对应的类型上存在某些属性。这时,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。

一个很好的例子是在处理字符串或数组时,我们会假设 length 属性是可用的。让我们再次使用 identity 函数并尝试输出参数的长度:

function identity<T>(arg: T): T { 
  console.log(arg.length); // Error 
  return arg; 
}

在这种情况下,编译器将不会知道 T 确实含有 length 属性,尤其是在可以将任何类型赋给类型变量 T 的情况下。我们需要做的就是让类型变量 extends 一个含有我们所需属性的接口,比如这样:

interface Length { 
  length: number; 
} 
  
function identity<T extends Length>(arg: T): T { 
  console.log(arg.length); // 可以获取length属性 
  return arg; 
}

T extends Length 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。之后,当我们使用不含有 length 属性的对象作为参数调用 identity 函数时,TypeScript 会提示相关的错误信息:

identity(68); // Error 
// Argument of type '68' is not assignable to parameter of type 'Length'.(2345)

3. 检查对象上的键是否存在

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 keyof 操作符,

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。"耳听为虚,眼见为实",我们来举个 keyof 的使用示例:

interface Person { 
  name: string; 
  age: number; 
  location: string; 
} 
  
type K1 = keyof Person; // "name" | "age" | "location" 
type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ... 
type K3 = keyof { [x: string]: Person };  // string | number

通过 keyof 操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。具体的使用方式如下:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 
  return obj[key]; 
}

在以上的 getProperty 函数中,我们通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单调用 let value = obj[key]; 不同。

下面我们来看一下如何使用 getProperty 函数:

enum Difficulty { 
  Easy, 
  Intermediate, 
  Hard 
} 
  
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 
  return obj[key]; 
} 
  
let tsInfo = { 
   name: "Typescript", 
   supersetOf: "Javascript", 
   difficulty: Difficulty.Intermediate 
} 
  
let difficulty: Difficulty =  
  getProperty(tsInfo, 'difficulty'); // OK 
  
let supersetOf: string =  
  getProperty(tsInfo, 'superset_of'); // Error

在以上示例中,对于 getProperty(tsInfo, 'superset_of') 这个表达式,TypeScript 编译器会提示以下错误信息:

Argument of type '"superset_of"' is not assignable to parameter of type  
'"difficulty" | "name" | "supersetOf"'.(2345)

很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。

分享
微博
QQ
微信
回复
2024-06-12 16:46:43
相关问题
HarmonyOS jsBridge 最佳实践
147浏览 • 1回复 待解决
HarmonyOS ArkTS中如何实现构造函数
346浏览 • 1回复 待解决
网络监听的最佳实践有哪些?
361浏览 • 1回复 待解决
应用内整体换肤的最佳实践
267浏览 • 1回复 待解决
web写入customUseragent最佳实践
109浏览 • 1回复 待解决
HarmonyOS 沉浸式状态栏最佳实践
208浏览 • 1回复 待解决
参数转换为Object编译失败
572浏览 • 1回复 待解决
输出静态库的最佳实践
32浏览 • 1回复 待解决
视频和直播播放HarmonyOS最佳实践
543浏览 • 1回复 待解决
HarmonyOS 单列和瀑布流的最佳实践
96浏览 • 1回复 待解决
APP内整体置灰的最佳实践
178浏览 • 1回复 待解决
如何实现ArkTS高性能编程
373浏览 • 1回复 待解决
HarmonyOS类的属性如何初始化?
355浏览 • 1回复 待解决
ArkTS 的异步编程模型是如何实现的?
56浏览 • 0回复 待解决