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;
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.
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.
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);
}
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:
UnsharedVersion--Holds the only reference in existence to its internal data object, and is free to modify it
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.
发布评论
评论(4)
更好的解决方案是在需要真正不变性的地方使用 F# 吗?
Would the better solution be to just use F# where you want true immutability?
使用我整理的这个 T4 模板 来解决这个问题。它通常应该满足您需要创建的任何类型的不可变对象的需求。
无需使用泛型或使用任何接口。出于我的目的,我不希望我的不可变类可以相互转换。你为什么要这么做?他们应该具有哪些共同特征才能使他们可以相互转换?强制执行代码模式应该是代码生成器的工作(或者更好的是,一个足够好的类型系统允许您定义通用代码模式,不幸的是 C# 没有)。
下面是模板的一些示例输出,用于说明基本概念(不用介意用于属性的类型):
基本模式是拥有一个不可变类,其中包含一个嵌套可变 Builder 类,该类用于以可变方式构造不可变类的实例。设置不可变类属性的唯一方法是构造一个
ImmutableType.Builder
类,并以正常的可变方式设置它,并使用隐式方法将其转换为包含它的ImmutableType
类。转换运算符。您可以扩展 T4 模板,向 ImmutableType 类本身添加一个默认的公共构造函数,这样如果您可以预先设置所有属性,就可以避免双重分配。
以下是用法示例:
或...
请注意,在赋值中使用了从
CommitPartial.Builder
到CommitPartial
的隐式转换运算符。这就是通过使用正常复制语义构造一个新的不可变CommitPartial
实例来“冻结”可变CommitPartial.Builder
的部分。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):
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 aImmutableType.Builder
class and set that in the normal mutable way and convert that to its containingImmutableType
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:
or...
Note that the implicit conversion operator from
CommitPartial.Builder
toCommitPartial
is used in the assignment. That's the part that "freezes" the mutableCommitPartial.Builder
by constructing a new immutableCommitPartial
instance out of it with normal copy semantics.就我个人而言,我并不真正了解这个问题的任何第三方或以前的解决方案,因此,如果我涉及旧问题,我深表歉意。但是,如果我要为我正在从事的项目实现某种不变性标准,我会从这样的开始:
该接口将实现如下所示:
客户端代码将类似于:
明显的缺陷是该实现仅与
ISnapshot
的TakeSnapshot
实现的客户端一样好,但至少它可以标准化事物,并且如果您有与可疑的可变性相关的问题。潜在的实现者也有责任认识到他们是否可以提供快照不变性并且不实现该接口,如果不能(即该类返回对不支持任何类型的克隆/复制的字段的引用,因此不能被快照版)。正如我所说,这是一个开始——我可能会这样开始——当然不是一个最佳的解决方案或一个完成的、完美的想法。从这里,我会看到我的用法是如何演变的,并相应地修改这种方法。但是,至少在这里我知道我可以定义如何使某些东西不可变并编写单元测试以确保它是不可变的。
我意识到这与实现对象复制相差不远,但它标准化了复制与不可变性。在代码库中,您可能会看到一些
ICloneable
实现者、一些复制构造函数和一些显式复制方法,甚至可能在同一个类中。定义这样的东西会告诉你,意图与不变性特别相关——我想要一个快照而不是一个重复的对象,因为我碰巧想要n个该对象。Immtuable
类还集中了不变性和副本之间的关系;如果您稍后想要以某种方式进行优化,例如缓存快照直到变脏,则无需在复制逻辑的所有实现器中执行此操作。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:
This interface would be implemented something like:
And client code would be something like:
The glaring deficiency is that the implementation is only as good as the client of
ISnapshot
's implementation ofTakeSnapshot
, 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. TheImmtuable<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.如果目标是让对象充当非共享的可变对象,但可以在这样做时共享以提高效率,那么我建议使用私有的、可变的“基本数据”类型。尽管任何持有这种类型对象引用的人都能够改变它,但这样的引用永远不会逃脱程序集。对数据的所有外部操作都必须通过包装对象来完成,每个包装对象都拥有两个引用:
可以填充一个或两个字段;当两者都填充时,它们应该引用具有相同数据的实例。
如果尝试通过包装器改变对象并且 UnsharedVersion 字段为 null,则 SharedImmutableVersion 中对象的克隆应存储在 UnsharedVersion 中。接下来,应清除 SharedImmutableCVersion,并根据需要更改 UnsharedVersion 中的对象。
如果尝试克隆一个对象,并且 SharedImmutableVersion 为空,则 UnsharedVersion 中的对象克隆应存储到 SharedImmutableVersion 中。接下来,应构造一个新的包装器,其 UnsharedVersion 字段为空,其 SharedImmutableVersion 字段填充有原始的 SharedImmutableVersion 字段。
如果多个克隆是由一个对象直接或间接组成的,并且该对象在这些克隆的构造之间没有发生变化,那么所有克隆都将引用同一个对象实例。然而,这些克隆中的任何一个都可能发生突变,而不会影响其他克隆。任何此类突变都会生成一个新实例并将其存储在 UnsharedVersion 中。
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:
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.