06 - 泛型-開課吧學習中心
阿新 • • 發佈:2022-03-08
# 泛型
泛型這一講提前了!主要是,學完這節就可以做事了。
## 泛型的概念
泛型, 或者說提取了一類事物的共性特徵的一種抽象。比如說,松樹、柳樹都是樹,在程式裡有3種表達:
- 介面(Interface)
- 繼承(Inheritance)
- 泛型(Generics)
**繼承是一種強表達。**
松樹繼承於樹,松樹同時也是木材。這樣關係的表達,要麼讓松樹多重整合(樹、木材),要麼松樹<-樹<-木材。
無論哪種,增加程式設計複雜度,也加強了**繼承關係的維護成本**(或者說耦合)。這麼看,關係太強,反而不好!
**介面是一種方面(Aspect)描述**。比如松樹可以生長,那麼松樹是:Growable;動植物都可以進化,那麼它們是Evolvable。
一個型別可以擁有多個方面的特性。
**泛型(Generics)**是對共性的提取(不僅僅是描述)。
```ts
class BedMaker<T> {
//....
make(){
}
}
const A = new BedMaker<紅木>()
const B = new BedMaker<桃木>()
```
- 木頭可以製造床,但是不是所有的木頭可以製造床
- 製造床()這個方法,放到木頭類中會很奇怪,因為木頭不僅僅可以製造床
- 同理,讓木頭繼承於“可以製造床”這個介面也很奇怪
奇怪的程式碼展示:
```tsx
class 紅木 implements IMakeBed{
makeBed(){...}
}
```
設計`IMakeBed` 的目標是為了拆分描述事物不同的方面(Aspect),其實還有一個更專業的詞彙——關注點(Interest Point)。拆分關注點的技巧,叫做關注點分離。如果僅僅用介面,不用泛型,那麼關注點沒有做到完全解耦。
**劃重點:**泛型是一種**抽象共性**(本質)的程式設計手段,它允許將**型別作為其他型別的引數**(表現形式),從而**分離不同關注點的實現**(作用)。
Array\<T\> 分離的是資料可以被線性訪問、儲存的共性。Stream\<T\>分離的是資料可以隨著時間產生的共性。Promise\<T\>分離的是資料可以被非同步計算的特性。
## Hello泛型
```tsx
// 一個identity函式是自己返回自己的函式
// 當然可以宣告它是:number -> number
function identity(arg: number): number {
return arg;
}
// 為了讓identity支援更多型別可以宣告它是any
function identity(arg: any): any {
return arg;
}
// any會丟失後續的所有檢查,因此可以考慮用泛型
function identity<Type>(arg: Type): Type {
return arg;
}
let output = identity<string>("MyString")
// 不用顯示的指定<>中的型別
// let output = identity("MyString")
output = 100 // Error
```
- <>叫做鑽石操作符,代表傳入的型別引數
## 泛型類
泛型類的例子。
```tsx
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
// (number, number) -> number
myGenericNumber.add = function (x, y) {
return x + y;
};
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
```
當然推薦將宣告(Declaration)和定義(Definition)寫到一起:
```tsx
class GenericNumber<T> {
zeroValue : T
constructor(v : T){
this.zeroValue = v
}
add(x : T, y : T) {
return x + y
}
}
```
## 泛型約束(Generic Constraints)
下面的程式會報錯:
```tsx
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
```
考慮為arg增加約束:
```tsx
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length);
return arg;
}
```
小技巧**keyof 操作符**
可以用keyof關鍵字作為泛型的約束。
```tsx
type Point = { x: number; y: number };
type P = keyof Point;
// P = "x" | "y"
```
如下面這個例子:
```tsx
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
```
為什麼可以這麼做?
對TS而言所有物件的key是靜態的。
```ts
const a = {x : 1, y : 2}
a.z = 3 // Error
```
因為是靜態的,所以可以用`keyof` 操作符求所有的key。如果一個物件的型別是`any` ,那麼keyof就沒有意義了。
## 例項化泛型型別(將類作為引數)
```ts
function create<Type>(c: { new (): Type }): Type {
return new c();
}
create(Foo) // Foo的例項
```
一個不錯的例子:
```tsx
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
```
## 總結
思考:什麼時候用介面?什麼時候用泛型?
思考:將型別作為引數傳遞,並例項化有哪些應用場景?
思考:這個程式會不會報錯?
```tsx
function add<T>(a : T, b : T){
return a + b
}
```