2018年6月19日 星期二

談一談從 Delphi 2009 之後就支援的重要功能 - 泛型 (Generic)

前言

在C++的語言基礎當中,除了物件導向、事件驅動的概念之外,模版設計(Template)也是非常重要的一環。然而,C++的開發人員能夠善用模版設計的並不多。模版設計這個好物,一般還有一個名稱,就是泛型 (Generic),這個好物在Delphi 2009 之後,也已經被加入到 Object Pascal裡面了,只是我們實在很少用到它。

然而,江湖一點訣,說破沒秘訣,大家對於泛型的少用,很多是因為不知道有這個功能,其次是知道有這個功能卻不知道怎麼使用。

所以,我們這一篇就來深入淺出的介紹一下『泛型』是什麼,順便用幾個簡單的範例來使用『泛型』吧。

泛型? 樣板? 揭起它的神祕面紗

所謂的泛型、樣板,其實就是在寫code的時候,把需要先定義好型別的宣告用一個關鍵字 <T> 來取代,未來真正在使用的時候,把T改成真正的型別,就可以讓這段code適用於多種不同的型別了。

這樣說明,如果您就聽懂了,那應該也不需要來看這篇文章,表示您的悟性頗高,屬於非常有能力的Programmer。(謎之音:喵的,聽的懂我跟你姓! 這不是跟我大學資料結構或物件導向程式設計老師說的一樣嘛?)

用實例來說明吧,我們說地球話,才不會被趕回火星..........

以前,寫資料結構作業的時候,或者用 Object Pascal 寫程式的時候,如果我們要用Delphi 來實作一個堆疊,我們通常會這麼想:

  • 堆疊嘛,資料要先進先出,所以要宣告一個陣列來儲存資料
  • 然後要定義一個 Push 方法,把資料放進去
  • 也要定義一個Pop方法,把資料取出來
  • 因為是堆疊,所以是後進先出 (最後放進去的要最先被取出,只有一個進出口)

可以用底下這張圖片來幫助思考:
以一個存放『整數』的堆疊來說,最最基本的宣告一般就會寫成這樣:
TMyStack = class (TObject)
private
   FElements: array[0..5] of Integer; // 用來存放元素的陣列.
public
   function push(element: integer) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: integer; // 直接傳回最後一個元素.

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

實作的程式碼我就不寫了,最近實在太多學生上網到處找作業的答案範本。

這段程式碼宣告了一個名為 TMyStack 的堆疊類別 (Stack Class),裡面是有很多問題的,例如 FElements 只能放 6 個整數,有元素個數的限制,因為我們前面說過這是一個存放『整數』的堆疊,所以 push 方法的參數是整數型別,pop 方法所回傳的資料也是整數型別。

先來解決資料長度限制的問題

我記不清是從 Delphi 5 還是 Delphi 7開始,Object Pascal就被賦予了可變長度陣列的功能,可以透過 setLength 來調整陣列的長度,宣告的寫法可以寫成:

var
   varLengIntArray : array of Integer;

調整長度的作法則是:
  setLength(varLengIntArray, 20);

後面的數字就是陣列調整後的長度。

這樣的作法,讓上面的整數堆疊陣列脫離了固定長度的限制,改寫過的 Class 宣告就會變成:
TMyStack = class (TObject)
private
   FElements: array of Integer; // 用來存放元素的陣列.
   FElementCount: integer;
public
   function push(element: integer) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: integer; // 直接傳回最後一個元素.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;
這樣修改以後,push跟pop方法裡面也都要有相對應的程式修改,例如在 Create的時候,就要先對 FElementCount 做初始化,push 跟 pop 方法裡面,也得調整長度:

==================================================================
constructor TMyStack.Create();
begin
   inherited Create();

  FElementCount := 0; // 初始化,把元素個數設為 0;
  setLength(FElements, 0); // 初始化,把陣列長度也設為 0;
end;

function TMyStack.push(element: integer) : integer;
begin
    Inc(self.FElementCount); // 把元素個數加一
    setLength(FElements, self.FElementCount); // 把陣列長度也多加一個

   self.FElements[self.FElementCount -1] := element; // 把要 push 的元素放在新增的
                                                                                    //  陣列位置上
end;

function TMyStack.pop: integer;
begin
    Result := self.FElements[self.FElementCount -1]; // 把最後一個元素回傳.

    Dec(self.FElementCount); // 把元素個數減一
    setLength(FElements, self.FElementCount); // 把陣列長度也多減掉一個
end;
==================================================================

這樣修改完以後,整數堆疊就沒有長度限制了。

但是,我們只需要整數堆疊嗎? 會不會明天要一個字串堆疊? 後天會不會要一個自定 record 或者 class 的堆疊?

如果每次需要堆疊,就要重寫一次上面的程式碼,而要修改的地方只有型別,那不是煩死人了?如果又好死不死遇到堆疊裡面要加一些額外的功能(客戶的想像力永遠走在我們前面, #壽山, 你說是吧?),那所有堆疊的程式碼要一個一個去修改,光想像就很想對電腦下毒手.........

那有沒有什麼方法,可以讓我們寫一個堆疊,就可以存放所有型別?

當然有,泛型,就是我們的救贖啊........
要使用泛型,我們得在 use 區段裡面引入 System.Generics.Collections,這裡面有非常多的好物可以用。

我們首先把前面已經改過的類別宣告,再做一些小調整,使用TArray<T> 這段程式碼來取代 array of Integer,讓 FElements 可以容納各種型別的資料:

TMyStack<T> = class (TObject)
private
   FElements: TArray<T>; // 用來存放元素的陣列.
   FElementCount: integer;
public
   function push(element: T) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: T; // 直接傳回最後一個元素.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

實作的程式碼則需要修改為:
==================================================================
constructor TMyStack<T>.Create();
begin
   inherited Create();

  FElementCount := 0; // 初始化,把元素個數設為 0;
  setLength(FElements, 0); // 初始化,把陣列長度也設為 0;
end;

function TMyStack<T>.push(element: T) : integer;
begin
    Inc(self.FElementCount); // 把元素個數加一
    setLength(FElements, self.FElementCount); // 把陣列長度也多加一個

   self.FElements[self.FElementCount -1] := element; // 把要 push 的元素放在新增的
                                                                                    //  陣列位置上
end;

function TMyStack<T>.pop: T;
begin
    Result := self.FElements[self.FElementCount -1]; // 把最後一個元素回傳.

    Dec(self.FElementCount); // 把元素個數減一
    setLength(FElements, self.FElementCount); // 把陣列長度也多減掉一個
end;
==================================================================

我的老天鵝啊,這真是太方便了吧,程式碼這樣寫就好了? 那使用上要怎麼用?
就這樣:
var
   integerStack : TMyStack<Integer>;
begin
    integerStack := TMyStack<Integer>.Create;
    try
        integerStack.push(79);
        integerStack.push(7);
        integerStack.push(21);
        integerStack.push(13); 
    finally
         integerStack.Free;
    end;
end; 

上述這段程式碼,在 finally執行以前,就會建立出以下圖為範例的堆疊資料了:

我們也可以做字串堆疊:
var

   stringStack : TMyStack<String>;
begin
    stringStack := TMyStack<String>.Create;
    try
        stringStack.push('這');
        stringStack.push('就');
        stringStack.push('是');
        stringStack.push('泛'); 
        stringStack.push('型');  
        stringStack.push('啊');  
    finally
         stringStack.Free;
    end;
end;

上述這段程式碼,在 finally執行以前,建立出來的堆疊資料則如下圖:
這樣一來,程式碼都沒有變,我們只在使用 TMyStack<T> 這個 Class 的時候,在宣告、建立Class的時候指明要使用什麼型別,就能夠自由的把一份程式碼用在各種不同型別上了,是不是很方便?

System.Generics.Collections 裡面,TList<T>更是好用,以前我們得要自己做TObjectList,才能透過所有物件都是從 TObject 衍生出來的特性建立出可以儲存物件的List,而且每次使用的時候還得做型別轉換才能正確使用。

現在透過 TList<T>,這些額外的程式碼、型別轉換的工作就都省下來了,甚至連TStack<T>, TQueue<T>, 也都有提供,是不是也讓您想要玩玩看了呢?

泛型說穿了,就是把原本我們需要先寫明的型別,用<T>這個關鍵字取代掉,而改以在實際宣告、使用的時候才敘明型別,這樣一來,真的省下好多好多程式碼,也省下很多時間可以做其他更有意義的事情了,當然,這些事情還是要我們自己去發掘的,大家加油!
 

沒有留言:

張貼留言