【问题标题】:Is there a much better way to create deep and shallow clones in C#?有没有更好的方法在 C# 中创建深克隆和浅克隆?
【发布时间】:2011-12-22 23:27:27
【问题描述】:

我一直在为一个项目创建对象,并且在某些情况下我必须为这些对象创建一个深层副本我想出了使用 C# 的内置函数 MemberwiseClone()。困扰我的问题是,每当我创建了一个新类时,我就必须编写一个类似下面代码的函数来进行浅拷贝。有人可以帮我改进这部分并给我一个更好的浅拷贝比第二行代码。谢谢:)

浅拷贝:

public static RoomType CreateTwin(RoomType roomType)
{
    return (roomType.MemberwiseClone() as RoomType);
}

深拷贝:

public static T CreateDeepClone<T>(T source)
{
    if (!typeof(T).IsSerializable)
    {
        throw new ArgumentException("The type must be serializable.", "source");
    }

    if (Object.ReferenceEquals(source, null))
    {
        return default(T);
    }

    IFormatter formatter = new BinaryFormatter();
    Stream stream = new MemoryStream();
    using (stream)
    {
        formatter.Serialize(stream, source);
        stream.Seek(0, SeekOrigin.Begin);
        return (T)formatter.Deserialize(stream);
    }
}

【问题讨论】:

  • Cloning objects in C#的可能重复
  • 嗯,谢谢你的链接,我向我展示了我正在寻找的东西 :)
  • 错误你的例子显示了一个浅克隆。
  • 是的,我知道这是完全错误的,让我改变它;)

标签: c# .net oop cloning


【解决方案1】:

sll 建议的使用序列化的解决方案是迄今为止最简单的,但如果您尝试克隆的类型不可序列化,则该解决方案不起作用。

Felix K. 的代码是一个不错的选择,但我发现它存在一些问题。这是一个修订版,修复了我发现的一些问题。我还删除了一些我不需要的功能(例如构造函数参数)。

/// <summary>
/// A DeepClone method for types that are not serializable.
/// </summary>
public static T DeepCloneWithoutSerialization<T>(this T original)
{
    return original.deepClone(new Dictionary<object, object>());
}

static T deepClone<T>(this T original, Dictionary<object, object> copies)
{
    return (T)original.deepClone(typeof(T), copies);
}

/// <summary>
/// Deep clone an object without using serialisation.
/// Creates a copy of each field of the object (and recurses) so that we end up with
/// a copy that doesn't include any reference to the original object.
/// </summary>
static object deepClone(this object original, Type t, Dictionary<object, object> copies)
{
    // Check if object is immutable or copy on update
    if (t.IsValueType || original == null || t == typeof(string) || t == typeof(Guid)) 
        return original;

    // Interfaces aren't much use to us
    if (t.IsInterface) 
        t = original.GetType();

    object tmpResult;
    // Check if the object already has been copied
    if (copies.TryGetValue(original, out tmpResult))
        return tmpResult;

    object result;
    if (!t.IsArray)
    {
        result = Activator.CreateInstance(t);
        copies.Add(original, result);

        // Maybe you need here some more BindingFlags
        foreach (var field in t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance))
        {
            var fieldValue = field.GetValue(original);
            field.SetValue(result, fieldValue.deepClone(field.FieldType, copies));
        }
    }
    else
    {
        // Handle arrays here
        var originalArray = (Array)original;
        var resultArray = (Array)originalArray.Clone();
        copies.Add(original, resultArray);

        var elementType = t.GetElementType();
        // If the type is not a value type we need to copy each of the elements
        if (!elementType.IsValueType)
        {
            var lengths = new int[t.GetArrayRank()];
            var indicies = new int[lengths.Length];
            // Get lengths from original array
            for (var i = 0; i < lengths.Length; i++)
                lengths[i] = resultArray.GetLength(i);

            var p = lengths.Length - 1;

            /* Now we need to iterate though each of the ranks
             * we need to keep it generic to support all array ranks */
            while (increment(indicies, lengths, p))
            {
                var value = resultArray.GetValue(indicies);
                if (value != null)
                    resultArray.SetValue(value.deepClone(elementType, copies), indicies);
            }
        }
        result = resultArray;
    }
    return result;
}

static bool increment(int[] indicies, int[] lengths, int p)
{
    if (p > -1)
    {
        indicies[p]++;
        if (indicies[p] < lengths[p])
            return true;

        if (increment(indicies, lengths, p - 1))
        {
            indicies[p] = 0;
            return true;
        }
    }
    return false;
}

【讨论】:

  • +1,简单又酷。进行了小的格式编辑(加上一些关键字一致性)
  • 代码也会有一些问题。如果您检查“t.IsValueType”并返回原始子对象,则不会复制!无论如何还有一些工作要做,你需要实现序列化的特殊构造函数(我做过一次,不好笑)。
  • 我是新手,不太清楚如何使用它。绝对需要某种形式的 deepClone。我是否只是将其作为方法添加到我希望能够克隆的类中?当我这样做时,会标记许多超出范围的样式错误。 TIA
【解决方案2】:

你也可以使用反射来创建对象的副本,这应该是最快的方式,因为序列化也使用反射。

这里有一些代码(已测试):

public static T DeepClone<T>(this T original, params Object[] args)
{
    return original.DeepClone(new Dictionary<Object, Object>(), args);
}

private static T DeepClone<T>(this T original, Dictionary<Object, Object> copies, params Object[] args)
{
    T result;
    Type t = original.GetType();

    Object tmpResult;
    // Check if the object already has been copied
    if (copies.TryGetValue(original, out tmpResult))
    {
        return (T)tmpResult;
    }
    else
    {
        if (!t.IsArray)
        {
            /* Create new instance, at this point you pass parameters to
                * the constructor if the constructor if there is no default constructor
                * or you change it to Activator.CreateInstance<T>() if there is always
                * a default constructor */
            result = (T)Activator.CreateInstance(t, args);
            copies.Add(original, result);

            // Maybe you need here some more BindingFlags
            foreach (FieldInfo field in t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance))
            {
                /* You can filter the fields here ( look for attributes and avoid
                    * unwanted fields ) */

                Object fieldValue = field.GetValue(original);

                // Check here if the instance should be cloned
                Type ft = field.FieldType;

                /* You can check here for ft.GetCustomAttributes(typeof(SerializableAttribute), false).Length != 0 to 
                    * avoid types which do not support serialization ( e.g. NetworkStreams ) */
                if (fieldValue != null && !ft.IsValueType && ft != typeof(String))
                {
                    fieldValue = fieldValue.DeepClone(copies);
                    /* Does not support parameters for subobjects nativly, but you can provide them when using
                        * a delegate to create the objects instead of the Activator. Delegates should not work here
                        * they need some more love */
                }

                field.SetValue(result, fieldValue);
            }
        }
        else
        {
            // Handle arrays here
            Array originalArray = (Array)(Object)original;
            Array resultArray = (Array)originalArray.Clone();
            copies.Add(original, resultArray);

            // If the type is not a value type we need to copy each of the elements
            if (!t.GetElementType().IsValueType)
            {
                Int32[] lengths = new Int32[t.GetArrayRank()];
                Int32[] indicies = new Int32[lengths.Length];
                // Get lengths from original array
                for (int i = 0; i < lengths.Length; i++)
                {
                    lengths[i] = resultArray.GetLength(i);
                }

                Int32 p = lengths.Length - 1;

                /* Now we need to iterate though each of the ranks
                    * we need to keep it generic to support all array ranks */
                while (Increment(indicies, lengths, p))
                {
                    Object value = resultArray.GetValue(indicies);
                    if (value != null)
                       resultArray.SetValue(value.DeepClone(copies), indicies);

                }
            }
            result = (T)(Object)resultArray;
        }
        return result;
    }
}

private static Boolean Increment(Int32[] indicies, Int32[] lengths, Int32 p)
{
    if (p > -1)
    {
        indicies[p]++;
        if (indicies[p] < lengths[p])
        {
            return true;
        }
        else
        {
            if (Increment(indicies, lengths, p - 1))
            {
                indicies[p] = 0;
                return true;
            }
            else
            {
                return false;
            }
        }
    }
    return false;
}

更新

添加了更多代码,现在您可以使用该方法复制复杂对象(甚至是多维数组)。请注意,委托仍未实现。

如果您想要一个完整的实现,您需要处理ISerializable 接口,这并不难,但需要一些时间来反映现有代码。为远程实现做了一次。

【讨论】:

  • 您可以尝试从我的示例中克隆CustomSerializableType 类的实例,看看是否全部克隆得很好,您也可以使用我提供的测试吗?我的假设 - 引用类型不会被克隆,只有引用而不是内存位置
  • 添加了更多代码并对其进行了测试,它可以正常工作并克隆所有引用类型。
  • @Felix K.:刚刚在代码Does not support parameters for subobjects (!!!) 中找到了您的评论。这是否意味着如果它封装了任何不提供默认构造函数的对象,它将无法克隆对象?
  • 没错,但据我所知,使用.NET的序列化也是不可能的。当然,您可以扩展上面的代码来处理非默认构造函数,但这可能需要大量工作。如果您想这样做,您可以使用自定义属性并提供有关创建过程的信息。您还可以为该方法提供一个委托,该委托处理没有任何默认构造函数的类型。
  • 什么是Increase(...)方法?
【解决方案3】:

MemberwiseClone 不是进行深度复制的好选择 (MSDN):

MemberwiseClone 方法通过创建一个新的 对象,然后将当前对象的非静态字段复制到 新对象。如果字段是值类型,则逐位复制 场进行。 如果字段是引用类型,则引用是 复制但引用的对象不是;因此,原 对象及其克隆引用同一个对象。

这意味着如果克隆对象具有引用类型的公共字段或属性,它们将引用与原始对象的字段/属性相同的内存位置,因此克隆对象的每次更改都将反映在初始对象中。这不是真正的深拷贝。

您可以使用 BinarySerialization 创建一个完全独立的对象实例,有关序列化示例,请参阅 BinaryFormatter class 的 MSDN 页面。


示例和测试工具:

创建给定对象的深层副本的扩展方法:

public static class MemoryUtils
{
    /// <summary>
    /// Creates a deep copy of a given object instance
    /// </summary>
    /// <typeparam name="TObject">Type of a given object</typeparam>
    /// <param name="instance">Object to be cloned</param>
    /// <param name="throwInCaseOfError">
    /// A value which indicating whether exception should be thrown in case of
    /// error whils clonin</param>
    /// <returns>Returns a deep copy of a given object</returns>
    /// <remarks>Uses BInarySerialization to create a true deep copy</remarks>
    public static TObject DeepCopy<TObject>(this TObject instance, bool throwInCaseOfError)
        where TObject : class
    {
        if (instance == null)
        {
            throw new ArgumentNullException("instance");
        }

        TObject clonedInstance = default(TObject);

        try
        {
            using (var stream = new MemoryStream())
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(stream, instance);

                // reset position to the beginning of the stream so
                // deserialize would be able to deserialize an object instance
                stream.Position = 0;

                clonedInstance = (TObject)binaryFormatter.Deserialize(stream);
            }
        }
        catch (Exception exception)
        {
            string errorMessage = String.Format(CultureInfo.CurrentCulture,
                            "Exception Type: {0}, Message: {1}{2}",
                            exception.GetType(),
                            exception.Message,
                            exception.InnerException == null ? String.Empty :
                            String.Format(CultureInfo.CurrentCulture,
                                        " InnerException Type: {0}, Message: {1}",
                                        exception.InnerException.GetType(),
                                        exception.InnerException.Message));
            Debug.WriteLine(errorMessage);

            if (throwInCaseOfError)
            {
                throw;
            }
        }

        return clonedInstance;
    }
}

NUnit 测试:

public class MemoryUtilsFixture
{
    [Test]
    public void DeepCopyThrowWhenCopyInstanceOfNonSerializableType()
    {
        var nonSerializableInstance = new CustomNonSerializableType();
        Assert.Throws<SerializationException>(() => nonSerializableInstance.DeepCopy(true));
    }

    [Test]
    public void DeepCopyThrowWhenPassedInNull()
    {
        object instance = null;
        Assert.Throws<ArgumentNullException>(() => instance.DeepCopy(true));
    }

    [Test]
    public void DeepCopyThrowWhenCopyInstanceOfNonSerializableTypeAndErrorsDisabled()
    {
        var nonSerializableInstance = new CustomNonSerializableType();            
        object result = null;

        Assert.DoesNotThrow(() => result = nonSerializableInstance.DeepCopy(false));
        Assert.IsNull(result);
    }

    [Test]
    public void DeepCopyShouldCreateExactAndIndependentCopyOfAnObject()
    {
        var instance = new CustomSerializableType
                        {
                            DateTimeValueType =
                                DateTime.Now.AddDays(1).AddMilliseconds(123).AddTicks(123),
                            NumericValueType = 777,
                            StringValueType = Guid.NewGuid().ToString(),
                            ReferenceType =
                                new CustomSerializableType
                                    {
                                        DateTimeValueType = DateTime.Now,
                                        StringValueType = Guid.NewGuid().ToString()
                                    }
                        };

        var deepCopy = instance.DeepCopy(true);

        Assert.IsNotNull(deepCopy);
        Assert.IsFalse(ReferenceEquals(instance, deepCopy));
        Assert.That(instance.NumericValueType == deepCopy.NumericValueType);
        Assert.That(instance.DateTimeValueType == deepCopy.DateTimeValueType);
        Assert.That(instance.StringValueType == deepCopy.StringValueType);
        Assert.IsNotNull(deepCopy.ReferenceType);
        Assert.IsFalse(ReferenceEquals(instance.ReferenceType, deepCopy.ReferenceType));
        Assert.That(instance.ReferenceType.DateTimeValueType == deepCopy.ReferenceType.DateTimeValueType);
        Assert.That(instance.ReferenceType.StringValueType == deepCopy.ReferenceType.StringValueType);
    }

    [Serializable]
    internal sealed class CustomSerializableType
    {            
        public int NumericValueType { get; set; }
        public string StringValueType { get; set; }
        public DateTime DateTimeValueType { get; set; }

        public CustomSerializableType ReferenceType { get; set; }
    }

    public sealed class CustomNonSerializableType
    {            
    }
}

【讨论】:

  • 我认为这不是一个好的解决方案,您将流中的所有内容序列化并在之后反序列化。使用反射并创建对象的副本要好得多。
  • @Felix K.:您可以将您的解决方案添加为新答案,我们可以看看
  • @sll 具有数组类型属性的测试也很合适。没关系,我会自己做
猜你喜欢
  • 2013-10-11
  • 1970-01-01
  • 2013-04-28
  • 1970-01-01
  • 2017-09-17
  • 2011-09-05
  • 2017-11-03
  • 2011-08-14
相关资源
最近更新 更多