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

后端 未结 3 2033
借酒劲吻你
借酒劲吻你 2020-11-29 23:38

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

相关标签:
3条回答
  • 2020-11-29 23:56

    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
        {            
        }
    }
    
    0 讨论(0)
  • 2020-11-29 23:58

    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.

    0 讨论(0)
  • 2020-11-29 23:58

    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;
    }
    
    0 讨论(0)
提交回复
热议问题