C# ValueTuple 原理

本文揭示了C# 7中的ValueTuple工作原理,包括json转换中的命名问题、动态类型限制、性能优化以及命名规则。了解这些将帮助避免意外的编程错误和提高代码效率。

本文告诉大家一些 ValueTuple 的原理,避免在使用出现和期望不相同的值。ValueTuple 是 C# 7 的语法糖,如果使用的 .net Framework 是 4.7 以前,那么需要使用 Nuget 安装System.ValueTuple

虽然 ValueTuple 的很好用,但是需要知道他有两个地方都是在用的时候需要知道他原理。如果不知道原理,可能就发现代码和预期不相同

json 转换

先创建一个项目,然后安装 Json 解析,使用下面的代码,在运行之前,先猜一下,下面的代码会出现什么

            var foo = (name: "lindexi", site: "blog.youkuaiyun.com/lindexi_gd");
            var str = JsonConvert.SerializeObject(foo);

实际上输出的是 {"Item1":"lindexi","Item2":"blog.youkuaiyun.com/lindexi_gd"}

那么刚才的命名在哪?

如果想知道,那么请看 ValueTuple 的原理

原理

先来写一段代码,编译之后对他反编译,看一下他是怎么做的

        static void Main(string[] args)
        {
            var foo = Foo();
            var str = JsonConvert.SerializeObject(foo);
            Console.WriteLine(str);
        }
        static (string name, string site) Foo()
        {
            return (name: "lindexi", site: "blog.youkuaiyun.com/lindexi_gd");
        }

不需要安装反编译软件,可以使用这个网站拿到反编译

可以看到Foo被编译为 TupleElementNames 特性的两个字符串

    [return: TupleElementNames(new string[]
    {
        "name",
        "site"
    })]
    private static ValueTuple<string, string> Foo()
    {
        return new ValueTuple<string, string>("lindexi", "blog.youkuaiyun.com/lindexi_gd");
    }

所以实际上代码是 ValueTuple<string, string> 不是刚才定义的代码,只是通过 TupleElementNames 让编译器知道值,所以是语法糖。

IL 代码是

private hidebysig static valuetype [mscorlib]System.ValueTuple`2<string, string> 
    Foo() cil managed 
  {
    .param [0] 
    .custom instance void [mscorlib]System.Runtime.CompilerServices.TupleElementNamesAttribute::.ctor(string[]) 
      = (
        01 00 02 00 00 00 04 6e 61 6d 65 04 73 69 74 65 // .......name.site 这里就是 return: TupleElementNames 的命名
        00 00                                           // ..
      )
    .maxstack 2
    .locals init (
      [0] valuetype [mscorlib]System.ValueTuple`2<string, string> V_0
    )
    // [20 9 - 20 10]
    IL_0000: nop          
    // [21 13 - 21 72]
    IL_0001: ldstr        "lindexi"
    IL_0006: ldstr        "blog.youkuaiyun.com/lindexi_gd"
    IL_000b: newobj       instance void valuetype [mscorlib]System.ValueTuple`2<string, string>::.ctor(!0/*string*/, !1/*string*/)
    IL_0010: stloc.0      // V_0
    IL_0011: br.s         IL_0013
    // [22 9 - 22 10]
    IL_0013: ldloc.0      // V_0
    IL_0014: ret          
  }

这个特性只有编译器可以用,不可以在代码使用。

在上面的解释,实际上 IL 不知道存在定义的命名,所以不可以通过这个方法获得值。

动态类型获得值

如果希望使用动态类型获得值,那么下面的代码实际上会运行出现异常

        static void Main(string[] args)
        {
            dynamic foo = Foo();
            Console.WriteLine(foo.name);
        }
        static (string name, string site) Foo()
        {
            return (name: "lindexi", site: "blog.youkuaiyun.com/lindexi_gd");
        }

运行出现 RuntimeBinderException 异常,因为没有发现 name 属性

实际上对比下面匿名类,也就是很差不多写法。

        dynamic foo = new { name = "lindexi", site = "blog.youkuaiyun.com/lindexi_gd" };
            Console.WriteLine(foo.name);

运行是可以的,所以在使用动态类型,请不要使用 ValueTuple ,如果需要使用,那么请知道有存在找不到变量异常,而且是在运行才出现异常。

性能提升

如果使用 ValueTuple 编程会有一些优点,性能是其中之一。而且对于异步编程,使用 ValueTuple 可以继续使用 await 的方法。

假如有一个方法需要返回 5 个参数,那么以前的做法有三个方法,第一个方法是使用 out 的方法,第二个方法是使用 Tuple ,第三个方法是定义一个临时的类。

如果使用了 out 的方法,那么这个方法就不可以继续使用异步 await 的方法,因为 await 需要做出状态机,参见我写的await原理。如果使用 Tuple ,或这定义一个临时的类,就会出现性能的问题。

从上面的原理,已经告诉大家,ValueTuple 是值类型,而 Tuple 或定义的一个类不是值类型。编译器的优化是让 ValueTuple 分配在栈,对于普通的类分配在堆空间。如果一个类分配到堆空间,那么就需要使用垃圾回收才可以清理空间。而分配到栈就不需要使用垃圾回收,使用完成就清空栈,效率比堆空间大。

但是使用栈空间需要注意,栈空间是很小的,如果使用了大量栈空间可能会出现堆栈gg。因为考虑到部分刚入门的小伙伴,所以我就需要多说一些,上面说的 ValueTuple 使用了栈空间需要小心栈空间不足,和你存放的值的关系不大,而是和定义的 ValueTuple 数量有关,这个数量是非常大的。但是在递归方法中,本来是刚好空间足够的,在使用了 ValueTuple 可能就不够了。

使用 ValueTuple 可以继续使用异步,而且不需要垃圾回收,性能比Tuple高,所以建议在多返回参数使用 ValueTuple,而不是定义一个类。

其他需要知道的

不要随便定义一个看不懂的值

实际上下面的代码,编译是可以通过

(int x, (int y, (float a, float b))[] c) f1

但是这个值,在看的时候,几乎说不出他的属性

第二个需要知道的,ValueTuple 是值类型,所以他的默认值不是 null 而是 default(xx),在C# 7.2 支持使用关键字,所以不需要去写 defalut(xx,xx)

关于 ValueTuple 变量名的定义也是很难说的,有的小伙伴觉得需要使用 Axx 的方式命名,但是很多小伙伴觉得使用 aaBa 的命名更好,所以暂时对于他的命名使用 aaBa 的方法,大家觉得什么方式好请告诉我

参见: Exploring Tuples as a Library Author

C# 7: Dynamic types and Reflection cannot access Tuple fields by name

https://github.com/lindexi/lindexi/blob/3d4426cf2ec3a450978d90ff3079b23378626cb7/_posts/2018-08-10-C%23-ValueTuple-%E5%8E%9F%E7%90%86.md

<think>首先,用户的问题是:“我想查找C#中不允许重复元素的容器 请问C#中不允许重复元素的容器有哪些”。这是一个询问C#中不允许重复元素的容器的问题。 我需要在回答中参考系统指令和用户提供的引用。系统指令包括:使用$...$和$$...$$格式化数学表达式,但这个问题不涉及数学,所以可能不需要;使用中文回答;生成相关问题;引用的末尾添加引用标识;回答结构清晰可靠。 用户提供了几个引用: - 引用[1]:讨论SortedSet,它不允许重复元素。 - 引用[2]:提到HashSet<T>,不允许重复元素。 - 引用[3]:列出各种容器,包括HashSet<T>和SortedSet<T>,两者不允许重复。 - 引用[4]:关于元组,但元组不是容器;它提到Tuple和ValueTuple,但不涉及重复元素问题。 - 引用[5]:详细解释HashSet<T>的工作原理,它是基于哈希表的,不允许重复元素。 关键点:用户问的是C#中不允许重复元素的容器。从引用中,我可以看出: - HashSet<T>:不允许重复元素。[^2], [^3], [^5] - SortedSet<T>:有序集合,不允许重复。[^1], [^3] 引用[3]还提到SortedDictionary<TKey, TValue>,但它是一个键值对集合;键不允许重复,但值可以重复。用户问的是“容器”,可能指的是集合类型,而不是键值对。所以,我应该只包括集合类型如Set相关。 常见的C#容器不允许重复元素: - HashSet<T>:基于哈希表,无序,不允许重复。 - SortedSet<T>:基于排序,有序,不允许重复。 其他容器如List<T>允许重复,Dictionary<TKey, TValue>的键不允许重复,但值可以。用户明确说“容器”,focus on sets. 在我的回答中,我需要: 1. 使用中文回答。 2. 结构清晰:逐步解释。 3. 参考提供的引用:添加引用标识在末尾。 4. 生成相关问题。 5. 保证可靠:基于C#知识。 回答结构: - 先确认用户需求:查找不允许重复元素的容器。 - 列出主要容器:HashSet<T>和SortedSet<T>。 - 简要描述每个。 - 添加引用支持。 - 然后生成相关问题。 生成相关问题:根据系统指令示例,如“快速排序”的例子,生成了三个相关问题。我应该为这个主题生成相关问题。 相关问题ideas: 1. HashSet<T>和SortedSet<T>的性能差异是什么? 2. 如何在C#中实现自定义比较器来定义重复元素? 3. HashSet<T>内部是如何工作的?(基于引用[5]) 4. 什么时候使用HashSet<T> vs SortedSet<T>? 5. 是否有线程安全版本的Set容器? 基于引用[3],它有ConcurrentDictionary等,但Set没有线程安全版本?C#有ConcurrentBag,但那允许重复;没有ConcurrentSet。标准库中没有ConcurrentHashSet,但用户可以使用其他方式。 引用[3]提到ConcurrentDictionary, ConcurrentQueue等,但无ConcurrentSet。 生成2-3个相关问题。 最终回答大纲: 1. 介绍:C#中不允许重复元素的容器主要是Set集合。 2. 详细描述: - HashSet<T>:无序,基于哈希表。[^2], [^3], [^5] - SortedSet<T>:有序,排序。[^1], [^3] 3. 总结。 4. 生成相关问题。 添加引用标识:在每个引用点添加[^n],但指令说“回答中引用的段落末尾自然地添加引用标识”,意思是当引用内容时,在段落末尾添加。 例如,描述HashSet<T>后添加[^2], [^3], [^5]。 使用中文。 </think>### C#中不允许重复元素的容器介绍 在C#中,不允许重复元素的容器主要用于高效存储唯一元素。这些容器基于集合(Set)概念实现,通过特定数据结构确保元素唯一性。根据您的需求,我参考了相关引用资料,并结合C#标准库知识,整理出主要容器类型。以下是详细的解释: #### 1. **HashSet<T>(哈希集合)** - **特点**:HashSet<T> 是一个泛型集合容器,它不允许元素重复,且内部基于哈希表(Hash Table)实现。元素是无序存储的,不支持索引访问。当尝试添加重复元素时,它会自动忽略或拒绝操作。 - **底层原理**:使用哈希函数将元素映射到哈希值,并通过桶(Buckets)管理元素以避免冲突(常用链地址法解决碰撞)。其平均查找和插入操作的时间复杂度为 $O(1)$(常数时间),但在极端情况下可能退化为 $O(n)$(线性时间)[^5]。容器会自动调整大小以优化负载因子(元素数量与桶数量的比例)。 - **适用场景**:适用于高速去重操作,如检查元素是否存在或快速过滤重复数据。 ```csharp // 示例代码:创建一个HashSet并尝试添加重复元素 HashSet<int> numbers = new HashSet<int> { 1, 2, 3 }; bool added = numbers.Add(3); // 返回false,元素3已存在,不会添加 ``` 引用支持:[^2], [^3], [^5] #### 2. **SortedSet<T>(有序集合)** - **特点**:SortedSet<T> 也是一个泛型集合容器,不允许元素重复,但与HashSet<T>不同,它维护元素的排序状态(基于元素的自然顺序或自定义比较器)。类似C++的优先队列(堆),但提供完整集合功能。添加重复元素会被忽略。 - **底层原理**:通常基于平衡二叉树(如红黑树)实现,确保元素有序,查找和插入操作的平均时间复杂度为 $O(\log n)$(对数时间)。这使其在需要有序查询的场景更高效。 - **适用场景**:适用于需要自动排序且元素唯一的场景,如维护有序任务队列或实现自定义优先级逻辑。 ```csharp // 示例代码:创建一个SortedSet,元素会自动排序 SortedSet<int> sortedNumbers = new SortedSet<int> { 3, 1, 2 }; sortedNumbers.Add(2); // 忽略添加,因为2已存在 // 输出:[1, 2, 3] ``` 引用支持:[^1], [^3] ### 总结 - **主要容器**:C#中不允许重复元素的核心容器是 `HashSet<T>`(无序) 和 `SortedSet<T>`(有序)。它们都属于命名空间 `System.Collections.Generic`。 - **选择建议**: - 如果需要高速去重且不关心顺序,优先使用 `HashSet<T>`(性能更高)。 - 如果元素需要自动排序,选择 `SortedSet<T>`。 - **注意事项**:其他容器如 `Dictionary<TKey, TValue>` 的键(Key)不允许重复,但值(Value)可以重复;而 `List<T>` 或 `Array` 等允许元素重复,不符合您的需求[^3]。此外,C#标准库没有内置线程安全的Set容器,但可通过 `ConcurrentDictionary<TKey, TValue>` 模拟键的唯一性来实现类似功能[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值