More Effective C# 07.使用 Tuples 限制型別的範圍 More Effective C# 07.使用 Tuples 限制型別的範圍(Limit Type Scope by Using Anonymous Types)

這個做法說明了匿名型別的使用方式,以及怎麼使用 Tuples 來建立匿名型別和背後的原理。

在 C# 中我們建立型別有許多選擇例如 classstructtupleanonymous types,並不是一定要從 classstruct 二選一。 如果今天要處理的問題更加簡單,那麼就可以改用 tupleanonymous types 節省開發時間。

首先要建立一個匿名型別非常簡單,只需要在大括號之間定義欄位即可。

void Main()
{
	var aPoint = new { X = 5, Y = 67 };
}

編譯器會自動產生一個 internal sealed class 並且可以看出這是一個泛型的類型而且名稱是自動產生的。

internal sealed class <>f__AnonymousType0<<X>j__TPar, <Y>j__TPar>
{
    private readonly <X>j__TPar <X>i__Field;

    private readonly <Y>j__TPar <Y>i__Field;

    public <X>j__TPar X
    {
        get
        {
            return <X>i__Field;
        }
    }

    public <Y>j__TPar Y
    {
        get
        {
            return <Y>i__Field;
        }
    }

    public <>f__AnonymousType0(<X>j__TPar X, <Y>j__TPar Y)
    {
        <X>i__Field = X;
        <Y>i__Field = Y;
    }
    
    ...
}

也就是說實際上的呼叫的程式碼會轉換成下面這樣子。

void Main()
{
    <>f__AnonymousType0<int, int> anon = new <>f__AnonymousType0<int, int>(5, 67);
}

所以實際上我們也可以自己寫出這匿名類型的程式碼,但是不如讓編譯器自行處理可以節省很多工作,也可以省下讓其他開發者閱讀與理解的時間。

但缺點就是不知匿名型別的實際名稱,所以就不可能把它當成參數或是方法回傳來使用,不過可以透過 lambda 搭配 function parameters 來達成把匿名型別當成參數的需求。

void Main()
{
	var aPoint = new { X = 5, Y = 67 };
	var anotherPoint = Transform(aPoint, (p) => new { X = p.X * 2, Y = p.Y * 2 });
}

static T Transform<T>(T element, Func<T, T> transformFunc)
{
	return transformFunc(element);
}

使用匿名型別最大的好處就是擴充性,可以隨意地增加欄位非常方便,這樣的特性就很適合拿來做中繼結果保存的功能,例如先從資料庫讀取資料後經過演算法處理後 保存成中繼結果,最後再把這個中繼結果傳給第二階段的處理工作,這樣就不用在建立多個型別來保存也可以避免命名空間有許多用來輔助用的類別。

void Main()
{
	var aPoint = new { X = 5, Y = 67, Z = 3 };
	var anotherPoint = Transform(aPoint, (p) => new { X = p.X * 2, Y = p.Y * 2, Z = p.Z * 2 });
}

static T Transform<T>(T element, Func<T, T> transformFunc)
{
	return transformFunc(element);
}

還有一個特性是編譯器產生出來的匿名型別是 immutable types 因為它並屬性並沒有 set 存取子,而且只能在建構函式中初始化, 但如果你自己寫一個 immutable types 你會發現沒辦法使用 object initializer syntax 來建立物件,下面這種寫法就會報錯只能透過建構函式。

void Main()
{
	var xx = new MyClass{ X = 1, Y = 2 };
}

public class MyClass
{
	public int X { get; }

	public int Y { get; }
	
	public MyClass(int x, int y)
	{
		X = x;
		Y = y;
	}
}

不過匿名型別同樣沒有 set 存取子但可以使用 object initializer syntax 來建立物件,是因為編譯器會幫你轉換成使用建構函式呼叫的版本。

var aPoint = new { X = 5, Y = 67 };
// 轉換成下面
AnonymousMumbleMumble aPoint = new AnonymousMumbleMumble(5, 67);

最後匿名型別其實比想像的還要花費更少的資源,像上面用到的例子看起來就是建立了兩個匿名型別,但實際上只要結構是相同的編譯器就會重複利用 編譯出來的新型別,所以其實下面只會產生一次新型別之後就可以重複使用。

void Main()
{
	var aPoint = new { X = 5, Y = 67 };
	var anotherPoint = Transform(aPoint, (p) => new { X = p.X * 2, Y = p.Y * 2 });
}

static T Transform<T>(T element, Func<T, T> transformFunc)
{
	return transformFunc(element);
}

編譯器要判斷兩個匿名型別是否相同有兩個階段,第一是兩個宣告的同樣匿名型別是否在同一個 assembly 中,第二是兩個結構是否真的完全一致這其中包含 欄位必須要以相同的順序出現,所以你把先後順序調轉過來就會產生新的匿名型別。

void Main()
{
	var aPoint = new { Y = 67, X = 5 };
}

最後匿名型別可以做到複合鍵的效果,下面這段語法就會建立出一個字典 key{ c.SalesRep, c.ZipCode } 的複合鍵,然後 valuecustomers 清單。

var query = from c in customers
 group c by new { c.SalesRep, c.ZipCode };

接下來討論一下 tupleanonymous types 之間的差異,他們最大的差異就是 tuple 不會建立新的類型而是使用名叫 ValueTuple 的結構, 由此可以知這是一個 value type 並且是 mutable types 所以我們可以通過屬性再次修改內部欄位的值,不像 anonymous types 無法再次修改。

void Main()
{
	var aPoint = (X: 5, Y: 67);
	aPoint.X = 1;
	aPoint.Y = 2;
	Console.WriteLine(aPoint);
}

tuple 是依賴 shape 而不是 name 來判斷是否兩個型相同的,例如包含兩個 inttuple 就會和上面例子中的 aPoint 是相同的型別, 因為它們都是 System.ValueTuple<int, int> 的實例化。

tuple 可以是在宣告變數的時候明確指定或者是在左側宣告欄位名稱。

void Main()
{
	var aPoint = (X: 5, Y: 67);
	var anotherPoint = aPoint;
	(int Rise, int Run) pointThree = aPoint;
	
	Console.WriteLine(aPoint.X);
	Console.WriteLine(pointThree.Rise);
}

tuple 比較適合做為回傳型別或者方法參數,並且擁有 mutable typesvalue type 的好處。 anonymous types 則適合用來作為集合中的 composite keys 也擁有 immutable typesreference type 的好處。


Summary

anonymous typestuple 並沒有想像的那麼花費資源,適當的使用可以加快開發的速度並且增加可讀性, 如果想要當成中繼結果使用建議使用 anonymous types,如果需要可改變值的型別則使用 tuple