撰写了文章 发布于 2020-04-18 21:33:55
从C#开始的编程入门——元组
配图这个东西叫シャイ煮。
我们目前为止学到的内容中,所有方法要么没有返回值,要么返回了一个值。
那么如果你需要返回多个值怎么办?比如实现一个方法,同时返回列表中的最大值和最小值。注意是同时。
你可能已经想到了一种解决办法,那就是创建一个新的类来包装返回值。虽然这可以解决返回多个值的问题,但是又产生了一些新的问题。首先返回的多个值并没有过多的联系,包装在一起显得有点别扭。而如果程序中涉及到多处返回各种不同类型的值的组合,那么将需要额外创建很多不同的类,很麻烦,也使得代码不那么直观。
为了解决这种问题,就需要用到元组(tuple)。
元组源自一个数学概念,它是一个有序的有限个元素的序列。实际上你可以简单理解为就是把几个东西单纯地放在一起。
在学习较新版本中的语法之前,我们来了解一下元组的基础类型。
Tuple
用于表示元组的Tuple是一个范型类型,可以包含两个到7个甚至更多个元素(存在只有一个元素的元组,但较少使用,当元素个数达到七个以上时,第八个类型参数类型为元组类型,用于保存剩下的元素)。它从.NET Framework 4.0就得到支持。
构造元组只需要直接调用构造器并提供对应位置的元素即可。
Tuple<int, int> couple = new Tuple<int, int>(1, 2);
var a = couple.Item1;
var b = couple.Item2;
访问元素时使用ItemN属性来获得对应位置的元素。
N元组的名字:实际上不同长度的元组有各自的英语名称。比如一元组monuple、二元组couple、三元组triple等等。可以参见维基百科的这个表格。
Tuple还有一个非范型的静态类,主要提供创建N元组的帮助方法。Create方法可以创建各种类型的N元组。
var triple = Tuple.Create(“Mitsumine”, “Yuika”, 19);
Tuple存在一些问题。首先Tuple的元素属性没有明确的语义,只代表位置,很容易出错且不易理解。
Tuple是类类型,每次构造元组都需要在堆上分配空间,而对于元组的使用场景和频率,经常分配和回收这些小对象可能会造成不必要的性能损失。
值得注意的是,Tuple的ItemN属性是只读的,也就是说一旦通过Tuple创建了一个元组,则其元素就不能被修改成其它对象。作为一个用来表示不同对象的简单组合来说,显得有点严格。
ValueTuple
ValueTuple从.NET Framework 4.7以及.NET Core 1.0开始启用,随着C#7.0一起出现。基本用法和Tuple类似,和Tuple不同的是,顾名思义,它是值类型,避免了一些性能问题。另外,它的ItemN都是字段,并且可以自由修改。
让元组真正好用起来的实际上是从C#7.0开始支持的一些元组相关的语法。
如果你在使用其它版本的.NET实现,或者某些实现的较低版本,下面某些语法可能并不适用,请注意。
未命名元组
C#7.0开始提供了对元组的语法支持。元组字面量通过小括号进行初始化,括号内是元素列表,由逗号分隔。就像我前面说的,元组只是元素的单纯组合,它不需要所有元素都属于同一种类型。
var vector2 = (1, 2);
var guy = (“John”, “Doe”, 24);
当使用元组字面量创建一个元组时,得到的对象是一个ValueType的实例。
你也可以显示声明元组的类型,但是没有必要。因为很长很难写,很多时候使用var声明即可。
(int, int) t = (1, 2);
不过访问未命名元组时,仍然需要通过其Item1属性和Item2属性等等来访问对应位置上的元素,相关的问题依然存在。我们很难知道某个元素是什么东西,我们有没有拿错我们想要的元素。
命名元组
命名元组的引入极大地提高了代码可读性,也使得使用元组时更加方便。通过这种语法可以为元组的每个元素取一个名字,在访问时可以像访问类的成员一样直接使用这个名称。这些名称实际上相当于给ItemN取了一个别名,在编译之后这些名称就不存在了,但是却极大地方便了我们人类。 要创建命名元组只需要在元组元素表达式前加上名称和冒号即可。
var aqua = (FirstName:”Minato”, LastName:”Aqua”);
System.Console.WriteLine($”{aqua.FirstName} {aqua.LastName}”);
命名元组也可以显式声明类型,这个时候类型声明中带上名称即是元组字段的名称。
(int a, int b) t = (1, 2);
名称投影
我也不知道为什么要取这样一个炫酷的名字。简单来说投影初始化(tuple projection initializer)就是把用来初始化元组的变量名称直接作为元组元素的名称。比如说:
var firstName = “Sakura”;
var lastName = “Miko”;
var fullName = (firstName, lastName);
那么在之后使用这个元组时就可以:
Console.WriteLine(fullname.firstNaame);
由于命名元组的字段名称都是编译时别名,最终仍然对应的是ItemN字段,因此需要一些规则来避免造成歧义。元组进行名称投影时有以下规则,出现下列情况时不会发生名称投影:
- 当候选名称是元组的保留名称时。例如ToSttring、Item1、Rest。
- 当候选名称和另一个元组字段重复时。
例如:
var Item2 = 1;
var two = 2;
var tuple = (Item2, two);
这个时候元组的第二个字段可以通过two访问,但是**第一个字段只能使用Item1访问。** 又如:
var pt1 = (X: 3, Y: 0);
var pt2 = (X: 3, Y: 4);
var xCoords = (pt1.X, pt2.X);
这个时候对应的就是第二条规则。这个元组的两个字段都没有别名,只能使用IttemN访问。
相等性
元组之间可以相互比较。自C#7.3起,元组支持使用==和!=运算符进行相等性判别。这些运算符会将两个元组的字段从左到右依次进行比较,!==运算符是“短路的”,也就是说,一旦发现某个位置上的元素不相等则立即判断为不等。 比较时会尝试将不同类型的元素进行转换。如果无法转换则会发生编译时异常。
System.Console.WriteLine((1.0, 2.0) == (1, 2)); // True。发生了转换
System.Console.WriteLine((1,2) == (“1”, “2”)); // Error CS0019
另外,命名元组的名称不参与比较。不过,如果参与比较的元组中某一个元组被显式命名,且和另一个元组的名称不一样,会发生警告,但是不会发生错误。
赋值
当具有相同字段个数、并且对应位置字段类型相同或者可以完成转换时,则可以相互赋值。
var untamed = (1.0,2.0);
var named = (first:3, last:4);
named = unnamed;
var a = unnamed.first;
值得注意的是,命名元组即使被赋值(无论通过命名元组还是未命名元组)它的字段名称也不会发生改变,仍然可用。
作为返回值和参数
现在利用元组可以解决一开始的问题。比如我们返回一个列表的最大值和最小值。我们按照上面讲的方式构造一个元组返回即可。
public static (int max, int min) MaxMin(List numbers)
{
return (numbers.Max(), numbers.Min());
}
相应的,我们可能很少会将元组作为参数。因为和返回值不一样,参数本身就可以包含多个值的。但是我们可以把元组作为out参数的类型来修改外部的变量。
元组析构
析构(destruct)是构造(construct)的反义词。元组是几个值的组合,而在我们拿到一个元组时,我们可以把它拆开成几个变量。比如利用前面的MaxMin方法:
var maxmin = MaxMin(new List{1,3,12,4});
(int max, int min) = maxmin;
maxmin从MaxMin获得了一个元组,然后被析构成了两个本地变量,析构之后就可以在本作用域内直接使用析构后的变量。 也可以直接使用var进行类型推断:
var (max, min) = maxmin;
元组析构还有一个方便的特性是它可以析构用户定义的类型。只需要类型提供一个Deconstruct方法。比如:
class Person
{
public string FirstName { set; get; }
public string LastName { set; get; }
public void Deconstruct(out string firstName, out string lastName)
{
firstName = FirstName;
lastName = LastName;
}
}
这时就可以将一个Person对象作为一个元组析构。
var person = new Person() { FirstName = “Kaguya”, LastName = “Luna” };
var (first, last) = person;
更多有关元组的介绍,可以参见这里。
目录