在上一個做法提到 Tuple
與 anonymous types
兩種定義輕量型別的方式,但是要把這些型別作為方法的參數、回傳、屬性則需要使用一些特別的技巧。
以 Tuple
為例,我們只需要定義 Tuple
的 shape
就可以很輕鬆地把一個 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 搭配泛型方法也可以使用泛型與高階函式做到更複雜的功能。