Is there a much better way to create deep and shallow clones in C#?

牧云@^-^@ 提交于 2019-11-26 10:28:52

问题


I have been creating object for a project and there are some instances that I have to create a deep copy for this objects I have come up with the use of a built in function for C# which is MemberwiseClone(). The problem that bothers me is whenever there is a new class that i created , I would have to write a function like the code below for a shallow copy..Can someone please help me improve this part and give me a shallow copy that is better than the second line of code. thanks :)

SHALLOW COPY:

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

DEEP COPY:

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);
    }
}

回答1:


MemberwiseClone is not a good choice to do a Deep Copy (MSDN):

The MemberwiseClone method creates a shallow copy by creating a new object, and then copying the nonstatic fields of the current object to the new object. If a field is a value type, a bit-by-bit copy of the field is performed. If a field is a reference type, the reference is copied but the referred object is not; therefore, the original object and its clone refer to the same object.

This mean if cloned object has reference type public fields or properties they would reffer to the same memory location as the original object's fields/properties, so each change in the cloned object will be reflected in the initial object. This is not a true deep copy.

You can use BinarySerialization to create a completely independent instance of the object, see MSDN Page of the BinaryFormatter class for an serialization example.


Example and Test Harness:

Extension method to create a deep copy of a given object:

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 tests:

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
    {            
    }
}



回答2:


You can also use reflection to create a copy of the object, this should be the fastest way, because the serialization uses the reflection too.

Here some code ( tested ):

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;
}

Update

Added some more code, now you can use the method to copy complex objects ( even arrays with multiple dimensions ). Note that delegates are still not implemented.

If you want a complete implementation you need to handle the ISerializable interface which is not really hard but takes some time to reflect over the existing code. Did this once for a remoting implementation.




回答3:


The solution using serialization, as suggested by sll, is by-far the simplest but doesn't work if the type you are trying to clone is not serializable.

The code from Felix K. is a good alternative but I found a few issues with it. Here is a revised version that fixes some of the problems I found. I also removed some of the functionality I didn't need (eg. constructor parameters).

/// <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;
}


来源:https://stackoverflow.com/questions/8025890/is-there-a-much-better-way-to-create-deep-and-shallow-clones-in-c

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!