友元是 C++ 中的概念,包含友元函數和友元類。被某個類聲明爲友元的函數或類能夠訪問這個類的私有成員。友元的正確使用能提升程序的運行效率,但同時也破壞了類的封裝性和數據的隱藏性,致使程序可維護性變差。所以,除了 C++ 外很難再看到友元語法特性。函數
可是友元並不是一無可取,在某些時候確實有這樣的需求。舉例來講,如今咱們須要定義一個 User
類,爲了不 User
對象在使用過程當中屬性被修改,須要將它設計成 Immutable 的。到目前爲止尚未什麼問題,但接下來問題來了——因爲用戶信息較多,其屬性設計有十數個,爲了 Immutable 所有經過構造方法的參數來設置屬性是件讓人悲傷的事情。ui
那麼通常咱們會想到這樣幾個方案:this
這是 JavaScript 中經常使用的作法,使用參數對象,在構造 User
的時候,經過參數對象提供全部設置好的屬性,再由 User
的構造方法從參數裏把這些屬性拷貝出來設置給只讀成員。那麼實現可能像這樣:設計
爲了簡化代碼,只定義了
Id
、Username
和Name
三個屬性。下同。指針
public sealed class User { public ulong Id { get; } public string Username { get; } public string Name { get; } public User(Properties props) { Id = props.Id; Username = props.Username; Name = props.Name; } public sealed class Properties { public ulong Id; public string Username; public string Name; } }
一個屬性就須要重複寫三遍,若是代碼是按行付費,這個定義會很是賺!code
這種作法是自定義屬性的 set
函數,或者定義一個 SetXxxxx
方法,判斷若是值爲 null
則能夠設置,一但設置將不能再設置(理論上來講應該拋異常,但這裏示例簡化爲無做爲)。對象
下面的示例經過 Username
和 Name
演示了一次性設置的兩種方法ip
public class User { public ulong Id { get; } public string Username { get; private set; } public void SetUsername(string username) { if (Username == null) { Username = username; } } public string Name { get { return name; } set { if (name == null) { name = value; } } } private string name; public User(ulong id) { Id = id; } }
這種方法中的 User
並不是 Immutalbe,只是近似,由於它的屬性不能從「有」到「無」,卻能夠從「無」到「有」。get
並且,我發現這個方法比上一個方法更賺錢。string
Builder 模式嘛,就是爲了解決初始化複雜對象問題的。
public class User { public ulong Id { get; } public string Username { get; internal set; } public string Name { get; internal set; } public User(ulong id) { Id = id; } } public class UserBuilder { private readonly User user; public UserBuilder(ulong id) { user = new User(id); } public UserBuilder SetUsername(string username) { user.Username = username; } public UserBuilder SetName(string name) { user.Name = name; } public User Build() { // 驗證 user 的屬性 // 或者對某個屬性進行一些後期加工(好比計算,格式化處理……) return user; } }
爲了不外部訪問,User
的各屬性(除 Id
)的 setter
都聲明爲 internal
的,由於只有這樣 UserBuilder
才能調用它們的 setter
。
顯然,採用這種方式在同一個 Assembly 中,好比 App Assembly 中,User
的屬性仍然未能獲得保護。
基於上面 Builder 模式的解決方案,很容易想到,若是把 UserBuilder
定義爲 User
的內部類(嵌套類),那它直接就能夠訪問 User
的私有成員,其形式以下
public class User { // .... public class UserBuilder { // .... } }
這其實和 C++ 的友元類語法仍是有類似之處——就是都須要在 User
內部去聲明,C++ 是聲明友元,C# 則在聲明的同時進行了定義
// C++ 代碼 class UserBuilder; class User { friend class UserBuilder; } class UserBuilder { // .... }
結構上沒有問題了。再利用 C# 的分部類(partial class
) 特性將 User
類和 UserBuilder
類分別寫在兩個源文件中,而後簡化一下 UserBuilder
的名稱,簡化爲 Builder
,由於它定義在 User
的內部,語義已經很是明確了。
// User.cs public sealed partial class User { ulong Id { get; } public string Username { get; private set; } public string Name { get; private set; } public User(ulong id) { Id = id; } public static Builder CreateBuilder(ulong id) { return new Builder(id); } }
// User.Builder.cs partial sealed class User { public class Builder { private readonly User user; public Builder(ulong id) { user = new User(id); } public Builder SetUsername(string username) { user.Username = username; return this; } public Builder SetName(string name) { user.Name = name; return this; } public User Build() { // 驗證和後期加工 return user; } } }
上面這段代碼就達到了 Immutable User
的目的,同時代碼還很優雅,經過分部類拆分源文件,代碼結構也很清晰。不過還有一點小小的瑕疵……Build()
能夠重複調用,並且在調用以後仍然能夠修改 user
的屬性。
若是想把 Build()
變成可屢次調用,每次調用生成新的 User
對象,同時生成的 User
對象不受以後 Builder
的 SetXxxx
影響,能夠在 Build()
的時候,產生一個 user
的複本返回。
另外,因爲每一個 User
對象的 Id
應該不一樣,因此由生成 CreateBuilder
的時候指定改成 Build()
的時候指定:
public partial class User { // .... public static Builder CreateBuilder()) { return new Builder(); } } partial class User { public class Builder { private readonly User user; public Builder() { user = new User(0); } // .... public User Build(ulong id) { var inst = new User(id); inst.Username = user.Username; inst.Name = user.Name; return inst; } } }
其實這裏 Builder
內部的 user
被看成參數對象使用了。
一次性 Builder 相對簡單一些,不須要在 Build()
的時候去拷貝屬性。
partial class User { public class Builder { private User user; // 這裏 user 再也不是 readonly 的 public Builder(ulong id) { user = new User(id); } // .... public User Build() { if (user == null) { throw new InvalidOperationException("Build 只能調用一次") } // 驗證和後期加工 var inst = user; user = null; // 將 user 置 null return inst; } } }
一次性 Builder
在 Build()
以後將 user
設置爲 null
,那麼再調用全部 SetXxxx
方法都會拋空指針異常,而再次調用 Build()
方法則會拋 InvalidOperationException
異常。
其實這個很普通的 C# 的內部類實現。但它確實能夠解答「C# 中沒有友元怎麼辦」這之類的問題。Java 中也能夠相似的實現,只不過 Java 沒有分部類,因此代碼都得寫在一個源文件裏,這個源文件可能會很長很長……