軟體工程的一個主要部分就是構建組件,構建的組件不僅需要具有明確的定義和統一的介面,同時也需要組件可復用。支持現有的數據類型和將來添加的數據類型的組件為大型軟體系統的開發過程提供很好的靈活性。 在C#和Java中,可以使用"泛型"來創建可復用的組件,並且組件可支持多種數據類型。這樣便可以讓用戶根據自己
軟體工程的一個主要部分就是構建組件,構建的組件不僅需要具有明確的定義和統一的介面,同時也需要組件可復用。支持現有的數據類型和將來添加的數據類型的組件為大型軟體系統的開發過程提供很好的靈活性。
在C#和Java中,可以使用"泛型"來創建可復用的組件,並且組件可支持多種數據類型。這樣便可以讓用戶根據自己的數據類型來使用組件。
泛型的簡單案例
首先,用泛型寫一個"Hello World":identity函數。identity函數將會返回我們傳入的數據。你可以認為它是個"echo"命令。
不用泛型,我們也不用給identity函數指定類型:
function identity(arg: number): number { return arg; }
或者,我們可以給identity函數指定"any"類型:
function identity(arg: any): any { return arg; }
雖然使用"any"類型的時候可以接收任何類型的"arg"參數,但是實際上已經失去函數返回值類型的信息。假如我們傳入一個number,我們只知道返回任何類型的值都是可以的。
所以,我們需要一直方式來捕捉參數的類型,也可以用它來表示返回值的類型。這裡使用的是"類型變數",一種特殊的變數,代表的是類型而非值。
function identity<T>(arg: T): T { return arg; }
現在我們已經為identity函數添加了類型變數"T"。"T"允許捕獲用戶提供的參數類型(如:number),以便我們稍後可以使用該類型。然後我們再次用"T"作為返回值的類型。現在我們可以看到,同一類型被用來作為參數類型和返回值類型。
我們稱這個版本的identity函數為泛型,它可用於多種類型。與使用"any"類型不同,它和第一個identity函數(使用number作為參數類型和返回值類型)一樣精準(它不會失去任何信息)。
一旦我們定義了泛型函數,有兩種方法可以使用。第一種就是傳入所有的參數,包括類型參數:
var output = identity<string>("myString"); // output的類型將會是 'string'
在這裡,我們明確的將"T"指定為string,作為函數中傳入的參數,使用<>包裹該參數而非()。
第二種是最常見的。我們使用/類型推斷/,我們希望編譯器根據傳入的參數自動為"T"指定類型。
var output = identity("myString"); // output的類型將會是 'string'
註意,我們並未顯示的給尖括弧<>內傳入類型,編譯器檢查"myString",然後將"T"設置為它的類型。雖然類型推斷是個很實用的工具,也能夠使代碼簡短易讀,但你還是需要跟前面的例子一樣明確的傳遞類型參數,因為可能會存在複雜的函數,使得編譯器未能正確的進行類型推斷。
使用泛型
當你開始使用泛型,你可能會註意到當你創建一個類似"identity"的泛型函數,編譯器會強制要求你在函數中正確的使用這些通用類型參數。也就是說,你真的把這些參數視為可以是任何類型的。
再看看之前的identity函數:
function identity<T>(arg: T): T { return arg; }
想要每次調用的時候在控制台列印出"arg"參數的length。我們可以嘗試這麼寫:
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // 錯誤: T 不存在 .length return arg; }
當我們這麼做的時候,編譯器會拋出一個錯誤提示我們使用"arg"的".length"屬性,但是沒有地方指定過"arg"的".length"屬性。之前我們說類型變數代表了所有類型,所有可能使用這個函數的時候會傳入一個"number"類型的值,而"number"是沒".length"屬性的。
實際上,我們想讓這個函數接受的參數是個"T"類型的數組而非直接"T"。當傳入的是數組,length屬性便是可用的了。我們可以像創建其他數組類型一樣:
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // 數組中存在 .length,所以沒報錯 return arg; }
你可以這樣理解loggingIdentity函數:loggingIdentity泛型函數,參數類型是"T",參數"arg"是個類型為"T"的數組,返回的也是個類型為"T"的數組。如果我們傳入一個都是數字的數組,那麼我們也會得到一個都是數字的數組,因為這時候"T"類型已經綁定為number了。這使得我們可以使用類型變數"T"作為我們使用的類型的一部分,而非全部類型,這也更具靈活性。
我們可以通過這種方式寫個例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // 數組中存在 .length,所以沒報錯 return arg; }
泛型類型
在前面例子中,我們創建了通用的identity函數,可以使用於不同的類型。現在,我們將探討函數類型及如何創建泛型介面。
泛型函數的類型與非泛型函數一樣,只是最前面放上一個類型參數,類似與聲明函數:
function identity<T>(arg: T): T { return arg; } var myIdentity: <T>(arg: T)=>T = identity;
我們也可以給類型中的泛型類型參數指定不同的名稱,只要類型變數的數量和其使用方式都能對應的上。
function identity<T>(arg: T): T { return arg; } var myIdentity: <U>(arg: U)=>U = identity;
我們也可以使用對象字面量的簽名調用來寫泛型類型:
function identity<T>(arg: T): T { return arg; } var myIdentity: {<T>(arg: T): T} = identity;
下麵開始寫第一個泛型介面。用上個例子中的對象字面量來寫介面:
interface GenericIdentityFn<T> { (arg: T): T; } function identity<T>(arg: T): T { return arg; } var myIdentity: GenericIdentityFn<number> = identity; var num = myIdentity(10); // 正確,因為類型是number var str = myIdentity("10") // 錯誤,參數類型不是number
註意,我們的例子稍微有些改變。我們把非泛型函數簽名作為泛型類型的一部分,而不是去描述泛型函數。當我們使用GenericIdentityFn,我們還需要指定對應的類型參數(這裡是number),有效的鎖定在底層簽名調用時會用到的類型。理解"何時將類型參數直接放到簽名調用"和"何時將它放到介面上"將有助於描述哪部分類型屬於泛型。
除了泛型介面,我們還可以創建泛型類。請註意,不可能創建泛型枚舉和模塊。
泛型類
泛型類和泛型介面相似。泛型類在類名後面使用尖括弧<>包含泛型類型參數列表。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } var myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 1; myGenericNumber.add = function(x, y) { return x + y; }; alert(myGenericNumber.add(myGenericNumber.zeroValue, 1)); // 2
這是對"GenericNumber"類想當直觀的使用,你也可能註意到並未限制只能使用"number"類型。我們可以使用"string"抑或更複雜的對象。
var stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = "Hello "; stringNumeric.add = function(x, y) { return x + y; }; alert(stringNumeric.add(stringNumeric.zeroValue, "World")); // Hello World
和介面一樣,將類型參數放在類之後來告訴我們類的所有屬性都是同一個類型。
正如前面"類"那一節所描述的,一個類由兩部分組成:靜態部分和實例部分。泛型類僅屬於實例部分,所以當我們使用類的時候,靜態成員不能使用類的類型參數。
泛型的限制
如果你還記得之前的例子,有時候你想要寫一個泛型函數來操作一組類型,並且你是知道這些類型具有什麼功能。
在"loggingIdentity"例子中,我們希望能夠訪問"arg"的".length"屬性,但是編譯器不能確定每個類型都有".length"屬性,所以它將報錯提示我們不能這麼做。
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // 錯誤: T 不存在 .length return arg; }
相對於處理任何類型或者所有類型,我們更希望強制去要求函數去處理帶有".length"屬性的任何類型或者所有類型。只要該類型有這個成員(屬性),我們便運行通過,也就是必須包含這個指定的成員(屬性)。
既然需要這麼做,我們就創建一個描述限制的介面。在這裡,先創建一個只有單個屬性".length"的介面,然後使用這個介面和"extends"關鍵字來指明限制:
interface hasLength { length: number; } function loggingIdentity<T extends hasLength>(arg: T): T { console.log(arg.length); // 現在我們知道它含有.length屬性,並且不報錯 return arg; }
因為這個泛型函數現在是有限制的,所以它不在支持任何類型或者所有類型:
loggingIdentity(3); // 錯誤,number不包含.length屬性
因此,我們需要傳入其類型具有所需屬性的值:
loggingIdentity({length: 10, value: 3}); // 正確
在泛型中使用類類型
當在TypeScript中使用泛型創建工廠函數的時候,需要引用其構造函數的類類型。
class Greeter{ greeter:string = "Hello World"; } function create<T>(c: {new(): T}): T { return new c(); } var newGreeter = create<Greeter>(Greeter);