重新認識 C# [7] - C# 7 ValueTuple
5 |
【本系列是我的 C# in Depth 第四版讀書筆記,背景故事在這裡】
Tuple 從 C# 4 開始就有,可用 new Tuple<int, int>(v1, v2)
建立臨時性物件一次傳遞多個值,但缺點是存取名稱為 Item1、Item2、Item3... 無法自訂。而 C# 4 的 Tuple 是 Immutable Reference Type Tuple,Item* 值唯讀,無法修改。
C# 7 / .NET 4.7 的 Tuple 是 Mutable Value Type ValueTuple,為 Value Type (使用 Stack 儲存,記憶體管理成本低,效能更好),Item 值可修改,應用上更彈性,同時加入 Tuple Literal、Tuple Type 語法,寫法極為簡潔:
//Tuple Literal,每個元素有值,並可具名
(5, title: "text") // 5 - Unamed Element, title - Named Element
(1, 2, 3)
(x:1, y:2)
//Tuple Type,每個元素有型別,並可具名
(int x, Guid) // x - Named Element, Guid - Unamed Element
(int, int)
(int x, int y, int z)
//若沒給名字,就依順序用 Item1, Item2, Item3...
static (int min, int max) MinMax(
IEnumerable<int> source)
{
using (var iterator = source.GetEnumerator())
{
if (!iterator.MoveNext())
{
throw new InvalidOperationException(
"Cannot find min/max of an empty sequence");
}
int min = iterator.Current;
int max = iterator.Current;
while (iterator.MoveNext())
{
min = Math.Min(min, iterator.Current);
max = Math.Max(max, iterator.Current);
}
return (min, max);
}
}
int[] values = { 2, 7, 3, -5, 1, 0, 10 };
var extremes = MinMax(values);
Console.WriteLine(extremes.min);
Console.WriteLine(extremes.max);
C# 7.1 加入自動推斷 Tuple Literal 元素名稱功能,比照 new 自動取 PorpName 作為屬性名的概念。
// C# 7.0
(name: emp.Name, title: emp.Title, departmentName: dept.Name)
// C# 7.1
(emp.Name, emp.Title, DepartmentName: dept.Name)
List<int> list = new List<int> { 5, 1, -6, 2 };
var tuple = (list.Count, Min: list.Min(), Max: list.Max());
心法:Tuple = Bag of Variables 裝了一堆變數的袋子
static IEnumerable<int> Fibonacci()
{
var pair = (current: 0, next: 1);
while (true)
{
yield return pair.current;
pair = (pair.next, pair.current + pair.next);
}
}
Tuple 轉換問題:Tuple Literal 到 Tuple Type,Tuple Type 到另一個 Tuple Type
Tuple Literal 隱含轉型到 Tuple Type 的前題:Arity (元素個數)必須相同且可以隱含轉型到對映元素型別
(byte, object) tuple = (5, "text"); //可以
(byte, string) tuple = (300, "text"); //不行 error CS0029: Cannot implicitly convert type 'int' to 'byte'
Tuple Literal 明確轉型到 Tuple Type
int x = 300;
var tuple = ((byte, string)) (x, "text");
// or
var tuple = ((byte) x, "text");
名稱不對會怎樣
(int a, int b, int c, int, int) tuple =
(a: 10, wrong: 20, 30, pointless: 40, 50);
/*
亂給名稱會被無視但不會出錯,Compiler 會當成未指定名稱,依順序對映
warning CS8123: The tuple element name 'wrong' is ignored because a different
name is specified by the target type '(int a, int b, int c, int, int)'.
warning CS8123: The tuple element name 'pointless' is ignored because a
different name is specified by the target type '(int a, int b, int c, int, int)'
*/
Tuple Type 到另一個 Tuple Type
var t1 = (300, "text"); // Tuple<int, string>
(long, string) t2 = t1; //可以
(byte, string) t3 = t1; //不行
(byte, string) t4 = ((byte, string)) t1; //可以
(object, object) t5 = t1; //可以
(string, string) t6 = ((string, string)) t1; //不行
var source = (a: 10, wrong: 20, 30, pointless: 40, 50);
(int a, int b, int c, int, int) tuple = source; //OK,沒警告,忽略名稱不符,依順序對映
更多轉換
//Identity Conversion
(int x, object y)
(int a, dynamic d)
(int, object)
//Identity Conversion
Dictionary<string, (int, List<object>)>
Dictionary<string, (int index, List<dynamic> values)>
// error CS0111: Type 'Program' already defines a member called 'Method' with the same parameter types
public void Method((int, int) tuple) {}
public void Method((int x, int y) tuple) {}
//這個不能轉換,因為 Generic Variance 只適用 Ref Type,Tuple 是 ValueType
IEnumerable<(string, string)> stringPairs = new (string, string)[10];
IEnumerable<(object, object)> objectPairs = stringPairs;
繼承或實作介面時,名稱很重要
interface ISample
{
void Method((int x, string) tuple);
}
public void Method((string x, object) tuple) {} //型別不對
public void Method((int, string) tuple) {} //第一個少名稱
public void Method((int x, string extra) tuple) {} //第二個名稱不對
public void Method((int wrong, string) tuple) {} //第一個名稱不對
public void Method((int x, string, int) tuple) {} //元素數量不對
public void Method((int x, string) tuple) {} //有效
C# 7.3 加入 == 及 != 運算子
var t1 = (x: "x", y: "y", z: 1);
var t2 = ("x", "y", 1);
Console.WriteLine(t1 == t2);
Console.WriteLine(t1.Item1 == t2.Item1 && // Compiler 轉換成
t1.Item2 == t2.Item2 &&
t1.Item3 == t2.Item3);
Console.WriteLine(t1 != t2);
Console.WriteLine(t1.Item1 != t2.Item1 || // Compiler 轉換成
t1.Item2 != t2.Item2 ||
t1.Item3 != t2.Item3);
Tuple 在 CLR 層有九個對映型別:
System.ValueTuple (nongeneric)
System.ValueTuple<T1>
System.ValueTuple<T1, T2>
System.ValueTuple<T1, T2, T3>
System.ValueTuple<T1, T2, T3, T4>
System.ValueTuple<T1, T2, T3, T4, T5>
System.ValueTuple<T1, T2, T3, T4, T5, T6>
System.ValueTuple<T1, T2, T3, T4, T5, T6, T7>
System.ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>
C# 7 Tuple 名稱只存在 Compiler 層,CLR 層 Tuple 元素沒有名稱的概念
(int, string) t1 = (300, "text");
(long, string) t2 = t1;
(byte, string) t3 = ((byte, string)) t1;
// Compiler 轉成如下程式
var t1 = new ValueTuple<int, string>(300, "text");
var t2 = new ValueTuple<long, string>(t1.Item1, t1.Item2);
var t3 = new ValueTuple<byte, string>((byte) t1.Item1, t1.Item2));
Tuple 可以比對相等與否,也可以比大小(型別不同會 ArgumentException):
var points = new[]
{
(1, 2), (10, 3), (-1, 5), (2, 1),
(10, 3), (2, 1), (1, 1)
};
var distinctPoints = points.Distinct();
Console.WriteLine($"{distinctPoints.Count()} distinct points");
Console.WriteLine("Points in order:");
foreach (var point in distinctPoints.OrderBy(p => p))
{
Console.WriteLine(point); //先比 Item1, 再比 Item2
}
// ValueTuple 還有實作 IStructuralEquatable 及 IStructuralComparable
// 可以 .Equals(x, StringComparer.OrdinalIgnoreCase)、.CompareTo(x, StringComparer.OrdinalIgnoreCase)
ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>
用來支援七個以上元素
ValueTuple<int, int, int, int, int, int, int, ValueTuple<int>>
ValueTuple<A, B, C, D, E, F, G, ValueTuple<H, I>>
var tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
Console.WriteLine(tuple.Item16); //=tuple.Rest.Rest.Item2
Nongeneric ValueTuple 用來實作靜態方法
var tuple = ValueTuple.Create(5, 10);
C# 7 ValueTuple 替代品:
- C# 4 Immutable Ref Type Tuple (System.Tuple),少數優點:Ref Type,傳遞有效率
- 匿名型別,優點:可在 Expression Tree 使用,Ref Type
注意:dynamic 無法識別 Tuple 名稱,不認得 Item9 (.Rest.Item2)
My notes for C# in Depth part 8
Comments
# by ChrisTorng
閱讀中,對於: 名稱不對會怎樣 (int a, int b, int c, int, int) tuple = (a: 10, wrong: 20, 30, pointless: 40, 50); /* 亂給名稱會被無視但不會出錯 */ var source = (a: 10, wrong: 20, 30, pointless: 40, 50); (int a, int b, int c, int, int) tuple = source; //OK,沒警告 這兩個,知道不會有錯誤,但不知道執行結果是如何。名稱不對的會依位置仍然指定正確值,或者會被忽略,值被給 default 值呢? 是否能再補充說明呢?
# by Jeffrey
to ChrisTorng,ValueTuple 名稱只存在 Compiler 層,Runtime 層一直都是 Item1, Item2, Item3.. 名稱不合被 Compiler 忽略時就是用位置對映,兩個案例的結果都是 (10,20,30,40,50)。
# by ChrisTorng
謝謝補充。我認為 Compiler 層一樣可以解釋為「名稱未對應,視為忽略該不正確名稱之值」,因此產出來的 IL 碼以 default 值填入,仍然符合「亂給名稱會被無視但不會出錯」的敘述。所以「名稱被無視」或是「該名稱之值被無視」對我還是需要明確區別說清楚。
# by Jeffrey
to ChrisTorng,補上"Compiler 會當成未指定名稱,依順序對映"的說明,謝謝建議。
# by h
n