友元是 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 没有分部类,所以代码都得写在一个源文件里,这个源文件可能会很长很长……
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。