什么是 C# 的"mostly complete"(不)可变性方法?



由于不变性并没有像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.BuilderCommitPartial的隐式转换运算符。这是通过用普通复制语义构造一个新的不可变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>类还集中了不变性和拷贝之间的关系;如果您以后想要以某种方式进行优化,比如缓存快照直到变脏,则不必在所有复制逻辑的实现者中都这样做。

如果目标是让对象表现为非共享的可变对象,但这样做可以提高效率,我建议使用私有的可变"基本数据"类型。尽管持有对该类型对象的引用的任何人都可以对其进行变异,但任何此类引用都不会脱离程序集。所有对数据的外部操作都必须通过包装器对象完成,每个包装器对象都有两个引用:

  1. 非共享版本--保留对其内部数据对象的唯一引用,并且可以自由修改
  2. SharedImmutableVersion--保留对数据对象的引用,除了在其他SharedImmutaleVersion字段中之外,不存在对该数据对象的任何引用;这样的对象可能是可变类型的,但在实践中是不可变的,因为任何引用都不会提供给会使它们发生变异的代码。

可以填充一个或两个字段;当两者都被填充时,它们应该引用具有相同数据的实例。

如果试图通过包装对对象进行变异,并且UnsharedVersion字段为null,则SharedImmutableVersion中对象的克隆应存储在UnsharedVersion。接下来,应该清除SharedImmutableCVersion,并根据需要对UnsharedVersion中的对象进行突变。

如果尝试克隆对象,而SharedImmutableVersion为空,则应将非共享版本中对象的克隆存储到SharedImmutaleVersion中。接下来,应该构造一个新的包装,使其UnsharedVersion字段为空,并用原始包装中的SharedImmutableVersion填充SharedImmutaleVersion字段。

如果多个克隆是由一个对象直接或间接组成的,并且该对象在这些克隆的构建之间没有发生突变,那么所有克隆都将引用同一个对象实例。然而,这些克隆中的任何一个都可能发生突变,而不会影响其他克隆。任何这样的突变都会生成一个新实例并将其存储在UnsharedVersion中。

相关内容

最新更新