C# 筆記:匿名型別

時候,例如在某個函式裡面,我們會臨時需要一個簡單的類別來儲存一些簡單資料,但又不想為了這個簡單的需求另外定義一個類別,此時便可使用 C# 的匿名型別(anonymous type)。


參考以下範例:
private void Foo()
{
var emp = new { Name = "Michael", Birthday = new DateTime(1971, 1, 1) };
Console.WriteLine("Employee name: " + emp.Name);
Console.WriteLine("Birthday: " + emp.Birthday.ToString());
Console.WriteLine("實際的型別是: " + emp.GetType());
}
這個範例使用了隱含型別 var 來宣告區域變數 emp,這是必須的,因為我們使用了匿名型別。這表示從 new 運算子之後的大括弧包住的部分,會由編譯器產生一個類別定義。當然該類別的名稱也是由編譯器決定,所以我們在寫程式時便無法得知實際的類別名稱,自然就得用 var 來宣告變數了。

執行結果如下:
Employee name: Michael
Birthday: 1971/1/1 上午 12:00:00
實際的型別是: <>f__AnonymousType0`2[System.String,System.DateTime]
範例程式的最後一行是把匿名型別的實際型別名稱秀出來。注意此型別是個泛型類別(generic class),它有兩個參數型別。

「OK! 既然從程式的執行結果已經知道實際型別,那我就直接用這個型別名稱來宣告變數就好啦!」你說。

不行!你如果直接在程式中使用 <>f__AnonymousType0`2 來宣告變數,編譯器會看不懂。

「那如果要宣告兩個相同匿名型別的變數怎麼辦?」

如果把範例程式碼改成這樣:
private void Foo()
{
var emp1 = new { Name = "Michael", Birthday = new DateTime(1971, 1, 1) };
var emp2 = new { Name = "John", Birthday = new DateTime(1981, 12, 31) };

Console.WriteLine("emp1 的實際型別是: " + emp1.GetType());
Console.WriteLine("emp2 的實際型別是: " + emp2.GetType());
}

你會發現 emp1 和 emp2 的實際型別都一樣。基於效率考量,編譯器會重複使用具有相同參數個數與參數名稱的匿名型別,而不會每碰到一個匿名型別的宣告就產生一個新的泛型類別。

不過,如果你將 emp2 的宣告改成這樣:
var emp2 = new { Name = "John", Birth = new DateTime(1981, 12, 31) };
由於參數名稱不同,編譯器會產生另一個泛型類別。執行結果如下:

emp1 的實際型別是: <>f__AnonymousType0`2[System.String,System.DateTime]
emp2 的實際型別是: <>f__AnonymousType1`2[System.String,System.String]

那麼,如果參數個數相同,參數名稱相同,但型別不同呢?這種情況,編譯器仍會重複使用先前產生的泛型類別。這些編譯器的處理規則對我們來說可能太過細節,不過,匿名型別還有另一種定義方式可能需要瞭解一下:projection initializers(投射式初始值設定)(路人:好彆扭啊!還是直接寫英文吧!)

Projection Initializers

下面是一個 projection initializers 的範例:
public class EmpInfo
{
    private string name;
    private DateTime birthday;

    public EmpInfo(string aName, DateTime aBirthday)
{
name = aName;
birthday = aBirthday;
}

    public string Name
{
get { return name; }
set { name = value; }
}

    public DateTime Birthday
{
get { return birthday; }
set { birthday = value; }
}
}

class Program
{
    static void Main(string[] args)
{
Console.WriteLine("示範 projection initializers 的匿名型別寫法。");

EmpInfo emp = new EmpInfo("Michael", new DateTime(1971, 1, 1));

var emp1 = new { emp.Name, emp.Birthday };

string name = "John";
int age = 20;

var emp2 = new { name, age };

Console.WriteLine("emp1.Name = " + emp1.Name);
Console.WriteLine("emp2.name = " + emp2.name);

Console.WriteLine("emp1 的實際型別: " + emp1.GetType());
Console.WriteLine("emp2 的實際型別: " + emp2.GetType());
}
}
此範例包含兩種 projection initializer 的寫法,一種是利用事先定義的類別 EmpInfo 的屬性名稱,也就是說,編譯器在產生泛型類別時,會用 EmpInfo 的屬性名稱來當作參數名稱。另一種則是直接帶入區域變數名稱,即範例中以英文小寫宣告的變數 name 和 age。

注意事項

使用匿名型別時,請注意幾件事:
  1. 編譯器為匿名型別所產生的泛型類別並未實作 IDisposable 介面,因此,你應該避免在匿名型別中存放可丟棄的(disposable)物件。
  2. 一般而言,匿名型別的物件不應該儲存在 List 這類物件串列中。要是你這麼做的話,到時候取出物件時,你怎麼知道要將它轉成甚麼型別呢?
  3. 承上,匿名型別的物件也不能當作參數傳遞。因為,如果硬要這麼做,參數型別勢必得宣告為 object,這當然可以通過編譯。可是同樣的問題,這個參數將來要如何轉型呢?

2 則留言:

  1. 不好意思,第一段code與第二段code好像一模一樣喔!

    回覆刪除
  2. 哎呀!程式碼貼錯了,被抓包 @_@
    多謝告知 :)

    回覆刪除

技術提供:Blogger.
回頂端⬆️