原型模式 是創建型設計模式之一,核心思想:**以原型實例為基礎,透過複製 (克隆) 方式建立新物件,而非透過類別實例化**。適用於物件建立成本高、構造函數複雜、或需動態增減產品類別的場景。C# 透過 `ICloneable` 介面與 `MemberwiseClone()` 方法提供原生支援,關鍵在於正確理解 **淺拷貝** 與 **深拷貝** 的差異。
using System;
// 抽象原型
public abstract class Prototype : ICloneable
{
protected string Id { get; }
protected Prototype(string id) => Id = id;
public abstract object Clone(); // 回傳 object 符合 ICloneable
}
// 具體原型 1
public class ConcretePrototype1 : Prototype
{
public ConcretePrototype1(string id) : base(id) { }
public override object Clone()
{
// MemberwiseClone() 執行淺拷貝:複製值型別、複製參考型別的「參考」
return this.MemberwiseClone();
}
}
// 具體原型 2
public class ConcretePrototype2 : Prototype
{
private readonly List _tags = new(); // 參考型別欄位
public ConcretePrototype2(string id) : base(id) { }
public void AddTag(string tag) => _tags.Add(tag);
public IReadOnlyList Tags => _tags.AsReadOnly();
public override object Clone()
{
// ⚠️ 淺拷貝:_tags 列表參考被複製,原物件與複製物件共享同一個列表!
return this.MemberwiseClone();
}
}
// 客戶端測試淺拷貝行為
var p1 = new ConcretePrototype1("I");
var c1 = (ConcretePrototype1)p1.Clone();
Console.WriteLine($"Cloned: {c1.Id}"); // 輸出: Cloned: I
var p2 = new ConcretePrototype2("II");
p2.AddTag("tag1");
p2.AddTag("tag2");
var c2 = (ConcretePrototype2)p2.Clone();
// 修改複製物件的標籤
c2.AddTag("tag3");
Console.WriteLine($"Original tags: {string.Join(", ", p2.Tags)}"); // 輸出: tag1, tag2, tag3 (受影響!)
Console.WriteLine($"Cloned tags: {string.Join(", ", c2.Tags)}"); // 輸出: tag1, tag2, tag3 (同一個列表)
public class DeepCopyPrototype : ICloneable
{
public int[] Numbers { get; set; } = { 1, 2, 3 };
public List Tags { get; set; } = new();
public Address Address { get; set; } // 巢狀參考型別
public object Clone()
{
var clone = (DeepCopyPrototype)this.MemberwiseClone(); // 先淺拷貝
// 手動深拷貝每個參考型別欄位
clone.Numbers = (int[])Numbers.Clone(); // 陣列支援 Clone()
clone.Tags = new List(Tags); // List 複製建構子
clone.Address = Address?.Clone() as Address; // 遞迴深拷貝巢狀物件
return clone;
}
}
public class Address : ICloneable
{
public string City { get; set; }
public string Street { get; set; }
public object Clone() => this.MemberwiseClone(); // 純值型別/字串(不可變)淺拷貝即深拷貝
}
// 測試
var original = new DeepCopyPrototype
{
Numbers = new[] { 1, 2, 3 },
Tags = new List { "A", "B" },
Address = new Address { City = "Taipei", Street = "Zhongxiao Rd" }
};
var cloned = (DeepCopyPrototype)original.Clone();
cloned.Numbers[0] = 99;
cloned.Tags.Add("C");
cloned.Address.City = "Kaohsiung";
Console.WriteLine($"Original: {string.Join(",", original.Numbers)}, {string.Join(",", original.Tags)}, {original.Address.City}");
// 輸出: 1,2,3, A,B, Taipei (未受影響)
Console.WriteLine($"Cloned: {string.Join(",", cloned.Numbers)}, {string.Join(",", cloned.Tags)}, {cloned.Address.City}");
// 輸出: 99,2,3, A,B,C, Kaohsiung
using System.Text.Json;
public class SerializablePrototype : ICloneable
{
public int[] Numbers { get; set; }
public List Tags { get; set; }
public Address Address { get; set; }
public object Clone()
{
// 使用 JSON 序列化深拷貝
string json = JsonSerializer.Serialize(this);
return JsonSerializer.Deserialize(json);
}
}
// 注意:需要無參數建構子、屬性可寫入、循環參考會拋例外
// 可改用 BinaryFormatter (舊版)、protobuf-net、MessagePack 等二進位格式提升效能
using System.Linq.Expressions;
public static class DeepCloner
{
private static readonly Dictionary> _cloners = new();
public static T DeepClone(T source) where T : class
{
if (source == null) return null;
var type = typeof(T);
if (!_cloners.TryGetValue(type, out var cloner))
{
cloner = CompileCloner(type);
_cloners[type] = cloner;
}
return (T)cloner(source);
}
private static Func
// 顏色原型
public abstract class ColorPrototype : ICloneable
{
public abstract object Clone();
}
public class Color : ColorPrototype
{
public int Red { get; }
public int Green { get; }
public int Blue { get; }
public Color(int r, int g, int b) => (Red, Green, Blue) = (r, g, b);
public override object Clone() => this.MemberwiseClone(); // 值型別欄位,淺即深
public void Display() => Console.WriteLine($"RGB: {Red}, {Green}, {Blue}");
}
// 原型管理器
public class ColorManager
{
private readonly Dictionary _prototypes = new();
public ColorPrototype this[string name]
{
get => _prototypes.TryGetValue(name, out var p) ? p : null;
set => _prototypes[name] = value;
}
}
// 應用:使用者自訂顏色主題
var manager = new ColorManager();
// 系統預設色
manager["Red"] = new Color(255, 0, 0);
manager["Green"] = new Color(0, 255, 0);
manager["Blue"] = new Color(0, 0, 255);
// 使用者自訂色 (克隆自預設色再微調)
manager["Angry"] = new Color(255, 54, 0);
manager["Peace"] = new Color(128, 211, 128);
manager["Flame"] = new Color(211, 34, 20);
// 客戶端使用:克隆後可安全修改
Color c1 = (Color)manager["Red"].Clone();
c1.Display(); // RGB: 255, 0, 0
Color c2 = (Color)manager["Peace"].Clone();
c2.Display(); // RGB: 128, 211, 128
| 特性 | 淺拷貝 | 深拷貝 |
|---|---|---|
| 實作方式 | MemberwiseClone() | 手動複製 / 序列化 / Expression |
| 值型別欄位 | 複製數值 | 複製數值 |
| 字串欄位 | 複製參考 (安全,字串不可變) | 複製參考 (安全) |
| 陣列/集合欄位 | 複製參考 (共享同一集合) | 建立新集合並複製元素 |
| 巢狀物件欄位 | 複製參考 (共享同一物件) | 遞迴深拷貝巢狀物件 |
| 修改複製物件影響原物件 | 是 (參考型別欄位) | 否 (完全獨立) |
| 效能 | 極快 (單指令) | 較慢 (配置新記憶體、複製資料) |
| 適用場景 | 全值型別、不可變參考型別、刻意共享 | 可變參考型別、需要完全隔離 |
Microsoft 官方文件建議:**公開 API 避免使用 `ICloneable`**,因為它不強制淺/深拷貝語意,導致呼叫端無法預期行為。
// 方案 1:明確命名的複製方法
public class Document
{
public Document DeepClone() { ... } // 語意清晰
public Document ShallowClone() { ... }
}
// 方案 2:複製建構子 (Copy Constructor)
public class Document
{
public Document(Document other) // 明確為深拷貝
{
// 複製邏輯
}
}
// 方案 3:Record 型別 (C# 9+) 自動提供 with 運算式
public record Document(string Title, List Tags)
{
// 編譯器自動產生複製建構子
// 使用: var copy = original with { Title = "New Title" }; // 淺拷貝 Tags
// 深拷貝需自行實作: var copy = original with { Tags = new List(original.Tags) };
}
考慮使用原型模式當:
| 場景 | 實作 | 說明 |
|---|---|---|
| 物件複製 | ICloneable, MemberwiseClone | 基礎支援 |
| 資料傳輸物件 | Record with 運算式 | C# 9+ 語法糖,淺拷貝 |
| Entity Framework | Entity.Clone() | 實體脫離追蹤建立新實體 |
| WPF Freezable | Clone(), CloneCurrentValue() | 凍結物件的深拷貝 |
| ASP.NET Core Options | IOptionsSnapshot | 每請求複製選項實例 |
原型模式透過「複製」替代「建構」,在特定場景下極具優勢。C# 開發者必須精通:
掌握這些核心概念,能在需要動態物件建立、狀態快照、撤銷重做、物件池化等場景中靈活運用原型模式。