Return to the Deep (Copy)


In a previous post, I mentioned the solution I use for performing deep copies in XNA. Recently, while fixing a bug in a recent project, I made a minor tweak to the deep copy function that made a major difference to its behavior.

The bug I ran into involved some objects mimicking the animations of other objects in the scene. The objects were sharing animation instances when they weren’t supposed to. The root of this problem had to do with the deep copy’s handling of C# value types, namely structs.

            if (type.IsValueType|| type == typeof(string))
            {
                return obj;
            }

Value types are inherently copied as you pass them around as parameters which makes the above implementation seem correct at first glance. While it’s ostensibly the correct implementation of a copy for the struct itself, it doesn’t take into account the proper handling of objects referenced by the struct (which may be by-reference objects that would need to be deep copied.) This means that the deep copy function would perform a deep copy of an object hierarchy until it ran into a struct, after which it would become a shallow copy.

Thankfully, the solution for the above problem was relatively simple. I changed the previous value type handling to limit it to primitive types (int, float, double, etc.) and strings. I then allowed value types to be handled by the recursive copy handling used by classes.

With that change (and a few unit tests to assert behavior) implemented, I was back in business. No longer would classes referenced by structs turn into shallow copies causing some of my game objects to inappropriately mimic each others’ animations.

The updated deep copy code is below:

        static T DeepCopy<T>(T obj)
        {
            if (obj == null)
                throw new ArgumentNullException("Object cannot be null");
            return (T)Process(obj);
        }

        static object Process(object obj)
        {
            if (obj == null)
                return null;

            Type type = obj.GetType();
            if (type.IsPrimitive || type == typeof(string))
            {
                return obj;
            }
            else if (type.IsArray)
            {
                Type elementType = Type.GetType(
                     type.FullName.Replace("[]", string.Empty));
                var array = obj as Array;
                Array copied = Array.CreateInstance(elementType, array.Length);
                for (int i = 0; i < array.Length; i++)
                {
                    copied.SetValue(Process(array.GetValue(i)), i);
                }
                return Convert.ChangeType(copied, obj.GetType(), null);
            }
            else if (type.IsClass || type.IsValueType)
            {
                object toret = Activator.CreateInstance(obj.GetType());
                
                FieldInfo[] fields = type.GetFields(BindingFlags.Public |
                            BindingFlags.NonPublic | BindingFlags.Instance);
                foreach (FieldInfo field in fields)
                {
                    object fieldValue = field.GetValue(obj);
                    if (fieldValue == null)
                        continue;
                    field.SetValue(toret, Process(fieldValue));
                }
                return toret;
            }
            else
                throw new ArgumentException("Unknown type");
        }
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print