More Effective C# 08.在匿名型別上定義區域函式 More Effective C# 08.在匿名型別上定義區域函式(Define Local Functions on Anonymous Types)

在上一個做法提到 Tupleanonymous types 兩種定義輕量型別的方式,但是要把這些型別作為方法的參數、回傳、屬性則需要使用一些特別的技巧。

Tuple 為例,我們只需要定義 Tupleshape 就可以很輕鬆地把一個 Tuples 當成方法的回傳型別,可以直接把回傳的 Tuple 指派給擁有相同格式的變數, 也可以使用 deconstruction 把值指派給不同格式的變數。

void Main()
{
	var list = new List<int>() { 40, 42, 45, 56 };
	(int sought, int index) result = FindFirstOccurence(list, 42);
	Console.WriteLine($"First {result.sought} is at {result.index}");

	(int number, int index) = FindFirstOccurence(list, 42);
	Console.WriteLine($"First {number} is at {index}");
}

static (T sought, int index) FindFirstOccurence<T>(IEnumerable<T> enumerable, T value)
{
   int index = 0;
   foreach (T element in enumerable)
   {
       if (element.Equals(value))
       {
           return (value, index);
       }
       index++;
   }
   return (default(T), -1);
}

anonymous types 就很難如此使用,因為它的名稱是編譯器自動產生的,所以你沒辦法在撰寫程式碼時獲得它的名稱並把它當成型別,解決辦法是透過 泛型方法並指定匿名型別作為 type parameter (T)。

下面這個例子建立了一個匿名型別 Number, Description 並把這個型別作為 FindValue<T> 的 type parameter,這種技巧就可以在寫程式 的期間使用匿名型別當成參數或回傳使用。

void Main()
{
	IDictionary<int, string> numberDescriptionDictionary = new Dictionary<int, string>()
	 {
		{1,"one"},
		{2, "two"},
		{3, "three"},
		{4, "four"},
		{5, "five"},
		{6, "six"},
		{7, "seven"},
		{8, "eight"},
		{9, "nine"},
		{10, "ten"},
	 };
	 
	List<int> numbers = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	
	var r = from n in numbers
			where n % 2 == 0
			select new
			{
				Number = n,
				Description = numberDescriptionDictionary[n]
			};
			
	r = from n in FindValue(r, new { Number = 2, Description = "two" })
		select n;
		
	r.Dump();
}

static IEnumerable<T> FindValue<T>(IEnumerable<T> enumerable, T value)
{
	foreach (T element in enumerable)
	{
		if (element.Equals(value))
		{
			yield return element;
		}
	}
}

不過這個方法只能處理一些簡單的問題,像是我想要單獨使用匿名型別中的特定屬性 Number 上面的方式就沒辦法做到, 這裡你需要的是建立一個 higher-order function,高階函式是一個以函式作為參數或者回傳一個函式的一種函式寫法。

像是下面的 TakeWhile 方法就是一個高階函式。

Random randomNumbers = new Random();
var sequence = (from x in Utilities.Generator(100,
				   () => randomNumbers.NextDouble() * 100)
				let y = randomNumbers.NextDouble() * 100
				select new { x, y }).TakeWhile(
			   point => point.x < 75);

它的內部其實跟我們上面提到的 FindValue 很像,只不過把 value 換成一個委派 predicate,也就是說這裡的 TSource 就代表一個 x, y 的匿名型別,這樣的方式就能透過委派把特定的屬性丟給方法。

public static IEnumerable<TSource> TakeWhile<TSource>
    (this IEnumerable<TSource> source,
    Func<TSource, bool> predicate);

可以參考 TakeWhile 的寫法建立出一個 Map 擴充方法。可以把重複的邏輯搬過去。

void Main()
{
	var sequence = (from x in Utilities.Generator(100,
					   () => randomNumbers.NextDouble() * 100)
					let y = randomNumbers.NextDouble() * 100
					select new { x, y }).TakeWhile(
				   point => point.x < 75);
	var scaled = sequence.Map(p =>
		new
		{
			x = p.x * 5,
			y = p.y * 5
		}
	);
	var translated = scaled.Map(p =>
		new
		{
			x = p.x - 20,
			y = p.y - 20
		}
	);
	var distances = translated.Map(p => new
		{
			p.x,
			p.y,
			distance = Math.Sqrt(p.x * p.x + p.y * p.y)
		}
	);
	var filtered = from location in distances
				   where location.distance < 500.0
				   select location;
}

public static IEnumerable<TResult> Map<TSource, TResult>(
	this IEnumerable<TSource> source,
   Func<TSource, TResult> mapFunc)
{
	foreach (TSource s in source)
		yield return mapFunc(s);
}

雖然這個技巧很有用,但是匿名型別不應該作為演算法中的必要型別,如果真的很常用這個匿名型別的話,應該把它轉換成一個具體的型別, 作者建議如果超過三個地方採用相同的匿名型別,那就應該把它改成具體型別。


Summary

這個做法提供了幾個技巧讓我們能夠將匿名函式當成是引數來傳遞,我們可以使用 lambda 搭配泛型方法也可以使用泛型與高階函式做到更複雜的功能。