简体   繁体   English

如何确保泛型的不变性

[英]How to Ensure Immutability of a Generic

This example is in C# but the question really applies to any OO language.这个例子是在 C# 中,但这个问题确实适用于任何 OO 语言。 I'd like to create a generic, immutable class which implements IReadOnlyList.我想创建一个通用的、不可变的类来实现 IReadOnlyList。 Additionally, this class should have an underlying generic IList which is unable to be modified.此外,此类应具有无法修改的基础通用 IList。 Initially, the class was written as follows:最初,这个类是这样写的:

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

However, this isn't immutable.然而,这不是一成不变的。 As you can likely tell, changing the initial IList 'obj' changes Datum's 'objects'.正如您可能知道的那样,更改初始 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]);
}

This writes "two" to the console.这会将“二”写入控制台。 As the point of Datum is immutability, that's not okay.由于 Datum 的要点是不变性,这是不行的。 In order to resolve this, I've rewritten the constructor of 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;
}

Given the same test as before, "one" appears on the console.给定与之前相同的测试,控制台上会出现“一个”。 Great.伟大的。 But, what if Datum contains a collection of non-immutable collection and one of the non-immutable collections is modified?但是,如果 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]);
}

And, as expected, "two" is printed out on the console.并且,正如预期的那样,控制台上会打印出“two”。 So, my question is, how do I make this class truly immutable?所以,我的问题是,如何让这个类真正不可变?

You can't.你不能。 Or rather, you don't want to, because the ways of doing it are so bad.或者更确切地说,您不想这样做,因为这样做的方式太糟糕了。 Here are a few:以下是一些:

1. struct -only 1. 仅struct

Add where T : struct to your Datum<T> class.where T : struct添加到您的Datum<T>类。 struct s are usually immutable , but if it contains mutable class instances, it can still be modified (thanks Servy). struct s通常是不可变的,但如果它包含可变class实例,它仍然可以被修改(感谢Servy)。 The major downside is that all classes are out, even immutable ones like string and any immutable class you make.主要的缺点是所有的类都被淘汰了,即使是不可变的类,比如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. Create an interface 2.创建接口

Create a marker interface and implement it on any immutable types you create.创建一个标记接口并在您创建的任何不可变类型上实现它。 The major downside is that all built-in types are out.主要的缺点是所有内置类型都被淘汰了。 And you don't really know if classes implementing this are truly immutable.而且你真的不知道实现这个的类是否真的不可变。

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

3. Serialize! 3. 连载!

If you serialize and deserialize the objects that you are passed (eg with Json.NET ), you can create completely-separate copies of them.如果您序列化和反序列化您传递的对象(例如使用Json.NET ),您可以创建它们的完全独立的副本。 Upside: works with many built-in and custom types you might want to put here.好处:适用于您可能想放在这里的许多内置和自定义类型。 Downside: requires extra time and memory to create the read-only list, and requires that your objects are serializable without losing anything important.缺点:需要额外的时间和内存来创建只读列表,并且需要您的对象是可序列化的,而不会丢失任何重要的东西。 Expect any links to objects outside of your list to be destroyed.预计任何指向列表之外对象的链接都会被销毁。

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

I would suggest that you simply document Datum<T> to say that the class should only be used to store immutable types.我建议您简单地记录Datum<T>以说明该类应该仅用于存储不可变类型。 This sort of unenforced implicit requirement exists in other types (eg Dictionary expects that TKey implements GetHashCode and Equals in the expected way, including immutability), because it's too difficult for it to not be that way.这种未强制执行的隐式要求存在于其他类型中(例如, Dictionary期望TKey以预期的方式实现GetHashCodeEquals ,包括不变性),因为它很难不那样做。

Kind of hacky, and definitely more confusing than it's worth in my opinion, but if your T is guaranteed to be serializable, you can store string representations of the objects in your collection rather than storing the objects themselves.有点hacky,在我看来肯定比它的价值更令人困惑,但是如果你的T保证是可序列化的,你可以在你的集合中存储对象的字符串表示,而不是存储对象本身。 Then even if someone pulls an item from your collection and modifies it, your collection would still be intact.这样,即使有人从您的收藏中提取项目并对其进行修改,您的收藏仍然完好无损。

It would be slow and you'd get a different object every time you pulled it from the list.它会很慢,每次从列表中取出它时都会得到一个不同的对象。 So I'm not recommending this.所以我不推荐这个。

Something like:就像是:

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

It's impossible.不可能。 There's no possible way to constrain the generic type to be immutable.没有办法将泛型类型限制为不可变的。 The best that you can possibly do is write a collection that cannot allow the structure of that collection to be modified.您可能做的最好的事情是编写一个不允许修改该集合结构的集合。 There is no way to prevent the collection from being used as a collection of some mutable type.没有办法阻止集合被用作某些可变类型的集合。

think that such collections are not match OOP, because this design leads to specific co-relation between independent classes - collection and it's items.认为这样的集合与 OOP 不匹配,因为这种设计导致了独立类之间的特定关联 - 集合和它的项目。 How one class can change behavior of other without knowlege of each other?一个班级如何在彼此不了解的情况下改变其他班级的行为?

So suggestions of serialization and so can allow you to do it on hacky way, but better is to decide if it's so required to make collection of immutable items, who trys to change them except your own code?所以序列化的建议可以让你以hacky的方式来做,但更好的是决定是否需要收集不可变的项目,除了你自己的代码之外,谁试图改变它们? May be better "to not mutate" items rather than try "make them immutable". “不改变”项目可能比尝试“使它们不可变”更好。

I faced the same problem, where I implement an object (say CachedData<T> ) which handles a cached copy of the property of another object (say T SourceData ).我遇到了同样的问题,我实现了一个对象(比如CachedData<T> ),它处理另一个对象(比如T SourceData )的属性的缓存副本。 When calling the constructor of CachedData , you pass a delegate which returns a SourceData .调用CachedData的构造函数时,您传递一个返回SourceData的委托。 When calling CachedData<T>.value , you get a copy of SourceData , which is updated every now and then.调用CachedData<T>.value ,您将获得SourceData的副本,该副本CachedData<T>.value更新。

It would make no sense to try caching an object, as .Value would only cache the reference to the data, not the data itself.尝试缓存对象是没有意义的,因为.Value只会缓存对数据的引用,而不是数据本身。 It would only make sense to cache data types, strings, and perhaps structures.只有缓存数据类型、字符串和结构才有意义。

So I ended up:所以我结束了:

  1. Thoroughly documenting CachedData<T> , and彻底记录CachedData<T> ,以及
  2. Throwing an error in the constructor if T is neither a ValueType, a Structure, or a String.如果T既不是 ValueType、结构也不是字符串,则在构造函数中抛出错误。 Some like (forgive my VB): If GetType(T) <> GetType(String) AndAlso GetType(T).IsClass Then Throw New ArgumentException("Explain")有些人喜欢(原谅我的 VB): If GetType(T) <> GetType(String) AndAlso GetType(T).IsClass Then Throw New ArgumentException("Explain")

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM