开发者

What is a "mostly complete" (im)mutability approach for C#? [closed]

开发者 https://www.devze.com 2023-04-11 03:26 出处:网络
Closed. This question needs to be more focused. It is not currently accepting answers. Want to improve this question? Update the question so it focuses on one problem only by editing this po
Closed. This question needs to be more focused. It is not currently accepting answers.

Want to improve this question? Update the question so it focuses on one problem only by editing this post.

Closed 6 years ago.

Improve this question

Since immutability is not fully baked into C# to the degree it is for F#, or fully into the framework (BCL) despite some support in the CLR, what's a fairly complete solution for (im)mutability for C#?

My order of preference is a solution consisting of general patterns/principles compatible with

  • a single open-source library with few dependencies
  • a small number of complementary/compatible open-source libraries
  • something commercial

that

  • covers Lippert's kinds of immutability
  • offers decent performance (that's vague I know)
  • supports serialization
  • supports cloning/copying (deep/shallow/partial?)
  • feels natural in scenarios such as DDD, builder patterns, configuration, an开发者_开发问答d threading
  • provides immutable collections

I'd also like to include patterns you as the community might come up with that don't exactly fit in a framework such as expressing mutability intent through interfaces (where both clients that shouldn't change something and may want to change something can only do so through interfaces, and not the backing class (yes, I know this isn't true immutability, but sufficient):

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");
    // ...
}


Would the better solution be to just use F# where you want true immutability?


Use this T4 template I put together to solve this problem. It should generally suit your needs for whatever kinds of immutable objects you need to create.

There's no need to go with generics or use any interfaces. For my purposes, I do not want my immutable classes to be convertible to one another. Why would you? What common traits should they share that means they should be convertible to one another? Enforcing a code pattern should be the job of a code generator (or better yet, a nice-enough type system to allow you to do define general code patterns, which C# unfortunately does not have).

Here's some example output from the template to illustrate the basic concept at play (nevermind the types used for the properties):

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);
    }
}

The basic pattern is to have an immutable class with a nested mutable Builder class that is used to construct instances of the immutable class in a mutable way. The only way to set the immutable class's properties is to construct a ImmutableType.Builder class and set that in the normal mutable way and convert that to its containing ImmutableType class with an implicit conversion operator.

You can extend the T4 template to add a default public ctor to the ImmutableType class itself so you can avoid a double allocation if you can set all the properties up-front.

Here's an example usage:

CommitPartial cp = new CommitPartial.Builder() { Message = "Hello", OtherFields = value, ... };

or...

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;

Note that the implicit conversion operator from CommitPartial.Builder to CommitPartial is used in the assignment. That's the part that "freezes" the mutable CommitPartial.Builder by constructing a new immutable CommitPartial instance out of it with normal copy semantics.


Personally, I'm not really aware of any third party or previous solutions to this problem, so my apologies if I'm covering old ground. But, if I were going to implement some kind of immutability standard for a project I was working on, I would start with something like this:

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();
    }
}

This interface would be implemented something like:

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) };
    }
}

And client code would be something like:

    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);
    }

The glaring deficiency is that the implementation is only as good as the client of ISnapshot's implementation of TakeSnapshot, but at least it would standardize things and you'd know where to go searching if you had issues related to questionable mutability. The burden would also be on potential implementors to recognize whether or not they could provide snapshot immutability and not implement the interface, if not (i.e. the class returns a reference to a field that does not support any kind of clone/copy and thus cannot be snapshot-ed).

As I said, this is a start—how I'd probably start—certainly not an optimal solution or a finished, polished idea. From here, I'd see how my usage evolved and modify this approach accordingly. But, at least here I'd know that I could define how to make something immutable and write unit tests to assure myself that it was.

I realize that this isn't far removed from just implementing an object copy, but it standardizes copy vis a vis immutability. In a code base, you might see some implementors of ICloneable, some copy constructors, and some explicit copy methods, perhaps even in the same class. Defining something like this tells you that the intention is specifically related to immutability—I want a snapshot as opposed to a duplicate object because I happen to want n more of that object. The Immtuable<T> class also centralizes the relationship between immutability and copies; if you later want to optimize somehow, like caching the snapshot until dirty, you needn't do it in all implementors of copying logic.


If the goal is to have objects which behave as unshared mutable objects, but which can be shared when doing so would improve efficiency, I would suggest having a private, mutable "fundamental data" type. Although anyone holding a reference to objects of this type would be able to mutate it, no such references would ever escape the assembly. All outside manipulations to the data must be done through wrapper objects, each of which holds two references:

  1. UnsharedVersion--Holds the only reference in existence to its internal data object, and is free to modify it
  2. SharedImmutableVersion--Holds a reference to the data object, to which no references exist except in other SharedImmutableVersion fields; such objects may be of a mutable type, but will in practice be immutable because no references will ever be made available to code that would mutate them.

One or both fields may be populated; when both are populated, they should refer to instances with identical data.

If an attempt is made to mutate an object via the wrapper and the UnsharedVersion field is null, a clone of the object in SharedImmutableVersion should be stored in UnsharedVersion. Next, SharedImmutableCVersion should be cleared and the object in UnsharedVersion mutated as desired.

If an attempt is made to clone an object, and SharedImmutableVersion is empty, a clone of the object in UnsharedVersion should be stored into SharedImmutableVersion. Next, a new wrapper should be constructed with its UnsharedVersion field empty and its SharedImmutableVersion field populated with the SharedImmutableVersion from the original.

It multiple clones are made of an object, whether directly or indirectly, and the object hasn't been mutated between the construction of those clones, all clones will refer to the same object instance. Any of those clones may be mutated, however, without affecting the others. Any such mutation would generate a new instance and store it in UnsharedVersion.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号