我就直接說了,有用前面的程式碼區塊的人,絕對執行不了。因為我們建立哥布林和戰士這兩張卡片的時候,根本就沒有給他們數值呀!

雖然可以先呼叫出來再賦值…

var goblin = new Card();
goblin.Name = "哥布林";
goblin.Attack = 3;
goblin.Health = 2;
/* ...其他賦值 */

這實在相當占空間,也有點奇怪。畢竟如果是阿福(狗),一出生的時候應該就確定了一些特徵才對,例如品種、血型、眼睛顏色這種。並不會出生後過一陣子,才突然決定這些東西,既然如此,我們在產生物件的時候,當然也會希望在建立的同時就先決定好一部份內容

這時候我們就可以藉由建構式的方式,在建立物件時就進行一些我們想要的操作。

建構式

事實上,當我們呼叫 new Card() 的時候(不覺得這個 () 很有呼叫方法的感覺嗎?)我們就是正在調用 Card 的建構式。而當我們沒有特別去定義建構式的時候,就會直接使用內建的建構式去幫我們產生物件。

現在我們替 Card 新增一個建構式:

public class Card
{
    public Card (string name, int attack, int health)
    {
        this.Name = name;
        this.Attack = attack;
        this.Health = health;
    }
    /* ... 其他屬性和方法 */
}

在 C# 的時候,建構式必須和類別同名,且不需要定義回傳類型。當我們有了建構式,剛剛的例子就可以改寫成:

var goblin = new Card(name: "哥布林", attack: 3, health: 2);
var warrior = new Card(name: "戰士" , attack: 4, health: 3);
warrior.Hit(goblin);

建構式也就是建立這個物件時執行的函式,通常會用來進行初始化,也就是做一些建立物件必要的準備。例如傳遞必要屬性或是建構需要的其他物件、或是給予私有屬性初始值等等,例如說我們的卡牌一建立,就會需要知道它的名字和戰鬥力,這樣才有卡牌的感覺,而不該像一些 壞決鬥者 邊打牌邊偷偷印卡。

註:當然有建立時執行的,也就會有消滅時執行的。請參見 解構式,由於較少用到,此處先按下不表。

多載

當然,有了建構式就會有更多問題。現在我們只有一個方法可以建立卡牌了,這無疑是相當不彈性的,例如說我希望預設的攻擊力和血量就是四呢?實際上我們經常會遇到需要用不同素材去建立一個物件的場合,這時候就必須得提到另一個要點:多載了。

多載指的就是可以有很多個同樣名字的方法,各自去接不同的參數。例如說我們的 Card 建構式就可以利用多載來改造一下:

public class Card
{
    public Card (string name, int attack, int health)
    {
        this.Name = name;
        this.Attack = attack;
        this.Health = health;
    }

    public Card (string name)
    {
        this.Name = name;
        this.Attack = this.Level;
        this.Health = this.Level;
    }

    public Card ()
    {
        this.Name = "Noname";
        this.Attack = this.Level;
        this.Health = this.Level;
    }
}

如此一來,我們在建立卡片的時候就能夠有更多選擇了,現在我們可以根據狀況給予需要的參數,剩下的就交給建構式去處理就好。

實務上,如果規則或是建立的步驟一致的話,為了能夠把規則集中到一個地方方便修改,並且減少多餘的程式碼。我們通常會試著讓其他的建構式去呼叫主要的建構式,在 C# 中,呼叫自己的建構式是使用 : this() 來進行的,例如:

public Card (string name, int attack, int health)
{
    this.Name = name;
    this.Attack = attack;
    this.Health = health;
}

// 會呼叫上面那個建構式
public Card (string name) : this(name: name, attack: 5, health: 5)
{
    // 呼叫完 Card(name, attack, health) 之後做的事
}

// 會呼叫上面那一個建構式
public Card () : this(name: "Noname") 
{
    // 呼叫完 Card(name) 之後做的事   
}

如此一來只要建構的方式有變更,我們只需要集中修改第一個建構式就好了。這部份的流程也可以參照 [C#.NET] 為建構子建立正確的初始化 - 余小章 @ 大內殿堂 這篇的說明。

當然多載也不只是用在建構子,而是大多數時候都可以用的寫法。例如當你的函式雖然做同樣的事,但允許接收不同的參數來處理時,就請考慮使用多載

例如說當你要記錄發生錯誤時的 Log ,就能允許只傳遞錯誤內容,或是傳遞錯誤內容和當時操作的參數,甚至是當下的環境資料等等。

又或者是查詢客戶資料(GetUser 之類的)的函式,提供使用 客戶代號,或是 訂單編號 等不同的方式進行查詢時,就可以考慮多載的應用。

例如說 .net 中協助類別對映的名套件 AutoMapper,在轉換類別的 Map 方法就很漂亮地使用了多載:

註:看不到圖片的可以直接看 Github

當然隨著多載的應用越來越稀鬆平常,時至今日,我們只要使用選擇性參數就可以輕鬆達到一樣的效果囉:

public Card (string name = "Noname", int attack = 5, int health = 5)
{
    this.Name = name;
    this.Attack = attack;
    this.Health = health;
}

當有預設值的時候,該參數就會變成可選的,這時候就可以輕鬆決定要傳進來的內容了。當然,如果傳進來的並非只是數量上的差別,而是整個型別都不一樣的話,還是要回歸到多載的做法,建立兩個同名但不同傳入參數類型的方法,可讀性會比較高呦。

多載提供的好處在於:同個目標的函式可以根據傳入的參數不同做不一樣的處理。例如當我們寫了一個連線取資料的方法,可以分為

(1) 傳入連線的話,就使用連線取得資料

(2) 傳入連線字串的話,就先用連線字串開啟連線,再使用連線取得資料

等等,根據參數的場合來進行處理。

藉由傳入不同的參數類型和數量,就可以處理不同狀況的內容,既擴展了函式在使用上的彈性,同時也增加函式能派上用場的時機

而最重要的是這將讓編寫程式的人員去思考:我設計的這個方法將能應用在什麼場景? 這將會成為一個相當優良的習慣。

那麼這次就說到這裡,我們下篇見!

本系列下一篇:菜雞與物件導向 (3): 封裝

參考資料

同系列文章