引子:印簡歷如同印草紙#
小菜為明天的供需見面會印了一大疊履歷,連小學得獎都寫上去了,還寫「精通 C#、Java、SQL Server、Oracle」。大鳥吐槽:
- 履歷印得像草紙,發履歷像發廣告
- 招聘方收到太多反而不重視
- 「精通」要寫得真誠,過度浮誇反讓人覺得不誠實
大鳥早年手寫履歷,公司也認真看;現在程式員寫履歷都知道複印——但寫程式時卻不一定懂得「複製」的真正用法。
第一版:每份履歷都 new 一次#
class Resume
{
private string name, sex, age, timeArea, company;
public Resume(string name) { this.name = name; }
public void SetPersonalInfo(string sex, string age) { ... }
public void SetWorkExperience(string timeArea, string company) { ... }
public void Display() { ... }
}客戶端:
Resume a = new Resume("大鳥");
a.SetPersonalInfo("男", "29");
a.SetWorkExperience("1998-2000", "XX 公司");
Resume b = new Resume("大鳥");
b.SetPersonalInfo("男", "29");
b.SetWorkExperience("1998-2000", "XX 公司");
Resume c = new Resume("大鳥");
// ... 二十份履歷就要重複二十次不僅冗長,而且若需把
1998改成1999,要改二十處——任一遺漏就出錯。
直接賦值行不行?#
Resume b = a;
Resume c = a;這是傳引用,等於 b、c 兩張紙上都寫著「履歷在 a 處」,沒有實際內容。
原型模式#
原型模式(Prototype Pattern):用原型實例指定創建物件的種類,並且透過拷貝這些原型創建新的物件。[DP]
結構#
- Prototype:原型類,宣告一個克隆自身的介面
- ConcretePrototype:具體原型類,實作克隆自身的操作
classDiagram
class Prototype {
<<abstract>>
-string id
+Clone() Prototype
}
class ConcretePrototype1 {
+Clone() Prototype
}
class ConcretePrototype2 {
+Clone() Prototype
}
class Client
Prototype <|-- ConcretePrototype1
Prototype <|-- ConcretePrototype2
Client ..> Prototypeabstract class Prototype
{
private string id;
public Prototype(string id) { this.id = id; }
public string Id => id;
public abstract Prototype Clone();
}
class ConcretePrototype1 : Prototype
{
public ConcretePrototype1(string id) : base(id) { }
public override Prototype Clone() => (Prototype)this.MemberwiseClone();
}.NET 在
System命名空間提供了ICloneable介面,唯一方法Clone()。實作這個介面就能直接套用原型模式。
第二版:用 ICloneable 改寫履歷#
class Resume : ICloneable
{
private string name, sex, age, timeArea, company;
public Resume(string name) { this.name = name; }
public void SetPersonalInfo(string sex, string age) { ... }
public void SetWorkExperience(string timeArea, string company) { ... }
public void Display() { ... }
public object Clone() => (object)this.MemberwiseClone();
}客戶端:
Resume a = new Resume("大鳥");
a.SetPersonalInfo("男", "29");
a.SetWorkExperience("1998-2000", "XX 公司");
Resume b = (Resume)a.Clone();
b.SetWorkExperience("1998-2006", "YY 企業");
Resume c = (Resume)a.Clone();
c.SetPersonalInfo("男", "24");原型模式不只是讓客戶端清爽。每次
new都要執行構造函式,若初始化耗時,多次new效率極低。複製克隆是最好的辦法:既隱藏了物件創建細節,又動態獲得物件運行時狀態。
淺複製 vs. 深複製#
MemberwiseClone() 的行為:
- 值類型:逐位複製
- 引用類型:複製引用,但不複製引用的物件——原始物件與複本指向同一物件
範例:履歷引用「工作經歷」物件#
把 WorkExperience 提取為獨立類別後:
class WorkExperience
{
public string WorkDate { get; set; }
public string Company { get; set; }
}
class Resume : ICloneable
{
private WorkExperience work;
public Resume(string name)
{
this.name = name;
work = new WorkExperience();
}
// ...
public object Clone() => (object)this.MemberwiseClone();
}對 a Clone 兩次後分別 SetWorkExperience,三份履歷的工作經歷都變成最後一次設定的值——因為三個引用都指向同一個 WorkExperience 物件。
這就是淺複製(Shallow Copy):被複製物件的所有變數都含有相同值,但所有對其他物件的引用仍指向原來的物件。
深複製(Deep Copy)#
深複製:把要複製物件所引用的物件也複製一遍——指針指向新複製的物件,而非原物件。
實作方式:讓 WorkExperience 也實作 ICloneable,並在 Resume 中提供私有建構子處理深複製:
class WorkExperience : ICloneable
{
public string WorkDate { get; set; }
public string Company { get; set; }
public object Clone() => (object)this.MemberwiseClone();
}
class Resume : ICloneable
{
private WorkExperience work;
public Resume(string name)
{
this.name = name;
work = new WorkExperience();
}
private Resume(WorkExperience work)
{
this.work = (WorkExperience)work.Clone();
}
public object Clone()
{
Resume obj = new Resume(this.work);
obj.name = this.name;
obj.sex = this.sex;
obj.age = this.age;
return obj;
}
}若引用層層巢狀(履歷 → 工作經歷 → 公司 → 職位 → ……),深複製要深入到第幾層需事先決定。
還要當心循環引用問題,需要小心處理。
.NET 中的對應實作#
DataSet物件提供了兩個方法:
Clone():複製結構,但不複製資料 → 對應淺複製Copy():複製結構也複製資料 → 對應深複製
本章小結#
- 原型模式從一個物件再創建另一個可訂製的物件,且不需知道任何創建細節
- 隱藏了創建細節、提升效能(避免重複初始化)
- 動態獲得物件運行時的狀態
- 但要注意淺複製與深複製的差異——對引用類型的成員,淺複製只複製引用
大鳥的吐槽:寫程式時用原型模式簡化程式碼是好事,但找工作時與其複印履歷,不如針對職缺寫一封手寫求職信——稀缺的東西才會被重視。