C#中的元组与解构
元组
元组提供了一种轻量级的方式,允许我们从一个方法中返回多个数据,而不需要自定义类或者使用out、ref等关键字,如:
public Tuple<int, string> GetTuple() { return new Tuple<int, string>(200, "OK"); }
上面是C#7.0之前的写法,从C#7.0开始,元组被分为两种:Tuple 和 ValueTuple,而上面的例子可以写成:
public (int, string) GetTuple() { return (200, "OK"); }
Tuple 和 ValueTuple的区别主要有:
1、Tuple是类(引用类型)、而ValueTuple是结构体(值类型) 2、Tuple中的每一项是只读的属性,而ValueTuple中的每一项都是字段,因此是可读可写的
Tuple还是原来那个Tuple,原来的配方,熟悉的味道,而ValueTuple是后来新加的类型,几乎所有Tuple能实现的地方,ValueTuple也能实现,而相比Tuple,ValueTuple又有许多不一样的地方:
1、声明与初始化
Tuple的初始化只能通过构造函数的参数传入,而ValueTuple则有多种形式,如:
//Tuple的初始化 Tuple<int, int> tuple1 = new Tuple<int, int>(1, 2); var tuple2 = Tuple.Create<int, string>(200, "OK"); //ValueTuple的初始化 ValueTuple<int, int> valueTuple1 = (1, 2);//形如 (a,b,c,...) 格式的写法来初始化 var valueTuple2 = (1, 2);//使用var简化 (int, int) valueTuple3 = new ValueTuple<int, int>(1, 2);//new var valueTuple4 = new ValueTuple<int, int>() { Item1 = 1, Item2 = 2 };//类似属性初始化
注:使用 (a,b,c...) 这样的格式初始化ValueTuple时至少需要两个元素,一个元素被括号当做运算符而非元组标志,如果要初始化一个元素的元组(没啥作用),需要使用new的方式
另外,Tuple和ValueTuple最多只支持8个泛型参数,如果还要多个,就必须使用第八参数Rest进行拓展,对于Tuple这无疑增加了初始化的难度,取值也不方便,而ValueTuple使用 (a,b,c,....) 这种格式的初始化可以很好地解决这个问题:
//初始化拥有20项的Tuple var tuple = new Tuple<int, int, int, int, int, int, int, Tuple<int, int, int, int, int, int, int, Tuple<int, int, int, int, int, int>>>( 1, 2, 3, 4, 5, 6, 7, new Tuple<int, int, int, int, int, int, int, Tuple<int, int, int, int, int, int>>(8, 9, 10, 11, 12, 13, 14, new Tuple<int, int, int, int, int, int>(15, 16, 17, 18, 19, 20)) ); //Tuple取最后一个元素 int last_of_tuple = tuple.Rest.Rest.Item6; //初始化拥有20项的ValueTuple var valueTuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20); //valueTuple类型是ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int, int, int, int, int>>> //ValueTuple取最后一个元素 var last_of_valueTuple1 = valueTuple.Rest.Rest.Item6;//方式一 var last_of_valueTuple2 = valueTuple.Item20;//方式二
2、ValueTuple可以声明元组每一项的名称
这个是Tuple的一个不足,因为每一项都是Item1、Item2等等,因此无法直观的知道Tuple中的每一项含义,因此Tuple的可读性一般都比较差。
为解决这个问题,ValueTuple因为可以从 (a,b,c...) 这样的格式初始化,于是就提供了一种自定义字段名称方式,如:
//方式一 (int count, double sum) tuple = (1, 3.14); //方式二 var tuple = (count: 1, sum: 3.14); //方式三 int count = 1; double sum = 3.14; var tuple = (count, sum); //使用自定义的名称,而非Item1、Item2等 Console.WriteLine($"count:{tuple.count} sum:{tuple.sum}"); //当然,也可以使用Item1、Item2等 Console.WriteLine($"count:{tuple.Item1} sum:{tuple.Item2}");
上面有三种方式自定义字段名称,我们甚至可以将组合使用:
int a = 1; var tuple = (a, b: "hello", true); ////使用自定义的名称,而非item1、item2等 Console.WriteLine($"a:{tuple.a} b:{tuple.b} c:{tuple.Item3}"); ////当然,也可以使用item1、item2等 Console.WriteLine($"a:{tuple.Item1} b:{tuple.Item2} c:{tuple.Item3}");
但是自定义字段要求:
1、同一个元组内的自定义名称不能重复 2、自定义名称不能是元组中已存在的成员名称,如:Rest,ToString等 3、自定义名称可以是元组中存在的项,如Item1,但是顺序应该相同
例子:
var tuple = (count: 1, count: 1);//错误,名称重复 var tuple = (count: 1, Rest: 2, ToString: 3);//错误,不能是已存在的成员名称(Rest是第八项的名名称) var tuple = (Item1: 1, Item3: 2);//错误,两元素元组只有Item1、Item2,名称可以是Item1和Item2,不能是Item3,同理三元素元组只有Item1、Item2、Item3,名称不能是Item4 var tuple = (Item2: 1, Item1: 2);//错误,顺序不对
注:自定义的名称只在编译时有效,在运行时还是会使用Item1、Item2等等,运行时通过反射也只会获取到Item1、Item2等属性字段,没有自定义的名称,换句话说,自定义的名称只是Item1、Item2等的别名
3、ValueTuple支持 == 和 != 运算符
因为ValueTuple是结构体,因此可以使用 == 和 != 来判断两个元组中的元素是否一样,而Tuple因为是引用类型,所以 == 和 != 运算符进行的是引用判断是否是同一实例:
//Tuple bool b1 = new Tuple<int, string>(1, "abc") == new Tuple<int, string>(1, "abc");//false //ValueTuple bool b2 = new ValueTuple<int, string>(1, "abc") == new ValueTuple<int, string>(1, "abc");//true bool b3 = new ValueTuple<int, string>(1, "abc") == (1, "abc");//true bool b4 = (1, "abc") == (1, "abc");//true //== 和 != 运算符与自定义的名称无关,只与顺序有关 bool b5 = (a: 1, b: "abc") == (1, "abc");//true bool b6 = (1, b: "abc") == (code: 1, "abc");//true var tuple = (a: 1, b: "abc"); int code = 1; string message = "abc"; bool b7 = tuple == (code, message);//true
元组的 == 和 != 运算符比较规则是将两个元组中的相同位置的项进行 == 和 != 运算,因此,要求两个元组具有相同的结构及长度
4、赋值
ValueTuple比Tuple更实用还表现在赋值方面,如果是单个变量的赋值,两者的要求是一样的,就是要求元组具有相同的结构及长度。
但是ValueTuple在赋值方面玩出了新高度:
var point = (0, 90); (int x, int y) = point;//定义两个变量并使用元组初始化 //(var x, var y) = point;//等价写法 //var (x, y) = point;//等价写法 int a = 0; int b = 0; (a, b) = point;//给已经声明过的变量赋值
这种赋值方法在某些场景值需要对多个变量赋值是很有用。
注:可能你会发现,Tuple也能这样赋值,其实这是解构在起作用
还有,某些情况下,我们可能只需要元组中的某些值,这个时候可以结合弃元一起使用:
var value = (1, true, "hello", 3.14); (_, bool b, string str, _) = value;//取第二、三项 (_, _, _, var pi) = value;//取最后一项 (int i, _, _, _) = value;//取第一项
5、ValueTuple与Tuple之间的转换
ValueTuple与Tuple是两个不同的东西,两者之间并不能通过隐式或者显式的转换得到,但是可以通过现有的拓展类TupleExtensions中的拓展方法来实现转换:
//Tuple转ValueTuple var tuple = new Tuple<int, string>(200, "OK"); var valueTuple = tuple.ToValueTuple(); Console.WriteLine($"{valueTuple.Item1}-{valueTuple.Item2}"); //ValueTuple转Tuple var valueTuple = (200, "OK"); var tuple = valueTuple.ToTuple(); Console.WriteLine($"{tuple.Item1}-{tuple.Item2}");
如果TupleExtensions中的拓展方法不够用,可能就需要我们自行实现一个拓展方法了。
解构
在C#中,将对象转换为(a,b,c...) 这样的格式的过程称为解构。
一个对象、结构体、接口想要解构,需要提供一个 Deconstruct
方法,且需遵循下面的规则:
1、方法名必须是 Deconstruct 2、方法只能是实例方法或者拓展方法 3、方法返回类型必须是 void 4、方法的每一个参数必须使用out关键字修饰(这些参数对应(a,b,c...) 这样的格式中各项) 5、Deconstruct 方法运行重载,继承,但重载与参数类型无关,只与参数个数有关
.net框架中,给我们编译时预先定义好的Deconstruct
方法的类不多,比如Tuple,记录(record)、DictionaryEntry类等,其它情况下就可能需要我们自行根据这个规则来定义Deconstruct
方法了。
Deconstruct
方法可以是实例方法,可以直接定义,或者集成,或者实现接口得到,也可以重载,但是重载只与参数个数有关,而与参数类型无关。
比如实例方法:
interface IFather { void Deconstruct(out int a, out int b); }
class Father : IFather { public void Deconstruct(out int a, out int b) => (a, b) = (1, 2);//解构赋值 public void Deconstruct(out int a, out int b, out string c) => (a, b, c) = (3, 4, "abc");//解构赋值 }
class Son : Father { }
static void Main(string[] args) { int a, b; string c; (a, b) = new Father();//类自身提供的Deconstruct (a, b, c) = new Father();//类自身提供的Deconstruct重载 (a, b) = new Son();//子类继承的Deconstruct (a, b, c) = new Son();//子类继承的Deconstruct IFather father = new Father(); (a, b) = father;//接口提供的Deconstruct }
Deconstruct方法也可以是拓展方法,我们可以重载,但是重载只与参数个数有关,而与参数类型无关。
基于拓展方法,我们就可以为已经定义好的类进行解构,比如结构DateTime,获取年月日等:
public static class DateTimeExtensions { public static void Deconstruct(this DateTime dateTime, out int year, out int month, out int day) => (year, month, day) = (dateTime.Year, dateTime.Month, dateTime.Day);//解构赋值 public static void Deconstruct(this DateTime dateTime, out int year, out int month, out int day, out int hour, out int minute, out int second) => (year, month, day, hour, minute, second) = (dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second);//解构赋值 } static void Main(string[] args) { //直接取年月日 var (year, month, day) = DateTime.Now; //或者 var (year, month, day, hour, minute, second) = DateTime.Now; }
回过头来,前面说了,Tuple也能这样解构赋值,这就是因为Tuple有这样一个Deconstruct拓展方法,在TupleExtensions类中。
参考文档1:https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/deconstruct
参考文档2:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples