C# 9.0元组(Tuple) 之 ValueTuple(值元组)与 Tuple(引用元组) 详细解说
元组Tuple 的简介
元组是具有特定数量和元素序列的数据结构。
我们使用有序对 x, y 。 在无序对 { x, y } 中,顺序无关紧要:{ x, y } = { y, x }。
1、{ } 表示无序属性的集合,它对应哲学上的实体,数据库中行数据, 编程的上的对象。
用途:表示具体的对象。因此 name{} 叫带名对象,{ }叫匿名对象。
2、( ) 表示有序属性的集合,它对应哲学上的属性集合(不表示实体),数据库的列。
用途:用于关系代数运算,心理学上叫的行为,不影响实体本身。
3、[ ]表示对象的集合。
因此FunctionName()表示带名函数 完整的含义是 动作|运算 +元组名。
( ) 表示匿名函数。
数学上,n元组或多元组是对象个数有限的序列。元组由三部分组成:边界符、分隔符和元素。通常采用的边界符是小括号“笛卡尔积中每一个 元 素(d1,d2,…,dn),叫作一个n元组(n-tuple)或简称元组。 当关系是一张表,二维表中的行表中的每行 (即 数据库 中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,
引用元组Tuple
提供用于创造元组对象的静态方法。
以下示例创建一个包含小于 20 的质数的 8 元组 (八进制) 。
var primes = Tuple.Create(2, 3, 5, 7, 11, 13, 17, 19); Console.WriteLine("Prime numbers less than 20: " + "{0}, {1}, {2}, {3}, {4}, {5}, {6}, and {7}", primes.Item1, primes.Item2, primes.Item3, primes.Item4, primes.Item5, primes.Item6, primes.Item7, primes.Rest.Item1); // The example displays the following output: // Prime numbers less than 20: 2, 3, 5, 7, 11, 13, 17, and 19
类 Tuple 本身并不表示元组。 相反,它是一个类,它提供用于创建.NET Framework支持的元组类型的实例的静态方法。 它提供帮助程序方法,你可以调用这些方法来实例化元组对象,而无需显式指定每个元组组件的类型。
尽管可以通过调用元组类构造函数来创建元组类的实例,但执行此操作的代码可能很麻烦。 以下示例使用类构造函数创建一个 7 元组或 9 元组,其中包含纽约市从 1950 年到 2000 年每次人口普查的人口数据。
// Create a 7-tuple. var population = new Tuple<string, int, int, int, int, int, int>( "New York", 7891957, 7781984, 7894862, 7071639, 7322564, 8008278); // Display the first and last elements. Console.WriteLine("Population of {0} in 2000: {1:N0}", population.Item1, population.Item7); // The example displays the following output: // Population of New York in 2000: 8,008,278
用帮助程序方法创建同一元组对象更为简单,如以下示例所示。
// Create a 7-tuple. var population = Tuple.Create("New York", 7891957, 7781984, 7894862, 7071639, 7322564, 8008278); // Display the first and last elements. Console.WriteLine("Population of {0} in 2000: {1:N0}", population.Item1, population.Item7); // The example displays the following output: // Population of New York in 2000: 8,008,278
值元组 (ValueTuple)
元组 (ValueTuple)类型是值类型;元组元素是公共字段,可以使用任意数量的元素定义元组。Tuple类型像一个口袋,在出门前可以把所需的任何东西一股脑地放在里面。您可以将钥匙、驾驶证、便笺簿和钢笔放在口袋里,您的口袋是存放各种东西的收集箱。
到了c# 4.0 应当使用元组Tuple而不是使用输出参数,在任何时候都应避免使用ref/out传递参数,尤其对引用类型。
继承
Object-> ValueType ->Enum
Object-> ValueType ->struct 包括int float等简单值类型
Object-> ValueType ->ValueTuple
Object-> ValueType ->ValueTuple<T1>
Object-> ValueType ->Nullable
元组项命名准则
务必对使用元组语法声明的所有变量使用驼峰式大小写。
****考虑对所有元组项名称使用 Pascal 命名法。
为什么要使用元组?
有时,可能会发现,将数据元素组合在一起非常有用。例如,假设要处理国家/地区相关信息,如 2017 年世界上最贫穷的国家/地区马拉维。它的首都是利隆圭,人均国内生产总值 (GDP) 为 226.50 美元。显然,可以为此类数据声明一个类,但它并不是真正典型的名词/对象。它似乎更像是一组相关数据,而不是对象。当然,若要设置 Country 对象(举个例子),所含数据远不止 Name、Capital 和 GDP per capita 这些属性。也可以将每个数据元素存储在各个变量中,但这样做的结果是,数据元素相互没有任何关联。除了变量名称可能共用后缀或前缀之外,226.50 美元与马拉维将无任何关联。另一种方法是,将所有数据组合到一个字符串中。不过,这样做的缺点是,必须先分析各个数据元素,然后才能分别处理这些元素。最后一种方法是创建匿名类型,但这样做同样有局限性。其实,元组足以完全替代匿名类型.-----Mark Michaelis
一、元组声明和分配示例代码:
//元组写法1通过example2.Item来引用 var example1 = (1,2,3,4,5,"23",1,2,3,1,5,6,2,3); Console.WriteLine(example1.Item10); //元组写法2, 通过example3.变量名引用 var example2 = (exa1:1, exa2: 2,3,4,5,6); Console.WriteLine(example2.exa2); //元组写法3, 通过example3.变量名引用 左侧不允许弃元 (int age, string name) example3 = (3, "Dog3"); Console.WriteLine(example3.name); //元组写法4 相当于批量赋值 可以单独使用 变量 左侧不允许弃元 (string sr, bool sb, int sc) = ("4sr",true,1); Console.WriteLine(sr); //元组写法5 元组元素是公共字段 所以可以单独引用 var (exa51, exa52) = ("51f", 5.1); Console.WriteLine(exa51); //元组写法6 元组元素是公共字段 所以可以单独引用 var example6 = ("post office", 6.3); (string destination, double distance) = example6; Console.WriteLine(distance); //元组写法7 将元组分配到各个已预声明的变量中。 var exa71 = string.Empty; var exa72 = 0.0; var example7 = ("post office", 7.2); (exa71, exa72) = example7; Console.WriteLine(exa72); //元组写法8 将元组分配到各个已预声明的变量中。 string country; string capital; double gdpPerCapita; (country, capital, gdpPerCapita) =("Malawi", "Lilongwe", 226.50); System.Console.WriteLine( $"The poorest country in the world in 2017 was {country}, {capital}: {gdpPerCapita}"); //元组写法9 弃元 将未命名的元组分配到一个隐式类型化变量中, var countrInfo = ("Malawi", "Lilongwe", 226.50); (string name, _, double gdpPerCapit) = countrInfo; Console.WriteLine(gdpPerCapit); //C#10混合定义 int y = 0; (var x, y, var z) = (1, 2, 3); 部分内容来源:https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2017/august/essential-net-csharp-7-0-tuples-explained
二、元组赋值和析构
C# 支持满足以下两个条件的元组类型之间的赋值:
1、两个元组类型有相同数量的元素
2、对于每个元组位置, 右侧元组元素的类型与左侧相应的元组元素的类型相同或可以隐式转换为左侧相应的元
组元素的类型
(int, double) t1 = (17, 3.14); (double First, double Second) t2 = (0.0, 1.0); t2 = t1; Console.WriteLine(t2);
三、比较运算符!= 和== 从 C# 7.3 开始支持
元组赋值和元组相等比较不会考虑字段名称。
同时满足以下两个条件时,两个元组可比较:
两个元组具有相同数量的元素。 例如,如果 t1 和 t2 具有不同数目的元素, t1 != t2 则不会进行编译。
对于每个元组位置,可以使用 == 和 != 运算符对左右侧元组操作数中的相应元素进行比较。 例如,
(1, (2, 3)) == ((1, 2), 3) 不会进行编译,因为 1 不可与 (1, 2) 比较。
(int a, byte b) left = (5, 10); (long a, int b) right = (5, 10); Console.WriteLine(left == right); // output: True Console.WriteLine(left != right); // output: False var t1 = (A: 5, B: 10); var t2 = (B: 5, A: 10); Console.WriteLine(t1 == t2); // output: True Console.WriteLine(t1 != t2); // output: False
== 和 != 运算符将以短路方式对元组进行比较。 也就是说,一旦遇见一对不相等的元素或达到元组的末尾,操
作将立即停止。 但是,在进行任何比较之前,将对所有元组元素进行计算,如以下示例所示:
Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4))); int Display(int s) { Console.WriteLine(s); return s; } // Output: // 1 // 2 // 3 // 4 // False
四、元组作为 out 参数
通常,你会将具有 out 参数的方法重构为返回元组的方法。 但是,在某些情况下, out 参数可以是元组类型。
下面的示例演示了如何将元组作为 out 参数使用:
var limitsLookup = new Dictionary<int, (int Min, int Max)>() { [2] = (4, 10), [4] = (10, 20), [6] = (0, 23) }; if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits)) { Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}"); } // Output: // Found limits: min is 10, max is 20
五、元组与 System.Tuple
System.ValueTuple 类型支持的 C# 元组不同于 System.Tuple 类型表示的元组。 主要区别如下:
ValueTuple 类型是值类型。 Tuple 类型是引用类型。
ValueTuple 类型是可变的。 Tuple 类型是不可变的。
ValueTuple 类型的数据成员是字段。 Tuple 类型的数据成员是属性。
六、元组结构
//元组写法9 弃元 将未命名的元组分配到一个隐式类型化变量中, var countrInfo = ("Malawi", "Lilongwe", 226.50); (string name, _, double gdpPerCapit) = countrInfo;
上面代码是不是很惊奇,反正我第一次看到时特别惊讶,也感觉特别有意思,那么这到底怎么实现的呢,我查看一下iL代码:
原来只是在类中添加一个解构函数(Deconstruct)就可以,解构参数方法名称必须是Deconstruct,返回值必须是void,参数列表必须是out
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
七 元组的本质
(string age, int sex, char name) bob2 = bob1; // 元组 //反编译 System.ValueTuple<string, int, char> valueTuple = new System.ValueTuple<string, int, char>("Bob", 23, 'M'); //(string,int) 是 ValueTuple<string,int> 的别名,
为什么我们需要元组?
让我们用一个问题来探究它。元组如何在不同类型的之间传递数据?
在 C# 7 之前,不支持在不声明数据类型的情况下传递具有多个值的数据。想象一下,如果我们需要从一个方法返回多个数据值,我们将不得不声明 type 并命名它。当您将松散相关的数据放在容器中时,很难命名事物。
下面的示例演示如何使用“值类型命名元组”返回两个值。它从书籍列表中返回最大和最小价格。它还提供了元组的各种用法。.
var bookService = new BookService(); //get me max, min prices of all books belong to a category. var thrillerPriceAggregates = bookService.CalculatePriceAggregatesBy(BookCategory.Thriller); //Notice how we can use properties of tuple by Name hence Named Tuples Console.WriteLine($"{nameof(thrillerPriceAggregates)} is {thrillerPriceAggregates.MaxPrice} {thrillerPriceAggregates.MinPrice}"); //tuples support deconstruction - notice variable does not need to match, and types in inferred var (maxPriceOfBook, minPriceOfBook) = thrillerPriceAggregates; Console.WriteLine($"Value Tuple Deconstruction {maxPriceOfBook} {minPriceOfBook}"); //Discard one value but project first value var (maxPrice, _ /*special operator-value is discarded*/) = thrillerPriceAggregates; Console.WriteLine($"Value Tuple Deconstruction with discard {maxPrice}"); //Use existing variable and project value on it double x = 500.00; double y = 100.00; (x, y) = thrillerPriceAggregates; Console.WriteLine($"Project tuple values on existing variables {x+y}"); public class BookService { private readonly List<Book> _books = new(); //Assume list has 1000 books. //Short hand named value type tuples syntax used as return type - (double MaxPrice, double MinPrice) public (double MaxPrice, double MinPrice) CalculatePriceAggregatesBy(BookCategory bookCategory) { var categoryBooks = _books.Where(b => b.BookCategory == bookCategory).ToList(); //return value tuple. return (categoryBooks.Max(b => b.Price), categoryBooks.Min(b => b.Price)); } } public class Book { public string Isbn { get; set; } public BookCategory BookCategory { get; set; } public string About { get; set; } public string AbstractDescription { get; set; } public double Price { get; set; } } public enum BookCategory { Thriller, Horror, Comedy, Nerdy }
使用值元组作为返回类型的缺点是使工具难以找到其用法。我鼓励你只在内部 API 中使用它。
我们也可以将值元组作为输入参数传递,但方法已经可以有多个输入参数。我鼓励你使用它。因为使用元组语法会损害可读性。
值元组适用于小尺寸的数据。但是,如果您需要返回多个引用类型或混合引用和值类型,请使用引用元组。
var bookService = new BookService(); //get me name of category, max, min prices of all books belong to a category. //return type is infered as Reference Type Tuple and projection is not supported var refTuple = bookService.CalculatePriceAggregatesBy(BookCategory.Thriller); //Notice how values are being accessed using Item1, Item2 Console.WriteLine($"{refTuple.Item1/*category name*/} {refTuple.Item2}"); public class BookService { private readonly List<Book> _books = new(); //Assume list has 1000 books. //Tuple<string, double, double> is return type //Reference type tuples do not support names public Tuple<string, double, double> CalculatePriceAggregatesBy(BookCategory bookCategory) { var categoryBooks = _books.Where(b => b.BookCategory == bookCategory).ToList(); //return value tuple. string categoryName=Enum.GetName(bookCategory); var max=categoryBooks.Max(b => b.Price); var min=categoryBooks.Min(b => b.Price); return Tuple.Create(categoryName, max, min); //Return a reference type tuple, thus by ref semantics } }
引用类型元组缺乏对命名值和值投影的支持,因此不适合可读性。我鼓励您仅在有性能原因时使用它。
元组的注意事项
- 不要作为输入参数传递,因为这会损害可读性。
- 将其用作非公共方法的返回类型,因为很难找到它们的用法。
- 将元组内的项目限制为最多 4 个,否则可读性将受到影响。
- 当引用类型处于混合状态时,不要使用值类型元组,因为性能会受到 Boxing 的影响。
- 请密切关注纯值类型元组的大小,因为如果大小较大,性能将受到影响。
- 在要提高性能的方案中使用值类型元组。
- 使用命名值类型元组。它适用于可读性。
- 通过实现(但仅在特定对象上实现)将元组支持添加到自定义类型。你可以在这里阅读更多关于解构的信息。
public void Deconstruct(out int id, out int name)
元组的替代方案
- 将自定义类型创建为 Struct 或 Class。 名称将随着时间的推移而出现。
- 使用语法非常简洁的记录
record Book(string Title, double Price);
性能比较
值元组的不正确使用可能会降低性能。下面的基准是对 1000 本书的列表进行分组,计算聚合,然后返回组中的类别名称、MinPrice、AvgPrice、MaxPrice 和 Books。引用类型元组之所以大胜一筹,是因为它是数据结构的正确选择。完整的代码在 GitHub 上。
下面的基准测试是使用 .一位审稿人要求使用 BenchmarkDotNet 进行 Benchmark,它只显示了微小的差异,但实际上从统计学上讲根本没有差异。