如何确保通用的不变性

csc*_*can 5 c# generics immutability

这个例子在C#中,但问题确实适用于任何OO语言.我想创建一个实现IReadOnlyList的通用不可变类.此外,此类应具有无法修改的基础通用IList.最初,该课程编写如下:

public class Datum<T> : IReadOnlyList<T>
{
    private IList<T> objects;
    public int Count 
    { 
        get; 
        private set;
    }
    public T this[int i]
    {
        get
        {
            return objects[i];
        }
        private set
        {
            this.objects[i] = value;
        }
    }

    public Datum(IList<T> obj)
    {
        this.objects = obj;
        this.Count = obj.Count;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    public IEnumerator<T> GetEnumerator()
    {
        return this.objects.GetEnumerator();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,这不是一成不变的.正如您可能知道的那样,更改初始IList'obj'会更改Datum的'对象'.

static void Main(string[] args)
{
    List<object> list = new List<object>();
    list.Add("one");
    Datum<object> datum = new Datum<object>(list);
    list[0] = "two";
    Console.WriteLine(datum[0]);
}
Run Code Online (Sandbox Code Playgroud)

这将"两个"写入控制台.由于Datum的观点是不变的,所以这不可行.为了解决这个问题,我重写了Datum的构造函数:

public Datum(IList<T> obj)
{
    this.objects = new List<T>();
    foreach(T t in obj)
    {
        this.objects.Add(t);
    }
    this.Count = obj.Count;
}
Run Code Online (Sandbox Code Playgroud)

给定与之前相同的测试,控制台上会出现"one".大.但是,如果Datum包含非不可变集合的集合并且其中一个非不可变集合被修改,该怎么办?

static void Main(string[] args)
{
    List<object> list = new List<object>();
    List<List<object>> containingList = new List<List<object>>();
    list.Add("one");
    containingList.Add(list);
    Datum<List<object>> d = new Datum<List<object>>(containingList);
    list[0] = "two";
    Console.WriteLine(d[0][0]);
}
Run Code Online (Sandbox Code Playgroud)

并且,正如预期的那样,在控制台上打印出"两个".所以,我的问题是,我如何让这个类真正不变?

Tim*_* S. 6

你不能.或者更确切地说,你不想这样做,因为这样做的方式非常糟糕.以下是一些:

1. struct-only

加入where T : struct你的Datum<T>班级.structs 通常是不可变的,但如果它包含可变class实例,它仍然可以修改(感谢Servy).主要的缺点是所有类都是out,甚至是不可变的类string和你所做的任何不可变类.

var e = new ExtraEvilStruct();
e.Mutable = new Mutable { MyVal = 1 };
Datum<ExtraEvilStruct> datum = new Datum<ExtraEvilStruct>(new[] { e });
e.Mutable.MyVal = 2;
Console.WriteLine(datum[0].Mutable.MyVal); // 2
Run Code Online (Sandbox Code Playgroud)

2.创建一个界面

创建标记接口并在您创建的任何不可变类型上实现它.主要缺点是所有内置类型都已淘汰.而且你真的不知道实现它的类是否真的是不可变的.

public interface IImmutable
{
    // this space intentionally left blank, except for this comment
}
public class Datum<T> : IReadOnlyList<T> where T : IImmutable
Run Code Online (Sandbox Code Playgroud)

3.序列化!

如果序列化和反序列化传递的对象(例如使用Json.NET),则可以创建完全独立的副本.好处:适用于您可能希望放在此处的许多内置和自定义类型.缺点:需要额外的时间和内存来创建只读列表,并要求您的对象可序列化而不会丢失任何重要的内容.期望销毁列表外部对象的任何链接.

public Datum(IList<T> obj)
{
    this.objects =
      JsonConvert.DeserializeObject<IList<T>>(JsonConvert.SerializeObject(obj));
    this.Count = obj.Count;
}
Run Code Online (Sandbox Code Playgroud)

我建议您只是简单地说明Datum<T>该类应该只用于存储不可变类型.这种未强制的隐式需求存在于其他类型中(例如,Dictionary期望TKey实现GetHashCode和Equals以预期的方式,包括不变性),因为它太难以不那样.

  • 基本上,简而言之,您可以礼貌地询问是否使用了不可变对象,但实际上阻止确定的用户将可变状态传递给该类型是不可能的. (2认同)