友元是 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 没有分部类,因此代码都得写在一个源文件里,这个源文件可能会很长很长……