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

  

posted @ 2023-03-30 18:47  没有星星的夏季  阅读(558)  评论(0编辑  收藏  举报