變來變去的Generic Type: 泛型介紹

泛型(Generic Type)是一個C#語言的功能,它可以讓你在定義ClassMethodInterface時先不用決定型別,到了要實體化的時候再決定其型別,這在集合的應用(System.Collections.Generic)上更為重要,因為集合通常只是容器而已,只需要訂定巡覽、新增、刪除元素...等的方法,而訂定這些方法並不需要知道元素的型別,管它是字串還是數字,跟容器本身的實作並沒有關係,因此集合用泛型實作是最佳的選擇,而LINQ就是一個集合的應用方法,當然就用到了大量的Generic Type。

箱子工廠

為了說明泛型的好處,我們來講一間箱子工廠的故事吧。

有間箱子工廠平常都是生產正方形的箱子:

class Program
{
    static void Main(string[] args)
    {
        BoxFactory boxFactory = new BoxFactory();
        boxFactory.Start();
    }
}

class BoxFactory
{
    public BoxFactory()
    {
        Console.WriteLine("這是一間箱子工廠");
    }

    class SquareBoxMaker
    {
        public SquareBoxMaker()
        {
            Console.WriteLine("正方形箱子製造機建置完成");
        }

        public SquareBox GetSquareBox()
        {
            Console.WriteLine("產生正方形箱子");
            return new SquareBox();
        }
    }

    public void Start()
    {
        Console.WriteLine("工廠開始運作");
        SquareBoxMaker maker = new SquareBoxMaker();
        maker.GetSquareBox();
    }
}

運作的結果如下:

/* 
 * 這是一間箱子工廠
 * 工廠開始運作
 * 正方形箱子製造機建置完成
 * 產生正方形箱子
 */

有一天工廠的老闆接到一個大客戶的訂單,興奮地跑到工廠跟廠長說了這個消息。

老闆: 廠長,我剛剛接到一個要生產大量三角形箱子的訂單啦~~。

廠長: 可是這個客戶它要的是三角形的箱子,工廠裡沒這樣的機器阿!!

老闆: 這是一個大客戶,沒關係,多買一台吧。

於是工廠裡就多了一台製造三角形箱子的機器:

class BoxFactory
{
    public BoxFactory()
    {
        Console.WriteLine("這是一間箱子工廠");
    }
    class SquareBoxMaker
    {
        ...
    }

    class TriangleBoxMaker
    {
        public TriangleBoxMaker()
        {
            Console.WriteLine("三角形箱子製造機建置完成");
        }
        public TriangleBox GetTriangleBox()
        {
            Console.WriteLine("產生三角形箱子");
            return new TriangleBox();
        }
    }

    public void Start()
    {
        Console.WriteLine("工廠開始運作");

        SquareBoxMaker squareBoxMaker = new SquareBoxMaker();
        TriangleBoxMaker triangleBoxMaker = new TriangleBoxMaker();
        squareBoxMaker.GetSquareBox();
        triangleBoxMaker.GetTriangleBox();
    }
}

現在工廠運作是這樣的:

/*
 * 這是一間箱子工廠
 * 工廠開始運作
 * 正方形箱子製造機建置完成
 * 三角形箱子製造機建置完成
 * 產生正方形箱子
 * 產生三角形箱子
*/

過了一陣子,公司就接到了一個客戶要做圓形的箱子,不過這個客戶的訂單量很少,為了他買一台圓形箱子的製造機並不划算,可是老闆怎麼會錯過這個賺錢的機會呢。

於是老闆左思右想終於想到了一個方法: 那我們的工廠就生產個比較大的箱子,這個箱子可以放下各個不同形狀的箱子,這樣不管以後再來多少不一樣的需求我都可以賺錢了阿。

廠長聽了老闆的想法後立刻改造了工廠:

class BoxFactory
{
    public BoxFactory()
    {
        Console.WriteLine("這是一間箱子工廠");
    }

    class ObjectBoxMaker
    {
        public ObjectBoxMaker()
        {
            Console.WriteLine("'大'箱子製造機建置完成");
        }

        public object GetBox(string shape)
        {
            Console.WriteLine("產生'大'箱子");
            if (shape == "Triangle") return new TriangleBox();
            if (shape == "Square") return new SquareBox();
            return new CircleBox();
        }
    }

    public void Start()
    {
        Console.WriteLine("工廠開始運作");
        ObjectBoxMaker maker = new ObjectBoxMaker();
        maker.GetBox("Square");
        maker.GetBox("Triangle");
        maker.GetBox("Circle");
    }
}

現在工廠的運作狀況變這樣:

/*
 * 這是一間箱子工廠
 * 工廠開始運作
 * '大'箱子製造機建置完成
 * 產生'大'箱子
 * 產生'大'箱子
 * 產生'大'箱子
 */

又過了一陣子,公司開始接到客戶的抱怨電話: 那個'大'箱子雖然可以容納各種形狀的箱子,但是每次都要打開來確定它真正的形狀是什麼效率差了一大截阿。

老闆這下子可真慌了手腳了,弄巧成拙,一時之間又想不出辦法,於是只能硬著頭皮去請救兵-泛型哥幫忙,泛型哥聽了老闆的說明以後不慌不忙地將工廠改造成這樣:

class SquareBox
{
    public SquareBox()
    {
        Console.WriteLine("正方形箱子");
    }
}

class TriangleBox
{
    public TriangleBox()
    {
        Console.WriteLine("三角形箱子");
    }
}

class CircleBox
{
    public CircleBox()
    {
        Console.WriteLine("圓形箱子");
    }
}

class BoxFactory
{
    public BoxFactory()
    {
        Console.WriteLine("這是一間箱子工廠");
    }
    
    class GenericBoxMaker
    {
        public GenericBoxMaker()
        {
            Console.WriteLine("箱子製造機建置完成");
        }

        public object GetBox<T>() where T : new()
        {
            Console.WriteLine("產生合適的箱子");
            return new T();
        }
    }
    public void Start()
    {
        Console.WriteLine("工廠開始運作");
        GenericBoxMaker maker = new GenericBoxMaker();
        maker.GetBox<SquareBox>();
        maker.GetBox<TriangleBox>();
        maker.GetBox<CircleBox>();
    }
}

現在工廠運作變成這樣:

/*
 * 這是一間箱子工廠
 * 工廠開始運作
 * 箱子製造機建置完成
 * 產生合適的箱子
 * 正方形箱子
 * 產生合適的箱子
 * 三角形箱子
 * 產生合適的箱子
 * 圓形箱子
 */

泛型哥完美的解決了公司的危機,這個故事也告一個段落了。

接著我們來回顧一下這個故事:

  1. 特定箱子的製造機: 每個製造機只能產出跟其對應形狀的箱子
    • 定義類別時就決定了型別
  2. 大箱子的製造機: 此製造機所產出的箱子(Object)是相容於每個不同形狀的箱子
    • 所有的輸出類別都轉為Object,使其變為弱型別
    • Object在取值及附值時都需要花費轉換的時間
  3. 泛型哥的製造機: 在定義時使用泛型先暫緩型別的規格定義,到了實體化時再定義其型別規格
    • 強型別
    • 不需做轉換

泛型方法介紹

泛型可以用在很多地方,像是類別、介面、方法...等,規則大同小異,因為LINQ常用的是泛型方法,所以就方法的部分來做介紹:

public T Generic<T>(T b) where T : new()
{
    return new T();
}
  • 在方法名稱的後面以<>括住待定義的型別參數
  • 待定義的型別參數名稱習慣以T開頭(Ex: TResult)
  • 泛型可用在傳入參數回傳值
  • 可以以where定義型別參數的條件,以此例來說,where T : new()定義T有建構子,如此一來我們才可以在new T(),請參考Microsoft Docs-型別參數的條件約束

結語

泛型非常好用,比起Object既是強型別又不用轉換,也比特定型別定義來的有彈性,可以大大減少程式碼的編寫量,又可以寫出可讀性更高的程式,好泛型,不用嗎?

範例程式

GitHub

參考