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引用类型,变量ab本质上是“指向对象的引用(地址)”,而非对象本身。
  • new A() 会在内存中创建一个新的A对象,并返回该对象的地址。
  • 执行 b = a 时,只是把a中存储的“对象地址”复制给了b,此时ab都指向同一个内存对象
    • 你通过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

总结

  1. b = a(引用类型)不会复制成员变量,只是让b和a指向同一个对象,修改其一都会影响另一个;
  2. 若要让b拥有a的成员变量值且相互独立,需手动复制每个成员变量实现克隆方法
  3. 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、自定义结构体)需要额外配置序列化选项。

总结

  1. 手动深拷贝:性能最优、可控性强,适合成员少、结构稳定的类;
  2. 二进制序列化:通用、适配复杂类,但需要标记[Serializable],性能中等;
  3. Json序列化:无需标记特性、现代.NET友好,性能略低,适合快速开发场景;
  4. 核心原则:深拷贝的关键是为所有引用类型成员创建新实例,而非仅复制引用,确保原对象和拷贝对象完全独立。
kumakoko avatar
kumakoko
pure coder
comments powered by Disqus