我想做一些事情,比如:

MyObject myObj = GetMyObj(); // Create and fill a new object
MyObject newObj = myObj.Clone();

然后对未反映在原始对象中的新对象进行更改。

我通常不需要这个功能,所以当有必要时,我会创建一个新对象,然后单独复制每个属性,但这总是让我觉得有更好或更优雅的方法来处理这种情况。

如何克隆或深度复制对象,以便在原始对象中不反映任何更改的情况下修改克隆的对象?


当前回答

深度克隆就是复制状态。对于.net state,表示字段。

假设有一个层次结构:

static class RandomHelper
{
    private static readonly Random random = new Random();

    public static int Next(int maxValue) => random.Next(maxValue);
}

class A
{
    private readonly int random = RandomHelper.Next(100);

    public override string ToString() => $"{typeof(A).Name}.{nameof(random)} = {random}";
}

class B : A
{
    private readonly int random = RandomHelper.Next(100);

    public override string ToString() => $"{typeof(B).Name}.{nameof(random)} = {random} {base.ToString()}";
}

class C : B
{
    private readonly int random = RandomHelper.Next(100);

    public override string ToString() => $"{typeof(C).Name}.{nameof(random)} = {random} {base.ToString()}";
}

克隆可以通过以下方式完成:

static class DeepCloneExtension
{
    // consider instance fields, both public and non-public
    private static readonly BindingFlags bindingFlags =
        BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

    public static T DeepClone<T>(this T obj) where T : new()
    {
        var type = obj.GetType();
        var result = (T)Activator.CreateInstance(type);

        do
            // copy all fields
            foreach (var field in type.GetFields(bindingFlags))
                field.SetValue(result, field.GetValue(obj));
        // for every level of hierarchy
        while ((type = type.BaseType) != typeof(object));

        return result;
    }
}

演示1:

Console.WriteLine(new C());
Console.WriteLine(new C());

var c = new C();
Console.WriteLine($"{Environment.NewLine}Image: {c}{Environment.NewLine}");

Console.WriteLine(new C());
Console.WriteLine(new C());

Console.WriteLine($"{Environment.NewLine}Clone: {c.DeepClone()}{Environment.NewLine}");

Console.WriteLine(new C());
Console.WriteLine(new C());

结果:

C.random = 92 B.random = 66 A.random = 71
C.random = 36 B.random = 64 A.random = 17

Image: C.random = 96 B.random = 18 A.random = 46

C.random = 60 B.random = 7 A.random = 37
C.random = 78 B.random = 11 A.random = 18

Clone: C.random = 96 B.random = 18 A.random = 46

C.random = 33 B.random = 63 A.random = 38
C.random = 4 B.random = 5 A.random = 79

注意,所有新对象的随机字段都有随机值,但克隆与图像完全匹配

演示2:

class D
{
    public event EventHandler Event;
    public void RaiseEvent() => Event?.Invoke(this, EventArgs.Empty);
}

// ...

var image = new D();
Console.WriteLine($"Created obj #{image.GetHashCode()}");

image.Event += (sender, e) => Console.WriteLine($"Event from obj #{sender.GetHashCode()}");
Console.WriteLine($"Subscribed to event of obj #{image.GetHashCode()}");

image.RaiseEvent();
image.RaiseEvent();

var clone = image.DeepClone();
Console.WriteLine($"obj #{image.GetHashCode()} cloned to obj #{clone.GetHashCode()}");

clone.RaiseEvent();
image.RaiseEvent();

结果:

Created obj #46104728
Subscribed to event of obj #46104728
Event from obj #46104728
Event from obj #46104728
obj #46104728 cloned to obj #12289376
Event from obj #12289376
Event from obj #46104728

注意,事件支持字段也被复制,客户端也订阅了克隆的事件。

其他回答

我更喜欢复制构造函数而不是克隆。意图更加明确。

我找到了一种新的方法,那就是Emit。

我们可以使用Emit将IL添加到应用程序并运行它。但我认为这不是一个好方法,因为我想完善这一点,所以我写了我的答案。

Emit可以看到官方文件和指南

你应该学习一些IL来阅读代码。我将编写可以在类中复制属性的代码。

public static class Clone
{        
    // ReSharper disable once InconsistentNaming
    public static void CloneObjectWithIL<T>(T source, T los)
    {
        //see http://lindexi.oschina.io/lindexi/post/C-%E4%BD%BF%E7%94%A8Emit%E6%B7%B1%E5%85%8B%E9%9A%86/
        if (CachedIl.ContainsKey(typeof(T)))
        {
            ((Action<T, T>) CachedIl[typeof(T)])(source, los);
            return;
        }
        var dynamicMethod = new DynamicMethod("Clone", null, new[] { typeof(T), typeof(T) });
        ILGenerator generator = dynamicMethod.GetILGenerator();

        foreach (var temp in typeof(T).GetProperties().Where(temp => temp.CanRead && temp.CanWrite))
        {
            //do not copy static that will except
            if (temp.GetAccessors(true)[0].IsStatic)
            {
                continue;
            }

            generator.Emit(OpCodes.Ldarg_1);// los
            generator.Emit(OpCodes.Ldarg_0);// s
            generator.Emit(OpCodes.Callvirt, temp.GetMethod);
            generator.Emit(OpCodes.Callvirt, temp.SetMethod);
        }
        generator.Emit(OpCodes.Ret);
        var clone = (Action<T, T>) dynamicMethod.CreateDelegate(typeof(Action<T, T>));
        CachedIl[typeof(T)] = clone;
        clone(source, los);
    }

    private static Dictionary<Type, Delegate> CachedIl { set; get; } = new Dictionary<Type, Delegate>();
}

代码可以是深度复制,但它可以复制属性。如果你想让它成为深度副本,你可以改变它,因为IL太难了,我做不到。

我想你可以试试这个。

MyObject myObj = GetMyObj(); // Create and fill a new object
MyObject newObj = new MyObject(myObj); //DeepClone it

我对当前的答案做了一些基准测试,发现了一些有趣的事实。

使用BinarySerializer=>https://stackoverflow.com/a/78612/6338072

使用XmlSerializer=>https://stackoverflow.com/a/50150204/6338072

使用Activator.CreateInstance=>https://stackoverflow.com/a/56691124/6338072

这些是结果

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1734 (1909/November2019Update/19H2)

Intel Core i5-6200U CPU 2.30GHz(Skylake),1个CPU,4个逻辑核和2个物理核[主机]:.NET Framework 4.8(4.8.4400.0),X86 LegacyJIT默认作业:.NET Framework 4.8(4.8.4400.0),X86 LegacyJIT

Method Mean Error StdDev Gen 0 Allocated
BinarySerializer 220.69 us 4.374 us 9.963 us 49.8047 77 KB
XmlSerializer 182.72 us 3.619 us 9.405 us 21.9727 34 KB
Activator.CreateInstance 49.99 us 0.992 us 2.861 us 1.9531 3 KB

通用方法在技术上都是有效的,但我只想补充一点,因为我们实际上很少需要真正的深度复制,我强烈反对在实际的业务应用程序中使用通用深度复制,因为这使得您可能会在许多地方复制对象,然后显式修改,这很容易丢失。

在大多数实际情况下,您还希望对复制过程进行尽可能多的粒度控制,因为您不仅耦合到数据访问框架,而且在实践中,复制的业务对象几乎不应该100%相同。举一个ORM用来识别对象引用的referenceId的例子,一个完整的深度副本也会复制这个id,所以在内存中,对象会不同,一旦你将其提交到数据存储,它就会抱怨,因此,无论如何,您都必须在复制后手动修改此财产,如果对象发生更改,则需要在使用通用深度复制的所有位置对其进行调整。

用ICloneable扩展@cregox答案,到底什么是深度副本?它只是堆上新分配的一个对象,与原始对象相同,但占用不同的内存空间,而不是使用通用的克隆器功能,为什么不创建一个新对象呢?

我个人在我的域对象上使用静态工厂方法的思想。

例子:

    public class Client
    {
        public string Name { get; set; }

        protected Client()
        {
        }

        public static Client Clone(Client copiedClient)
        {
            return new Client
            {
                Name = copiedClient.Name
            };
        }
    }

    public class Shop
    {
        public string Name { get; set; }

        public string Address { get; set; }

        public ICollection<Client> Clients { get; set; }

        public static Shop Clone(Shop copiedShop, string newAddress, ICollection<Client> clients)
        {
            var copiedClients = new List<Client>();
            foreach (var client in copiedShop.Clients)
            {
                copiedClients.Add(Client.Clone(client));
            }

            return new Shop
            {
                Name = copiedShop.Name,
                Address = newAddress,
                Clients = copiedClients
            };
        }
    }

如果有人在研究如何在保持对复制过程的完全控制的同时构建对象实例化,这是我个人非常成功的解决方案。受保护的构造函数也使其成为可能,其他开发人员被迫使用工厂方法,该方法提供了一个简洁的单点对象实例化,封装了对象内部的构造逻辑。如果需要,您还可以重载该方法,并为不同的位置提供多个克隆逻辑。