C#中的对象和拷贝
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
问题:在C#中,先创建类A的实例a并赋值其成员变量,再创建实例b,然后执行 b = a,是否能让b自动拥有和a相同的成员变量值。
核心解答
首先直接给出结论:执行 b = a 并不会让b拥有a的成员变量副本,而是让b和a指向内存中的同一个对象。这不是“赋值成员变量”,而是“引用赋值”。
1. 代码演示与解释
下面用一段完整的代码来展示这个过程:
using System;
// 定义类A
public class A
{
// 成员变量
public int Number { get; set; }
public string Text { get; set; }
}
class Program
{
static void Main()
{
// 1. 创建实例a并赋值成员变量
A a = new A();
a.Number = 10;
a.Text = "Hello";
// 2. 创建实例b(此时b指向一个新的空对象)
A b = new A();
// 3. 执行 b = a
b = a;
// 4. 验证结果
Console.WriteLine($"b.Number = {b.Number}"); // 输出:10
Console.WriteLine($"b.Text = {b.Text}"); // 输出:Hello
// 5. 修改b的成员变量,看a是否变化
b.Number = 20;
Console.WriteLine($"a.Number = {a.Number}"); // 输出:20(a的值也变了)
}
}
关键解释:
- C#中的
class是引用类型,变量a和b本质上是“指向对象的引用(地址)”,而非对象本身。 new A()会在内存中创建一个新的A对象,并返回该对象的地址。- 执行
b = a时,只是把a中存储的“对象地址”复制给了b,此时a和b都指向同一个内存对象:- 你通过
b访问成员变量,其实是访问的a指向的那个对象,所以能看到a的赋值; - 修改
b的成员变量,本质是修改同一个对象,因此a的成员变量也会同步变化; - 最初
new A()创建的b的原始对象,因为没有引用指向它,会被GC(垃圾回收器)回收。
- 你通过
2. 如何真正复制成员变量(深拷贝/浅拷贝)
如果你想让b成为一个独立的对象,但拥有和a相同的成员变量值,需要手动实现“拷贝”逻辑:
方式1:手动赋值每个成员变量(简单场景)
A a = new A { Number = 10, Text = "Hello" };
A b = new A();
// 手动复制每个成员变量
b.Number = a.Number;
b.Text = a.Text;
// 修改b不会影响a
b.Number = 20;
Console.WriteLine(a.Number); // 输出:10
方式2:实现ICloneable接口(通用场景)
public class A : ICloneable
{
public int Number { get; set; }
public string Text { get; set; }
// 实现克隆方法
public object Clone()
{
// 浅拷贝(值类型成员复制,引用类型成员复制引用)
return new A
{
Number = this.Number,
Text = this.Text
};
// 如果有引用类型成员(如List),需要实现深拷贝:
// return new A {
// Number = this.Number,
// Text = this.Text,
// List = new List<int>(this.List) // 重新创建List并复制元素
// };
}
}
// 使用
A a = new A { Number = 10, Text = "Hello" };
A b = (A)a.Clone(); // 克隆出独立的对象
b.Number = 20;
Console.WriteLine(a.Number); // 输出:10
总结
b = a(引用类型)不会复制成员变量,只是让b和a指向同一个对象,修改其一都会影响另一个;- 若要让b拥有a的成员变量值且相互独立,需手动复制每个成员变量或实现克隆方法;
- C#中
class是引用类型,struct是值类型(struct的赋值会复制成员变量),这是核心区别。
如何在C# 中如何正确实现深拷贝
所谓深拷贝,也就是让新对象拥有和原对象完全相同的成员变量值,且两个对象相互独立(即使包含引用类型成员,修改新对象也不会影响原对象)。
什么是深拷贝(先理清概念)
- 浅拷贝:只复制值类型成员,引用类型成员仅复制引用(两个对象共享同一个引用类型对象)。
- 深拷贝:不仅复制值类型成员,还会为引用类型成员创建全新的实例,并复制其内部值,最终两个对象完全独立。
下面我会给出几种常用且实用的深拷贝实现方式,按“易用性+场景适配”排序:
方式1:手动实现深拷贝(最可控、性能最好)
适合成员变量较少、结构固定的类,手动为每个成员(包括引用类型成员)创建新实例并赋值,是最直观且性能最优的方式。
using System;
using System.Collections.Generic;
// 定义包含引用类型成员的类
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
public class Person
{
// 值类型成员
public int Age { get; set; }
// 引用类型成员
public string Name { get; set; }
public Address HomeAddress { get; set; }
// 集合类型(引用类型)
public List<string> Hobbies { get; set; }
// 手动实现深拷贝方法
public Person DeepClone()
{
// 1. 创建新的Person实例
var clone = new Person
{
// 复制值类型成员
Age = this.Age,
// string是特殊的引用类型(不可变),直接赋值等价于深拷贝
Name = this.Name
};
// 2. 为引用类型成员创建新实例并赋值(核心:深拷贝关键)
if (this.HomeAddress != null)
{
clone.HomeAddress = new Address
{
City = this.HomeAddress.City,
Street = this.HomeAddress.Street
};
}
// 3. 为集合类型创建新实例并复制元素
if (this.Hobbies != null)
{
clone.Hobbies = new List<string>(this.Hobbies);
}
return clone;
}
}
// 测试代码
class Program
{
static void Main()
{
// 创建原对象并赋值
var person1 = new Person
{
Age = 25,
Name = "张三",
HomeAddress = new Address { City = "北京", Street = "长安街" },
Hobbies = new List<string> { "读书", "跑步" }
};
// 深拷贝
var person2 = person1.DeepClone();
// 修改拷贝对象的成员
person2.Age = 30;
person2.HomeAddress.City = "上海"; // 修改引用类型成员
person2.Hobbies.Add("游泳"); // 修改集合
// 验证原对象不受影响
Console.WriteLine(person1.Age); // 输出 25(值类型独立)
Console.WriteLine(person1.HomeAddress.City); // 输出 北京(引用类型独立)
Console.WriteLine(string.Join(",", person1.Hobbies)); // 输出 读书,跑步(集合独立)
}
}
关键说明:
- 对于
string类型,虽然是引用类型,但它是不可变的,直接赋值即可视为深拷贝; - 对于自定义引用类型(如
Address),必须创建新实例并复制其成员; - 对于集合(如
List),必须创建新的集合实例,并复制原集合的元素。
方式2:通过序列化实现通用深拷贝(最便捷、适配复杂类)
适合成员变量多、结构复杂的类,利用序列化将对象转为字节流,再反序列化为新对象,自动实现深拷贝。需要注意:所有涉及的类都要标记[Serializable]。
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections.Generic;
// 必须标记[Serializable]才能序列化
[Serializable]
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
[Serializable]
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public Address HomeAddress { get; set; }
public List<string> Hobbies { get; set; }
}
// 通用深拷贝工具类
public static class CloneHelper
{
// 泛型方法,适配任意可序列化的类
public static T DeepClone<T>(T obj)
{
// 处理空值
if (obj == null) return default;
// 创建二进制序列化器
BinaryFormatter formatter = new BinaryFormatter();
// 使用内存流存储序列化后的字节
using (MemoryStream stream = new MemoryStream())
{
// 序列化原对象到内存流
formatter.Serialize(stream, obj);
// 将流指针重置到起始位置
stream.Seek(0, SeekOrigin.Begin);
// 反序列化为新对象(深拷贝)
return (T)formatter.Deserialize(stream);
}
}
}
// 测试代码
class Program
{
static void Main()
{
var person1 = new Person
{
Age = 25,
Name = "张三",
HomeAddress = new Address { City = "北京", Street = "长安街" },
Hobbies = new List<string> { "读书", "跑步" }
};
// 通用深拷贝
var person2 = CloneHelper.DeepClone(person1);
// 修改拷贝对象
person2.HomeAddress.City = "上海";
person2.Hobbies.Add("游泳");
// 验证原对象不受影响
Console.WriteLine(person1.HomeAddress.City); // 输出 北京
Console.WriteLine(string.Join(",", person1.Hobbies)); // 输出 读书,跑步
}
}
注意事项:
BinaryFormatter在.NET Core/.NET 5+中需要引入 NuGet 包:System.Runtime.Serialization.Formatters;- 所有嵌套的类(如
Address)都必须标记[Serializable],否则会抛出序列化异常; - 性能略低于手动拷贝,但胜在通用、无需手动维护拷贝逻辑。
方式3:使用System.Text.Json(无序列化标记,适配现代.NET)
如果不想标记[Serializable],可以用.NET Core/.NET 5+内置的System.Text.Json实现深拷贝,无需额外依赖:
using System;
using System.Text.Json;
using System.Collections.Generic;
// 无需标记Serializable
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public Address HomeAddress { get; set; }
public List<string> Hobbies { get; set; }
}
public static class CloneHelper
{
public static T DeepClone<T>(T obj)
{
if (obj == null) return default;
// 序列化为JSON字符串,再反序列化为新对象
string json = JsonSerializer.Serialize(obj);
return JsonSerializer.Deserialize<T>(json);
}
}
// 测试代码和方式2一致,效果相同
优缺点:
- 优点:无需标记特性、使用现代API、跨平台友好;
- 缺点:性能比二进制序列化略低,且对一些特殊类型(如
DateTimeOffset、自定义结构体)需要额外配置序列化选项。
总结
- 手动深拷贝:性能最优、可控性强,适合成员少、结构稳定的类;
- 二进制序列化:通用、适配复杂类,但需要标记
[Serializable],性能中等; - Json序列化:无需标记特性、现代.NET友好,性能略低,适合快速开发场景;
- 核心原则:深拷贝的关键是为所有引用类型成员创建新实例,而非仅复制引用,确保原对象和拷贝对象完全独立。