友元是 C++ 中的概念,包含友元函数和友元类。被某个类声明为友元的函数或类可以访问这个类的私有成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。因此,除了 C++ 外很难再看到友元语法特性。
提出问题
但是友元并非一无是处,在某些时候确实有这样的需求。举例来说,现在我们需要定义一个 User
类,为了避免 User
对象在使用过程中属性被修改,需要将它设计成 Immutable 的。到目前为止还没有什么问题,但接下来问题来了——由于用户信息较多,其属性设计有十数个,为了 Immutable 全部通过构造方法的参数来设置属性是件让人悲伤的事情。
那么一般我们会想到这样几个方案:
方案简述
方案一,使用参数对象
这是 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;
}
}
一个属性就需要重复写三遍,如果代码是按行付费,这个定义会非常赚!
一次性设置
这种做法是自定义属性的 set
函数,或者定义一个 SetXxxxx
方法,判断如果值为 null
则可以设置,一但设置将不能再设置(理论上来说应该抛异常,但这里示例简化为无作为)。
下面的示例通过 Username
和 Name
演示了一次性设置的两种方法
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,只是近似,因为它的属性不能从“有”到“无”,却可以从“无”到“有”。
而且,我发现这个方法比上一个方法更赚钱。
Builder
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 {
// ....
}
内部类实现 Builder 模式
结构上没有问题了。再利用 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
的属性。
再严谨一点
可重复使用的 Builder
如果想把 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
一次性 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 没有分部类,所以代码都得写在一个源文件里,这个源文件可能会很长很长……
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。