Understanding Deep Copy and Shallow Copy in C#
Copy Semantics Overview
Object copying is a fundamental concept in C# that frequently appears in technical discussions and interviews. All .NET types inherit from System.Object, which provides a protected method MemberwiseClone() that performs what is known as a shallow copy.
Shallow Copy
A shallow copy creates a new object, then copies all non-static value type fields and all reference type field references to the new object. The key characteristic is that reference type member are shared between the original and copied objects—they point to the same underlying objects in memory.
Consider a simple example with a reference type:
public class Dog
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
Dog original = new Dog { Name = "Max" };
Dog shallow = original; // Shallow copy via reference assignment
shallow.Name = "Buddy";
Console.WriteLine($"Original: {original.Name}, Shallow: {shallow.Name}");
// Output: Both show "Buddy" because they reference the same object
}
}
With a reference type member, modifying the shared reference affects all references:
public class Pet
{
public string Nickname { get; set; }
}
public class Owner
{
public Pet Animal { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
Owner ownerA = new Owner
{
Age = 30,
Animal = new Pet { Nickname = "Rex" }
};
Owner ownerB = (Owner)ownerA.MemberwiseClone();
ownerB.Animal.Nickname = "Spot";
Console.WriteLine(ownerA.Animal.Nickname); // Prints "Spot"
}
}
In this scenario, ownerB.Animal points to the same Pet instance as ownerA.Animal. Changing the nickname through either owner affects the shared object.
Deep Copy
A deep copy duplicates not only the value types but also creates entirely new copies of all reference type members. After a deep copy, the original and copy are completely independent—modifications to one do not affect the other.
The ICloneable interface provides a standard pattern for implementing copy functionality, though it can be implemented as either shallow or deep copy depending on requirements:
public interface ICloneable
{
object Clone();
}
public class Person : ICloneable
{
public string Name { get; set; }
public Address HomeAddress { get; set; }
public int Score { get; set; }
public object Clone()
{
Person copy = new Person();
copy.Name = Name;
copy.Score = Score;
copy.HomeAddress = new Address
{
Street = HomeAddress.Street,
City = HomeAddress.City
};
return copy;
}
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
class Program
{
static void Main()
{
Person original = new Person
{
Name = "Alice",
Score = 100,
HomeAddress = new Address { Street = "123 Main St", City = "Seattle" }
};
Person cloned = (Person)original.Clone();
cloned.HomeAddress.City = "Portland";
Console.WriteLine(original.HomeAddress.City); // Still "Seattle"
}
}
Key Differences Illustrated
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| Value Types | Duplicated | Duplicated |
| Reference Types | Shared reference | New object created |
| Independence | Dependent | Independent |
Implementation Approaches
Manual Property Assignment
The most straightforward approach—create a new instance and assign each property individually:
public class Item
{
public string Label { get; set; }
public decimal Price { get; set; }
public CategoryInfo Category { get; set; }
}
public class CategoryInfo
{
public string Name { get; set; }
}
public static Item Duplicate(Item source)
{
return new Item
{
Label = source.Label,
Price = source.Price,
Category = new CategoryInfo { Name = source.Category.Name }
};
}
Reflection-Based Cloning
Dynamic cloning using reflection works for most scenarios:
using System.Reflection;
public static T CloneViaReflection<T>(T source) where T : new()
{
T result = new T();
Type type = typeof(T);
foreach (PropertyInfo prop in type.GetProperties())
{
if (prop.CanWrite)
{
object value = prop.GetValue(source);
prop.SetValue(result, value);
}
}
return result;
}
Note: This approach copies reference types by reference, which may not achieve true deep copying for nested reference types.
Serialization Techniques
JSON Serialization
Using Newtonsoft.Json (or System.Text.Json):
using Newtonsoft.Json;
public static T DeepClone<T>(T obj)
{
string json = JsonConvert.SerializeObject(obj);
return JsonConvert.DeserializeObject<T>(json);
}
Binary Serialization
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public static T DeepClone<T>(T obj)
{
using (MemoryStream stream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, obj);
stream.Position = 0;
return (T)formatter.Deserialize(stream);
}
}
XML Serialization
using System.Xml.Serialization;
public static T DeepClone<T>(T obj)
{
using (MemoryStream stream = new MemoryStream())
{
XmlSerializer serializer = new XmlSerializer(typeof(T));
serializer.Serialize(stream, obj);
stream.Position = 0;
return (T)serializer.Deserialize(stream);
}
}
Design Considerations
Inheritence concerns: Implementing ICloneable on a base class that will be inherited creates an obligation for all derived types to also implement the interface properly. Failing to do so means inherited reference type members won't be deeply copied by subclasses.
Performance: Deep copying via serialization has overhead. For frequently copied objects with complex graphs, manual copying or specialized copy constructors may offer better performence.
Security: When using serialization for cloning, ensure the type is marked [Serializable] and consider whether custom serialization logic is needed to handle sensitive fields appropriately.