【本系列是我的 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 替代品:

  1. C# 4 Immutable Ref Type Tuple (System.Tuple),少數優點:Ref Type,傳遞有效率
  2. 匿名型別,優點:可在 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

Post a comment