C# List引用类型的克隆

本文介绍了三种在C#中克隆List的方法:通过反射、序列化(使用Newtonsoft.Json)和二进制序列化(使用BinaryFormatter)。每种方法都有其适用场景,反射无需依赖,序列化依赖Newtonsoft.Json,而二进制序列化则需确保实体类标记为可序列化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

有时候我们想克隆一个List去做别的事,而不影响原来的List,我们直接在list后面加上小点点,发现并没有Clone这样的扩展函数。这时候就只有自己扩展了。

尝试了三种方式,测试都通过了,至于性能方面我还没有做测试。

一、反射

 1  public static List<T> Clone<T>(this List<T> list) where T : new()
 2         {
 3             List<T> items = new List<T>();
 4             foreach (var m in list)
 5             {
 6                 var model = new T();
 7                 var ps = model.GetType().GetProperties();
 8                 var properties = m.GetType().GetProperties();
 9                 foreach (var p in properties)
10                 {
11                     foreach (var pm in ps)
12                     {
13                         if (pm.Name == p.Name)
14                         {
15                             pm.SetValue(model, p.GetValue(m));
16                         }
17                     }
18                 }
19                 items.Add(model);
20             }
21             return items;
22         }

二、序列化(依赖Newtonsoft.Json)

1  public static List<T> Clone<T>(this List<T> list) where T : new()
2         {
3             var str = JsonConvert.SerializeObject(list);
4             return JsonConvert.DeserializeObject<List<T>>(str);
5         }

 

三、序列化(BinaryFormatter)

 1 public static List<T> Clone<T>(this List<T> list)
 2         {
 3             using (Stream objectStream = new MemoryStream())
 4             {
 5                 IFormatter formatter = new BinaryFormatter();
 6                 formatter.Serialize(objectStream, list);
 7                 objectStream.Seek(0, SeekOrigin.Begin);
 8                 return (List<T>)formatter.Deserialize(objectStream);
 9             }
10         }

测试

1 private void Test()
2 {
3     List<NormalSetting> list = new List<NormalSetting>();
4     list.Add(new NormalSetting { RedisIp = "123" });
5     List<NormalSetting> items = list.Clone();
6     list[0].RedisIp = "456";
7     logMessager.Show("{0}:{1}", list[0].RedisIp, items[0].RedisIp);
8 }

注意事项:

第一种方式无需任何依赖。

第二种方式需要Newtonsoft.Json,如果项目中没有用到它,不推荐使用这种方式。

第三种方式序要给引用类型实体加上[Serializable]特性

转载于:https://www.cnblogs.com/cglandy/p/10301548.html

<think>我们正在讨论C#中的HashSet类型。根据之前的讨论,我们知道Dictionary<TKey, TValue>是引用类型。那么HashSet<T>呢? 首先,HashSet<T>是System.Collections.Generic命名空间下的一个类,它表示一组值的集合。在C#中,类(class)是引用类型。因此,HashSet<T>也是引用类型。 我们可以通过以下几个方面来验证和说明: 1. **内存分配**:引用类型分配在托管堆上,而值类型通常分配在栈上(但并非绝对,如作为类的字段时,值类型也会在堆上)。HashSet<T>作为一个类,其实例在堆上分配。 2. **赋值行为**:当我们将一个HashSet<T>变量赋值给另一个变量时,实际上复制的是引用(地址),而不是整个集合的内容。因此,两个变量将指向同一个HashSet<T>对象。 3. **参数传递**:当HashSet<T>作为参数传递给方法时,传递的是引用(地址),所以在方法内部对集合的修改会影响到原始对象(除非进行了克隆)。 4. **与值类型的区别**:值类型(如结构体)在赋值时复制整个数据,而HashSet<T>显然不会,因为复制整个集合的成本很高,而且不符合引用类型的特征。 我们可以通过一个简单的代码示例来验证: ```csharp using System; using System.Collections.Generic; class Program { static void Main() { // 创建第一个HashSet HashSet<int> set1 = new HashSet<int> { 1, 2, 3 }; // 将set1赋值给set2 HashSet<int> set2 = set1; // 修改set2 set2.Add(4); // 检查set1是否也被修改 Console.WriteLine(string.Join(", ", set1)); // 输出: 1, 2, 3, 4 // 因为set1和set2引用同一个对象,所以set1也包含了4 } } ``` 输出结果将显示set1中也包含了4,这证明了set1和set2指向同一个对象,因此HashSet<T>是引用类型。 另外,根据引用[4]中的说明:引用类型分配在托管堆上,值类型分配的位置取决于声明的位置。局部变量和参数在栈上,而作为引用类型字段的值类型则分配在堆上。HashSet<T>是一个类,所以它分配在堆上。 同时,引用[1]提到:通常我们创建的引用类型总是多于值类型。HashSet<T>作为集合类型,自然也是引用类型。 所以,结论是:**C#中的HashSet<T>是引用类型**。 此外,我们需要注意,即使HashSet<T>中存储的是值类型(如int),HashSet<T>本身仍然是引用类型,存储的值类型元素会被存放在堆上(因为HashSet<T>本身在堆上,其内部存储的元素也在堆上)。 例如: ```csharp HashSet<int> numbers = new HashSet<int>(); ``` 这里,虽然int是值类型,但numbers变量是一个引用,指向堆上的一个HashSet<int>对象,该对象内部存储int值(这些int值在堆上,作为HashSet内部数组的一部分)。 因此,我们可以总结如下: - HashSet<T>是引用类型,因为它是类(class)类型。 - 赋值操作复制引用,而不是复制整个集合。 - 作为参数传递时,传递的是引用,方法内部对集合的修改会反映到原始对象上。 最后,关于如何避免不必要的装箱拆箱(引用[5]):当HashSet<T>中存储的是值类型时,不会发生装箱,因为HashSet<T>是泛型集合,它直接存储值类型的值(不需要装箱)。例如,HashSet<int>存储int,不会装箱。但是,如果我们将值类型存储到非泛型集合(如ArrayList)中,则会发生装箱。 因此,使用泛型集合如HashSet<T>可以避免装箱拆箱操作,提高性能。 希望这个回答能够解决你的疑问。</think>### C# `HashSet<T>` 的类型分类 **`HashSet<T>` 属于引用类型**。这是由 C# 的类型系统决定的,具体分析如下: #### 1. **引用类型的核心特征** - `HashSet<T>` 是 **`class` 类型**(定义在 `System.Collections.Generic` 命名空间) - 实例分配在**托管堆**上,变量存储的是**内存地址引用** - 赋值操作复制的是**引用地址**而非数据本身 - 可被赋值为 `null`(值类型不可为 `null`) #### 2. **验证示例** ```csharp var set1 = new HashSet<int> { 1, 2, 3 }; var set2 = set1; // 复制引用(非数据) set2.Add(4); // 修改第二个集合 Console.WriteLine(set1.Contains(4)); // 输出 True // 证明 set1 和 set2 指向同一对象 ``` #### 3. **内存分配原理** 根据引用[4]的说明: > "变量的值在它声明的位置存储。引用类型中值类型字段的存储位置与引用类型对象一起在堆上" > — [^4] 当 `HashSet<int>` 存储值类型时: - `HashSet` 对象本身在堆上 - 内部存储的 `int` 值也随对象在堆上 - 仅局部变量(如方法内的 `HashSet` 变量)的引用地址在栈上 #### 4. **与值类型的本质区别** | 特性 | `HashSet<T>` (引用类型) | 值类型 (如 `struct`) | |---------------------|------------------------|----------------------| | **继承关系** | 派生自 `System.Object` | 派生自 `System.ValueType`[^3] | | **赋值行为** | 复制引用地址 | 复制完整数据 | | **默认参数传递** | 按引用传递(传地址) | 按值传递(传副本) | | **内存开销** | 需堆分配 + 对象头 | 通常更轻量 | #### 5. **特殊注意事项** - **存储值类型时**:元素被**直接存储在堆中**(无额外装箱) ```csharp var pointSet = new HashSet<Point>(); var p = new Point(5, 10); pointSet.Add(p); // 值类型直接存入堆 p.X = 100; // 修改原始值 Console.WriteLine(pointSet.First().X); // 输出 5 (未改变) ``` - **存储引用类型时**:保存的是**对象引用** ```csharp var strSet = new HashSet<StringBuilder>(); var sb = new StringBuilder("Hello"); strSet.Add(sb); // 存储引用 sb.Append("!"); // 修改原始对象 Console.WriteLine(strSet.First()); // 输出 "Hello!" (同步改变) ``` #### 6. **设计意义** 作为引用类型的优势: - **动态扩容**:自动调整内部存储空间 - **高效共享**:多线程可通过引用共享同一集合 - **空值支持**:可用 `null` 表示未初始化 - **避免装箱**:泛型设计确保值类型元素**不装箱**[^5] > 引用[2]强调的"结构体包含引用类型字段仍是值类型"原则同样适用:即使 `HashSet<T>` 包含值类型元素,其自身仍是引用类型[^2]。 --- ### 相关问题 1. **C# 中值类型和引用类型在内存分配上有何区别?** [值类型通常分配在栈上,但作为类字段时在堆上;引用类型始终在堆上,变量存储引用地址[^4]] 2. **如何正确比较两个 `HashSet` 对象的内容是否相等?** [使用 `SetEquals()` 方法:`set1.SetEquals(set2)` 比较元素内容而非引用] 3. **为什么集合类型作为方法参数传递时会被修改原始对象?** [引用类型参数传递的是内存地址,方法内操作直接影响原始对象[^4]] 4. **C# 中哪些集合类型是值类型?** [所有标准集合(`List<T>`, `Dictionary<K,V>`, `Queue<T>` 等)都是引用类型;值类型集合需自定义 `struct` 实现]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值