首页 > 编程语言 >C#中的元组与解构

C#中的元组与解构

时间:2023-03-30 18:56:04浏览次数:44  
标签:Tuple C# tuple 元组 int var new 解构 ValueTuple

  元组

  元组提供了一种轻量级的方式,允许我们从一个方法中返回多个数据,而不需要自定义类或者使用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

  

标签:Tuple,C#,tuple,元组,int,var,new,解构,ValueTuple
From: https://www.cnblogs.com/shanfeng1000/p/15003115.html

相关文章

  • C#使用Java的秘钥对进行SHA256withRSA签名验签
    usingOrg.BouncyCastle.Crypto.Parameters;usingOrg.BouncyCastle.Security;usingSystem;usingSystem.Security.Cryptography;usingSystem.Text;usingSystem.Xml;///<summary>///RSA私钥,从Java格式转.net格式(不依赖第三方包)///</summary>//......
  • CentOS7 Docker安装 ElasticSearch8、Kibana8
    一、Docker安装yuminstall-ydocker#开机自启systemctlenabledocker#启动dockersystemctlstartdocker二、安装ElasticSearchdockerpulldocker.elastic.co/elasticsearch/elasticsearch:8.6.2vi/etc/sysctl.conf在/etc/sysctl.conf文件最后添加一行vm.ma......
  • [ABC273D] LRUD Instructions
    题目链接题解模拟题。观察题目,我们发现,无论问的是前/后/左/右,你都只会在一条直线上走,那对于这条直线,我们可以记录所有这条直线上的障碍物,然后找到距离当前点最近的障碍物,也就是说我们只能走到那个障碍物那块。虽然数据范围高达\(10^9\),但是\(n\le10^5\),所以用\(map\)套\(......
  • 如何限制进程内存:cgroup
    前两天刚知道cgroup,但是没用过,我就想做个简单的模拟OOM,网上搜了两天,发现一个比一个说的杂乱无章。 最后问的chatgpt:如何限制一个进程的内存在Linux系统中,可以使用cgroups来限制一个进程的内存。cgroups是一种内核机制,用于控制进程组的资源使用(CPU、内存、IO、网络等)。下面......
  • RocketMQ x OpenTelemetry 分布式全链路追踪最佳实践
    作者简介:艾阳坤,ApacheRocketMQPMCMember/Committer,CNCFOpenTelemetryMember,CNCFEnvoycontributor。在分布式系统中,多个服务之间的交互涉及到复杂的网络通信和数据传输,其中每个服务可能由不同的团队或组织负责维护和开发。因此,在这样的环境下,当一个请求被发出并经过多个......
  • 自制MCU的入门教程
    前言春意已起,却乍暖还寒。三四月的交接,是冷与暖的拥别。说明如今,以ChatGPT为代表的的人工智能驱动的语言处理系统已经火爆全网,开始渗透进日常生活;各种AI图像生成工具也大行其道,精美的生成图比肩专业的设计师。这些AI应用的广泛而快速的普及,是真的能够替代一些人的职位,导致失......
  • C# 当前进程是否有控制台窗口
    WPF应用程序,在VS的项目属性中,可以设置输出类型:那我们在代码中,如何判断应用的类型呢。有没有控制台?是否Windows应用程序还是控制台应用程序?Kernel32下函数GetConsoleWindow可以解决这个问题:[DllImport("kernel32.dll")]privatestaticexternIntPtrGetConsoleWindow();......
  • React 编程思想 #1
    React编程思想#1看太多语法,都不如简单尝试一下,跟着官方文档做了一下DEMO,文档写的真不错,就是没翻译完,一大半都还是英文(×_×),本篇其实大部分也是在重复文档内容,不过加上了自己的尝试。从原型开始React可以改变你对所看到的设计以及所构建的应用程序的看法。以前你看到的是......
  • Linux修改rc.local后重启无法进入系统
    Linux修改rc.local后重启无法进入系统复现:102服务器重启之后,一直卡在用户列表界面,但是不显示用户列表,用Alt+F2切换到黑屏终端,也无法进入终端 原因:在/etc/rc.d/rc.local中,在系统启动时会执行里面的任务,如果任务有问题,会导致无法进入系统 解决:1、重启服务器,在选择内核时,按......
  • Beego查数据库数据panic问题
    一开始没发现问题所在,请了位大佬帮忙排查错误逐步确定问题所在。问题起源于我查数据库没有得到正确的数据开始。一开始发现是数据类型问题,改过之后还是存在问题,于是debug一下,一步一步看问题出在哪里,结果走进了锁,就没仔细看,哪知在这中间出现了一个panic问题。但是这个panic没有打......