軟體工程的一個主要部分是構建不僅具有定義良好且一致的 API,而且還具有可重用性的元件。能夠處理當今資料以及未來資料的元件,將為您構建大型軟體系統提供最靈活的能力。
在 C# 和 Java 等語言中,建立可重用元件的主要工具之一是泛型,即能夠建立一個可以處理多種型別而不是單一型別的元件。這允許使用者消費這些元件並使用他們自己的型別。
泛型的 Hello World
首先,讓我們從泛型的“hello world”開始:恆等函式(identity function)。恆等函式是一個將返回任何傳入內容的函式。你可以將其類比為 echo 命令。
如果不使用泛型,我們必須給恆等函式指定一個特定的型別:
tsTryfunctionidentity (arg : number): number {returnarg ;}
或者,我們可以使用 any 型別來描述恆等函式:
tsTryfunctionidentity (arg : any): any {returnarg ;}
雖然使用 any 確實是泛型的,因為它使函式可以接受 arg 的所有型別,但我們實際上丟失了函式返回時該型別是什麼的資訊。如果我們傳入一個數字,我們唯一知道的資訊就是可以返回任何型別。
相反,我們需要一種捕獲引數型別的方法,以便我們可以用它來表示返回的內容。在這裡,我們將使用型別變數,這是一種在型別而非值上工作的特殊變數。
tsTryfunctionidentity <T >(arg :T ):T {returnarg ;}
我們現在為恆等函式添加了一個型別變數 T。這個 T 允許我們捕獲使用者提供的型別(例如 number),以便我們以後可以使用該資訊。在這裡,我們再次使用 T 作為返回型別。經檢查,我們現在可以看到引數和返回型別使用了相同的型別。這允許我們在函式的一側傳入型別資訊,並從另一側傳出。
我們稱這個版本的 identity 函式是泛型的,因為它適用於多種型別。與使用 any 不同,它與第一個使用數字作為引數和返回型別的 identity 函式一樣精確(即,它不會丟失任何資訊)。
編寫完泛型恆等函式後,我們可以透過兩種方式呼叫它。第一種方式是將所有引數(包括型別引數)傳遞給函式:
tsTryletoutput =identity <string>("myString");
在這裡,我們將 T 顯式設定為 string,作為函式呼叫的引數之一,並使用引數周圍的 <> 而不是 () 來表示。
第二種方式也許是最常見的。這裡我們使用型別引數推斷 —— 也就是說,我們希望編譯器根據我們傳入的引數型別自動為我們設定 T 的值:
tsTryletoutput =identity ("myString");
請注意,我們不必顯式地在尖括號 (<>) 中傳入型別;編譯器只是查看了值 "myString",並將其型別設定為 T。雖然型別引數推斷可以作為一個有用的工具來保持程式碼更短、更易讀,但在編譯器無法推斷型別的情況下(在更復雜的示例中可能會發生),你可能需要像上一個示例那樣顯式傳入型別引數。
使用泛型型別變數
當你開始使用泛型時,你會注意到當你建立像 identity 這樣的泛型函式時,編譯器會強制要求你在函式體內正確使用任何泛型型別引數。也就是說,你需要將這些引數視為它們可以是任何和所有型別。
讓我們使用之前的 identity 函式:
tsTryfunctionidentity <T >(arg :T ):T {returnarg ;}
如果我們還想在每次呼叫時將引數 arg 的長度記錄到控制檯呢?我們可能會想這樣寫:
tsTryfunctionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
當我們這樣做時,編譯器會報錯,指出我們正在使用 arg 的 .length 成員,但我們並沒有說明 arg 具有此成員。請記住,我們之前說過這些型別變數代表任何和所有型別,因此使用此函式的人可能傳入了一個 number,而它沒有 .length 成員。
假設我們實際上打算讓這個函式在 T 的陣列上工作,而不是直接在 T 上工作。由於我們在處理陣列,.length 成員應該是可用的。我們可以像建立其他型別的陣列一樣來描述它:
tsTryfunctionloggingIdentity <T >(arg :T []):T [] {console .log (arg .length );returnarg ;}
你可以這樣理解 loggingIdentity 的型別:“泛型函式 loggingIdentity 接收一個型別引數 T,以及一個作為 T 陣列的引數 arg,並返回一個 T 的陣列。” 如果我們傳入一個數字陣列,我們將得到一個數字陣列,因為 T 會繫結到 number。這允許我們將泛型型別變數 T 用作我們正在處理的型別的一部分,而不是整個型別,從而為我們提供了更大的靈活性。
或者,我們可以這樣寫示例:
tsTryfunctionloggingIdentity <T >(arg :Array <T >):Array <T > {console .log (arg .length ); // Array has a .length, so no more errorreturnarg ;}
你可能已經熟悉其他語言中這種型別的風格。在下一節中,我們將介紹如何建立自己的泛型型別,如 Array<T>。
泛型型別
在前面的章節中,我們建立了適用於多種型別的泛型恆等函式。在本節中,我們將探討函式本身的型別以及如何建立泛型介面。
泛型函式的型別與非泛型函式一樣,型別引數列在最前面,類似於函式宣告:
tsTryfunctionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <T >(arg :T ) =>T =identity ;
我們也可以在型別中使用不同的泛型型別引數名稱,只要型別變數的數量和使用方式保持一致即可。
tsTryfunctionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <U >(arg :U ) =>U =identity ;
我們也可以將泛型型別寫成物件字面量型別的呼叫簽名:
tsTryfunctionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : { <T >(arg :T ):T } =identity ;
這引導我們編寫第一個泛型介面。讓我們從上一個示例中取出物件字面量並將其移至介面中:
tsTryinterfaceGenericIdentityFn {<T >(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn =identity ;
在類似的示例中,我們可能希望將泛型引數作為整個介面的引數。這讓我們能夠看到我們正在泛化哪些型別(例如 Dictionary<string> 而不僅僅是 Dictionary)。這使得型別引數對介面的所有其他成員可見。
tsTryinterfaceGenericIdentityFn <T > {(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn <number> =identity ;
請注意,我們的示例已經發生了輕微的變化。我們現在不再描述一個泛型函式,而是擁有一個作為泛型型別一部分的非泛型函式簽名。當我們使用 GenericIdentityFn 時,我們也需要指定相應的型別引數(這裡:number),從而有效地鎖定底層呼叫簽名將使用的內容。瞭解何時將型別引數直接放在呼叫簽名上,以及何時將其放在介面本身上,將有助於描述型別的哪些方面是泛型的。
除了泛型介面,我們還可以建立泛型類。請注意,無法建立泛型列舉和名稱空間。
泛型類
泛型類的形狀與泛型介面類似。泛型類在類名後面有尖括號 (<>) 中的泛型型別引數列表。
tsTryclassGenericNumber <T > {zeroValue :T ;add : (x :T ,y :T ) =>T ;}letmyGenericNumber = newGenericNumber <number>();myGenericNumber .zeroValue = 0;myGenericNumber .add = function (x ,y ) {returnx +y ;};
這是 GenericNumber 類的一個非常字面的用法,但你可能已經注意到沒有任何東西限制它只能使用 number 型別。我們本可以使用 string 甚至是更復雜的物件。
tsTryletstringNumeric = newGenericNumber <string>();stringNumeric .zeroValue = "";stringNumeric .add = function (x ,y ) {returnx +y ;};console .log (stringNumeric .add (stringNumeric .zeroValue , "test"));
正如介面一樣,將型別引數放在類本身上,可以確保類的所有屬性都使用相同的型別。
正如我們在類章節中所述,類有兩種型別的側面:靜態側和例項側。泛型類僅在其例項側是泛型的,而不是靜態側,因此在處理類時,靜態成員不能使用類的型別引數。
泛型約束
如果你還記得前面的一個示例,你有時可能想編寫一個泛型函式,該函式適用於一組型別,並且你知道該組型別將具備哪些功能。在我們的 loggingIdentity 示例中,我們希望能夠訪問 arg 的 .length 屬性,但編譯器無法證明每個型別都有 .length 屬性,因此它警告我們不能做出這種假設。
tsTryfunctionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
我們不想處理任何和所有型別,而是希望將此函式限制為僅適用於也具有 .length 屬性的型別。只要型別有這個成員,我們就允許它,但它必須至少有這個成員。為此,我們必須將我們的要求作為 T 可以是什麼的約束列出。
為此,我們將建立一個描述我們約束的介面。在這裡,我們將建立一個具有單個 .length 屬性的介面,然後我們將使用此介面和 extends 關鍵字來表示我們的約束:
tsTryinterfaceLengthwise {length : number;}functionloggingIdentity <T extendsLengthwise >(arg :T ):T {console .log (arg .length ); // Now we know it has a .length property, so no more errorreturnarg ;}
因為泛型函式現在受到了約束,它將不再適用於任何和所有型別:
tsTryArgument of type 'number' is not assignable to parameter of type 'Lengthwise'.2345Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.loggingIdentity (3 );
相反,我們需要傳入型別具有所有必需屬性的值:
tsTryloggingIdentity ({length : 10,value : 3 });
在泛型約束中使用型別引數
你可以宣告一個由另一個型別引數約束的型別引數。例如,在這裡我們想根據名稱從物件中獲取屬性。我們想確保不會意外獲取 obj 上不存在的屬性,因此我們將在這兩個型別之間放置一個約束:
tsTryfunctiongetProperty <T ,K extends keyofT >(obj :T ,key :K ) {returnobj [key ];}letx = {a : 1,b : 2,c : 3,d : 4 };getProperty (x , "a");Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.2345Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.getProperty (x ,"m" );
在泛型中使用類型別
當在 TypeScript 中使用泛型建立工廠時,有必要透過建構函式來引用類型別。例如:
tsTryfunctioncreate <T >(c : { new ():T }):T {return newc ();}
一個更高階的示例使用 prototype 屬性來推斷和約束建構函式與類型別的例項側之間的關係。
tsTryclassBeeKeeper {hasMask : boolean;}classZooKeeper {nametag : string;}classAnimal {numLegs : number;}classBee extendsAnimal {keeper :BeeKeeper ;}classLion extendsAnimal {keeper :ZooKeeper ;}functioncreateInstance <A extendsAnimal >(c : new () =>A ):A {return newc ();}createInstance (Lion ).keeper .nametag ;createInstance (Bee ).keeper .hasMask ;