由于不变性并没有像F#那样完全融入C#,或者尽管CLR中有一些支持,但它并没有完全融入框架(BCL(,那么C#的(im(可变性有什么相当完整的解决方案?
我的首选顺序是由与兼容的通用模式/原则组成的解决方案
- 一个几乎没有依赖关系的单一开源库
- 少量互补/兼容的开源库
- 商业性的东西
- 涵盖了Lippert的各种不变性
- 提供了不错的性能(我知道这很模糊(
- 支持序列化
- 支持克隆/复制(深/浅/部分?(
- 在DDD、构建器模式、配置和线程等场景中感觉很自然
- 提供不可变的集合
我还想包括你作为社区可能会想出的不完全适合框架的模式,比如通过接口表达可变性意图(其中不应该更改某些内容,而可能想要修改某些内容的客户端只能通过接口来实现,而不能通过支持类来实现(是的,我知道这不是真正的不变性,但已经足够了(:
public interface IX
{
int Y{ get; }
ReadOnlyCollection<string> Z { get; }
IMutableX Clone();
}
public interface IMutableX: IX
{
new int Y{ get; set; }
new ICollection<string> Z{ get; } // or IList<string>
}
// generally no one should get ahold of an X directly
internal class X: IMutableX
{
public int Y{ get; set; }
ICollection<string> IMutableX.Z { get { return z; } }
public ReadOnlyCollection<string> Z
{
get { return new ReadOnlyCollection<string>(z); }
}
public IMutableX Clone()
{
var c = MemberwiseClone();
c.z = new List<string>(z);
return c;
}
private IList<string> z = new List<string>();
}
// ...
public void ContriveExample(IX x)
{
if (x.Y != 3 || x.Z.Count < 10) return;
var c= x.Clone();
c.Y++;
c.Z.Clear();
c.Z.Add("Bye, off to another thread");
// ...
}
更好的解决方案是在需要真正不变性的地方使用F#吗?
使用我组装的T4模板来解决这个问题。它通常应该适合您需要创建的任何类型的不可变对象。
没有必要使用泛型或任何接口。出于我的目的,我不希望我的不可变类可以相互转换。你为什么要?他们应该有哪些共同的特征,这意味着他们应该相互转换?强制执行代码模式应该是代码生成器的工作(或者更好的是,一个足够好的类型系统,允许您定义通用代码模式,而C#不幸没有(。
以下是模板的一些示例输出,以说明正在发挥作用的基本概念(更不用说用于属性的类型(:
public sealed partial class CommitPartial
{
public CommitID ID { get; private set; }
public TreeID TreeID { get; private set; }
public string Committer { get; private set; }
public DateTimeOffset DateCommitted { get; private set; }
public string Message { get; private set; }
public CommitPartial(Builder b)
{
this.ID = b.ID;
this.TreeID = b.TreeID;
this.Committer = b.Committer;
this.DateCommitted = b.DateCommitted;
this.Message = b.Message;
}
public sealed class Builder
{
public CommitID ID { get; set; }
public TreeID TreeID { get; set; }
public string Committer { get; set; }
public DateTimeOffset DateCommitted { get; set; }
public string Message { get; set; }
public Builder() { }
public Builder(CommitPartial imm)
{
this.ID = imm.ID;
this.TreeID = imm.TreeID;
this.Committer = imm.Committer;
this.DateCommitted = imm.DateCommitted;
this.Message = imm.Message;
}
public Builder(
CommitID pID
,TreeID pTreeID
,string pCommitter
,DateTimeOffset pDateCommitted
,string pMessage
)
{
this.ID = pID;
this.TreeID = pTreeID;
this.Committer = pCommitter;
this.DateCommitted = pDateCommitted;
this.Message = pMessage;
}
}
public static implicit operator CommitPartial(Builder b)
{
return new CommitPartial(b);
}
}
基本模式是具有一个不可变类和一个嵌套的可变Builder
类,该类用于以可变的方式构造不可变类的实例。设置不可变类属性的唯一方法是构造一个ImmutableType.Builder
类,并以正常的可变方式设置它,然后使用隐式转换运算符将其转换为包含它的ImmutableType
类。
您可以扩展T4模板,将默认的公共ctor添加到ImmutableType
类本身,这样,如果您可以预先设置所有属性,就可以避免双重分配。
下面是一个用法示例:
CommitPartial cp = new CommitPartial.Builder() { Message = "Hello", OtherFields = value, ... };
或者。。。
CommitPartial.Builder cpb = new CommitPartial.Builder();
cpb.Message = "Hello";
...
// using the implicit conversion operator:
CommitPartial cp = cpb;
// alternatively, using an explicit cast to invoke the conversion operator:
CommitPartial cp = (CommitPartial)cpb;
注意,在赋值中使用了从CommitPartial.Builder
到CommitPartial
的隐式转换运算符。这是通过用普通复制语义构造一个新的不可变CommitPartial
实例来"冻结"可变CommitPartial.Builder
的部分。
就我个人而言,我真的不知道这个问题有任何第三方或以前的解决方案,所以如果我涉及到旧的问题,我很抱歉。但是,如果我要为我正在进行的项目实现某种不变性标准,我会从以下内容开始:
public interface ISnaphot<T>
{
T TakeSnapshot();
}
public class Immutable<T> where T : ISnaphot<T>
{
private readonly T _item;
public T Copy { get { return _item.TakeSnapshot(); } }
public Immutable(T item)
{
_item = item.TakeSnapshot();
}
}
这个接口的实现方式类似于:
public class Customer : ISnaphot<Customer>
{
public string Name { get; set; }
private List<string> _creditCardNumbers = new List<string>();
public List<string> CreditCardNumbers { get { return _creditCardNumbers; } set { _creditCardNumbers = value; } }
public Customer TakeSnapshot()
{
return new Customer() { Name = this.Name, CreditCardNumbers = new List<string>(this.CreditCardNumbers) };
}
}
客户端代码可能类似于:
public void Example()
{
var myCustomer = new Customer() { Name = "Erik";}
var myImmutableCustomer = new Immutable<Customer>(myCustomer);
myCustomer.Name = null;
myCustomer.CreditCardNumbers = null;
//These guys do not throw exceptions
Console.WriteLine(myImmutableCustomer.Copy.Name.Length);
Console.WriteLine("Credit card count: " + myImmutableCustomer.Copy.CreditCardNumbers.Count);
}
明显的不足之处在于,该实现仅与ISnapshot
的客户端实现TakeSnapshot
一样好,但至少它会使事情标准化,并且如果您遇到与可疑的可变性相关的问题,您会知道该去哪里搜索。潜在的实现者也有责任识别他们是否可以提供快照不变性,如果不能,则不实现接口(即类返回对不支持任何类型的克隆/复制的字段的引用,因此不能进行快照(。
正如我所说,这是一个开始——我可能会这样开始——当然不是一个最佳的解决方案,也不是一个完善的想法。从这里,我将看到我的用法是如何演变的,并相应地修改这种方法。但是,至少在这里我知道我可以定义如何使某些东西不可变,并编写单元测试来确保它是不变的。
我意识到,这离实现对象复制并不遥远,但它将复制标准化为可见不变性。在代码库中,您可能会看到ICloneable
的一些实现者、一些复制构造函数和一些显式复制方法,甚至可能在同一个类中。定义这样的东西告诉你意图与免疫性特别相关——我想要一个快照,而不是一个重复的对象,因为我碰巧想要n更多的对象。Immtuable<T>
类还集中了不变性和拷贝之间的关系;如果您以后想要以某种方式进行优化,比如缓存快照直到变脏,则不必在所有复制逻辑的实现者中都这样做。
如果目标是让对象表现为非共享的可变对象,但这样做可以提高效率,我建议使用私有的可变"基本数据"类型。尽管持有对该类型对象的引用的任何人都可以对其进行变异,但任何此类引用都不会脱离程序集。所有对数据的外部操作都必须通过包装器对象完成,每个包装器对象都有两个引用:
- 非共享版本--保留对其内部数据对象的唯一引用,并且可以自由修改
- SharedImmutableVersion--保留对数据对象的引用,除了在其他SharedImmutaleVersion字段中之外,不存在对该数据对象的任何引用;这样的对象可能是可变类型的,但在实践中是不可变的,因为任何引用都不会提供给会使它们发生变异的代码。
可以填充一个或两个字段;当两者都被填充时,它们应该引用具有相同数据的实例。
如果试图通过包装对对象进行变异,并且UnsharedVersion字段为null,则SharedImmutableVersion中对象的克隆应存储在UnsharedVersion。接下来,应该清除SharedImmutableCVersion,并根据需要对UnsharedVersion中的对象进行突变。
如果尝试克隆对象,而SharedImmutableVersion为空,则应将非共享版本中对象的克隆存储到SharedImmutaleVersion中。接下来,应该构造一个新的包装,使其UnsharedVersion字段为空,并用原始包装中的SharedImmutableVersion填充SharedImmutaleVersion字段。
如果多个克隆是由一个对象直接或间接组成的,并且该对象在这些克隆的构建之间没有发生突变,那么所有克隆都将引用同一个对象实例。然而,这些克隆中的任何一个都可能发生突变,而不会影响其他克隆。任何这样的突变都会生成一个新实例并将其存储在UnsharedVersion中。