還記得小時候,我總覺得「鏡中的自己」很神奇。他會跟著我一起笑、一起生氣、一起模仿我的每個動作。但漸漸長大才明白——那並不是真正的「另一個我」,而只是一個淺薄的影子。

程式世界裡,物件的複製也是這樣。有時候,我們只是得到了「鏡子裡的自己」(Shallow Copy),稍不注意,動到這邊,另一邊也跟著被牽連。而有時候,我們會想要的是一個「能獨立走出鏡子」的自己(Deep Copy),擁有完全不同的記憶體、不同的命運。

🌺 Shallow Copy & Deep Copy

source : algodaily.com

  • 左圖:兩個物件其實共用相同的記憶體位址,所以改一個,另一個也會受影響。
  • 右圖:透過 Deep Copy,物件被完整複製,擁有獨立的記憶體,不會互相干涉。


🌺 序列化進行 Deep Copy

因為 反序列化一定會產生全新的物件,所以天然就避免了 Shallow Copy 的共享問題。

不須理會屬性的異動


方法簡單

只要一個「擴充方法」就能對任何型別用,不用每個 class 寫 Clone()。這比 AutoMapper 少了一些設定,入門更快。


只能複製相同型別

DeepCloneByJson() 就只能還原成 Person,不能變成 PersonDto。這不像 AutoMapper 可以 Person -> PersonDto。


Private 欄位無法複製

JSON 序列化只會處理「公開 (public) 的屬性」,私有欄位 (private) 不會被帶過去。
所以如果物件內部有一些「隱藏的狀態」,Clone 出來的會失真。


循環參考會出錯

如果物件 A 裡面有 B,B 又有 A(互相參考),JSON.NET 會無限遞迴,造成 StackOverflow 或例外。

1
2
3
4
5
6
7
8
9
class Node {
public Node Next { get; set; }
}
var node1 = new Node();
var node2 = new Node();
node1.Next = node2;
node2.Next = node1; // 循環參考
var clone = node1.DeepCloneByJson(); // ❌ 出錯

循環參考錯誤解法


實作擴充方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class CommonExtensions
{
/// <summary>
/// 深層複製
/// </summary>
/// <typeparam name="T">複製對象類別</typeparam>
/// <param name="source">複製對象
/// <returns>複製出的物件</returns>
public static T DeepCloneByJson<T>(this T source)
{
if (Object.ReferenceEquals(source, null))
{
return default(T);
}

//// 確保反序列化時完全使用 JSON 中的資料,而不會與物件原本的預設值混合
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source), deserializeSettings);
}
}


🌺 Automapper 進行 Deep Copy

不須理會屬性的異動

AutoMapper 在設定 cfg.CreateMap<TSource, TDestination>() 時,只要兩個型別屬性名稱、型別相同,它就會自動幫你把值填進去,不需要你手動一個個設定。


可複製不同的型別內容

AutoMapper 不只限於相同型別,它也能幫你做「不同型別的對映」,只要你告訴它怎麼對應。
這讓它非常靈活,比如你可能有個 ViewModel 或 Dto,要對應到 Entity,這時候用 AutoMapper 就超級好用。

1
2
cfg.CreateMap<PersonDto, Person>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.FullName));

彈性配置對應方式

AutoMapper 它不是死板的,它可以讓你針對某些屬性,自訂對應邏輯,比如轉換格式、改名字、忽略欄位、加入條件等等。

1
2
3
cfg.CreateMap<Person, Person>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name.ToUpper()))
.ForMember(dest => dest.Address, opt => opt.Ignore());

DeepClone 實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static void AutoMappingDeepClone(IServiceProvider services)
{
var source = new Entitys.Person();
source.Address = "hisn";
source.Age = 18;
source.Name = "YL";
source.Pets = new List<Entitys.Pet>
{
new Entitys.Pet { Name = "Party"},
new Entitys.Pet { Name = "Mochi"}
};

var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Entitys.Person, Entitys.Person>();
cfg.CreateMap<Entitys.Pet, Entitys.Pet>();
}, loggerFactory);

var mapper = config.CreateMapper();
var target = new Entitys.Person();
mapper.Map(source, target);
target.Age = 20;
target.Address = "chaung";
target.Name = "pan";

System.Console.WriteLine($"Source: {source.Name}, {source.Age}, {source.Address}");
System.Console.WriteLine($"target: {target.Name}, {target.Age}, {target.Address}");
}


🌺 結語

回想一開始的故事,鏡中的自己只是一個「影子」。Shallow Copy 就像那個影子,你以為獨立,卻其實共用同一份靈魂。Deep Copy 則像是從鏡子裡真正走出來的「另一個自己」,
能走上屬於自己的路,擁有獨立的選擇。程式設計裡,選擇何時該用「影子」、何時該創造「新生命」,正如人生中,我們也常在模仿與創造之間,尋找真正的自我。