如何确保泛型的不变性

问题描述 投票:6回答:5

此示例在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的'objects'。

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

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

给予与以前相同的测试,控制台上出现“一个”。大。但是,如果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]);
}

而且,正如预期的那样,控制台上会打印出“两个”。因此,我的问题是,如何使此类真正不可变?

c# generics immutability
5个回答
7
投票

[您不能。或者,您不想这样做,因为这样做的方法太糟糕了。这里有一些:

1。struct-only

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,并在您创建的任何不可变类型上实现它。主要缺点是所有内置类型都不可用。而且您真的不知道实现此目标的类是否是[[truly不可变的。

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,包括不变性),因为很难做到这一点。 >

0
投票
有点hacky,而且绝对比我认为的要混乱,但是如果保证T可序列化,则可以将对象的字符串表示形式存储在集合中,而不是存储对象本身。这样,即使有人从您的收藏夹中提取一项并对其进行了修改,您的收藏夹也将保持完整。

0
投票
不可能。没有办法将泛型限制为不可变的。您可能要做的最好的事情就是编写一个集合,该集合不允许修改该集合的结构。无法阻止将集合用作某些可变类型的集合。

0
投票
认为此类集合与OOP不匹配,因为这种设计会导致独立类-集合及其项之间的特定关联。一个班级如何在彼此不了解的情况下改变另一个班级的行为?

0
投票
我面临相同的问题,在这里我实现了一个对象(例如CachedData<T>),该对象处理另一个对象(例如T SourceData)的属性的缓存副本。调用CachedData的构造函数时,您传递了一个返回SourceData的委托。调用CachedData<T>.value时,会得到SourceData的副本,该副本会不时更新。
© www.soinside.com 2019 - 2024. All rights reserved.