如何确保通用的不变性

时间:2014-12-05 18:41:51

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

但是,这不是一成不变的。正如您可能知道的那样,更改初始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]);
}

这会将“two”写入控制台。由于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;
}

考虑到与之前相同的测试,控制台上会出现“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]);
}

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

5 个答案:

答案 0 :(得分:6)

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

1。 struct - 仅

where T : struct添加到Datum<T>课程。 structusually immutable,但如果它包含可变的class个实例,它仍然可以被修改(感谢Servy)。主要的缺点是所有类都已经出局,甚至是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

2。创建一个接口

创建一个marker interface并在您创建的任何不可变类型上实现它。主要缺点是所有内置类型都已淘汰。而且你真的不知道实现它的类是否真的 不可变。

public interface IImmutable
{
    // this space intentionally left blank, except for this comment
}
public class Datum<T> : IReadOnlyList<T> where T : IImmutable

3。连载!

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

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

我建议您只需记录Datum<T>来说该类只应用于存储不可变类型。这种未强制的隐式要求存在于其他类型中(例如Dictionary期望TKey以预期的方式实现GetHashCodeEquals,包括不变性,因为它是&#39;太难了它不是那样的。

答案 1 :(得分:0)

这是不可能的。没有可能的方法将泛型类型约束为不可变的。您可以做的最好的事情是编写一个不允许修改该集合结构的集合。没有办法阻止该集合被用作某些可变类型的集合。

答案 2 :(得分:0)

我遇到了同样的问题,我实现了一个对象(例如CachedData<T>),该对象处理另一个对象(例如T SourceData)的属性的缓存副本。调用CachedData的构造函数时,您传递了一个返回SourceData的委托。调用CachedData<T>.value时,您会得到SourceData的副本,该副本会不时更新。

尝试缓存对象没有任何意义,因为.Value仅缓存对数据的引用,而不缓存数据本身。缓存数据类型,字符串以及结构可能是有意义的。

所以我结束了:

  1. 彻底记录CachedData<T>,并
  2. 如果T既不是ValueType,不是Structure也不是String,则在构造函数中引发错误。有点喜欢(原谅我的VB):If GetType(T) <> GetType(String) AndAlso GetType(T).IsClass Then Throw New ArgumentException("Explain")

答案 3 :(得分:-1)

有点hacky,而且在我看来肯定比它的价值更令人困惑,但如果你的T保证可序列化,你可以在你的集合中存储对象的字符串表示,而不是存储对象本身。然后,即使有人从您的收藏中提取物品并对其进行修改,您的收藏仍然完好无损。

这很慢,每次从列表中删除它都会得到一个不同的对象。所以我不推荐这个。

类似的东西:

public class Datum<T> : IReadOnlyList<T>
{
    private IList<string> objects;
    public T this[int i] {
        get { return JsonConvert.DeserializeObject<T>(objects[i]); }
        private set { this.objects[i] = JsonConvert.SerializeObject(value); }
    }

    public Datum(IList<T> obj) {
        this.objects = new List<string>();
        foreach (T t in obj) {
            this.objects.Add(JsonConvert.SerializeObject(t));
        }
        this.Count = obj.Count;
    }

    public IEnumerator<T> GetEnumerator() {
        return this.objects.Select(JsonConvert.DeserializeObject<T>).GetEnumerator();
    }
}

答案 4 :(得分:-1)

认为这样的集合与OOP不匹配,因为这种设计导致了独立类 - 集合及其项目之间的特定共同关系。一个班级如何在没有相互了解的情况下改变他人的行为?

所以序列化的建议可以让你以hacky的方式做到,但更好的是决定它是否需要收集不可变项目,谁试图改变它们除了你自己的代码?可能会更好&#34;不变异&#34;物品而不是尝试使它们变得不可变#34;