好吧,据我所知, immutable 类型本质上是线程安全的,所以我在各个地方读过,我想我明白为什么会这样。如果在创建对象后无法修改实例的内部状态,则对实例本身的并发访问似乎没有问题。
因此,我可以创建以下List
:
class ImmutableList<T>: IEnumerable<T>
{
readonly List<T> innerList;
public ImmutableList(IEnumerable<T> collection)
{
this.innerList = new List<T>(collection);
}
public ImmutableList()
{
this.innerList = new List<T>();
}
public ImmutableList<T> Add(T item)
{
var list = new ImmutableList<T>(this.innerList);
list.innerList.Add(item);
return list;
}
public ImmutableList<T> Remove(T item)
{
var list = new ImmutableList<T>(this.innerList);
list.innerList.Remove(item);
return list;
} //and so on with relevant List methods...
public T this[int index]
{
get
{
return this.innerList[index];
}
}
public IEnumerator<T> GetEnumerator()
{
return innerList.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((System.Collections.IEnumerable)this.innerList).GetEnumerator();
}
}
所以问题是:这个真的是不可变的类型吗?它真的是线程安全吗?
显然,类型本身是 immutable ,但绝对没有T
的保证,因此您可能会遇到与泛型类型直接相关的并发访问和线程问题。这是否意味着ImmutableList
应被视为 mutable ?。
class ImmutableList<T>: IEnumerable<T> where T: struct
应该是真正被视为 immutable 的唯一类型吗?
感谢您就此问题提供任何意见。
更新:很多答案/评论都集中在我发布的ImmutableList
的特定实现上,这可能不是一个很好的例子。但问题的问题不在于实施。我问的问题是ImmutableList<MutableT>
是否真的是一个不可变的类型,考虑到不可变类型所需的一切。
答案 0 :(得分:27)
如果在创建对象后无法修改实例的内部状态,则对实例本身的并发访问似乎没有问题。
通常情况如此,是的。
这真的是一个不可变的类型吗?
简要总结一下:你有一个围绕可变列表的写时复制包装器。将新成员添加到不可变列表不会改变列表;相反,它会创建基础可变列表的副本,添加到副本,并在副本周围返回一个包装器。
如果您正在包装的基础列表对象在读取时不会改变其内部状态,那么您已经满足了原始定义&#34; immutable&#34;,所以,是。
我注意到这不是一种非常高效的方法来实现不可变列表。例如,您可能会使用不可变的平衡二叉树做得更好。每次制作新列表时,草图的时间和记忆都是O(n);你可以毫不费力地将其提高到O(log n)。
它真的是线程安全吗?
如果底层的可变列表对多个读者来说是线程安全的,是的。
您可能会对此感兴趣:
http://blogs.msdn.com/b/ericlippert/archive/2011/05/23/read-only-and-threadsafe-are-different.aspx
显然,类型本身是不可变的,但绝对没有保证T是,因此你可以直接与泛型类型相关的并发访问和线程问题。这是否意味着
ImmutableList<T>
应该被认为是可变的?。
这是一个哲学问题,而不是技术问题。如果你有一个不可改变的人名列表,并且列表永远不会改变,但是其中一个人死了,那就是名字列表&#34;可变的&#34;?我想不会。
如果有关列表的任何问题总是具有相同的答案,则列表是不可变的。在我们的人名列表中,&#34;列表中有多少名字?&#34;是关于该列表的问题。 &#34;这些人中有多少人还活着?&#34;这不是关于列表的问题,而是关于列表中提到的人的问题。这个问题的答案随着时间的推移而变化;第一个问题的答案没有。
类
ImmutableList<T>: IEnumerable<T> where T: struct
应该是真正被认为是不可变的唯一类型吗?
我没跟着你。如何限制T成为一个结构改变什么?好的,T仅限于struct。我创建了一个不可变的结构:
struct S
{
public int[] MutableArray { get; private set; }
...
}
现在我做了ImmutableList<S>
。什么阻止我修改存储在S实例中的可变数组?仅仅因为列表是不可变的并且结构是不可变的并不能使数组不可变。
答案 1 :(得分:5)
不变性有时以不同的方式定义。线程安全也是如此。
在创建一个目的是不可变的不可变列表时,你应该记录你正在做什么保证。例如。在这种情况下,您保证列表本身是不可变的并且没有任何隐藏的可变性(一些显然不可变的对象在幕后实际上是可变的,例如memoisation或内部重新排序作为优化)这消除了线程安全性从不变性(虽然也可以以一种以不同方式保证线程安全的方式执行此类内部突变)。您不能保证存储的对象可以以线程安全的方式使用。
您应记录的线程安全性与此相关。您不能保证另一个对象不会具有相同的对象(如果您在每次调用时创建新对象,则可以)。您可以保证操作不会破坏列表本身。
坚持T : struct
可以提供帮助,因为这意味着您可以确保每次返回某个项目时,它都是该结构的新副本(仅T : struct
不会这样做,因为你可以进行不改变列表的操作,但确实改变了它的成员,所以很明显你也必须这样做。)
这虽然限制了你们不支持不可变引用类型(例如string
,它往往是许多真实案例中的集合的成员),并且不允许用户使用它和提供自己的方法来确保所包含项目的可变性不会导致问题。由于没有任何线程安全的对象可以保证它所使用的所有代码都是线程安全的,因此确保(尽可能多地帮助你,但不要试图确保你可以做什么)确保)。
它也不保护不可变列表中不可变结构的可变成员!
答案 2 :(得分:2)
使用您的代码,假设我这样做:
ImmutableList<int> mylist = new ImmutableList<int>();
mylist.Add(1);
...发布在StackOverflow上的代码会导致StackOverflow-Exception。有很多明智的方法来创建线程保存集合,复制集合(至少尝试)并将它们称为不可变的,很多,并不是很有效。
Eric Lippert发布的链接可能非常值得一读。
答案 3 :(得分:2)
数据类型的主要示例,其行为是可变对象的不可变列表:MulticastDelegate。 MulticastDelegate可以非常准确地建模为(对象,方法)对的不可变列表。方法集和它们所作用的对象的标识是不可变的,但在绝大多数情况下,对象本身都是可变的。实际上,在许多情况下(如果不是大多数情况下),委托的目的将改变它所持有引用的对象。
委托人不负责知道它要调用其目标对象的方法是否会以线程不安全的方式改变它们。代表只负责确保其功能和对象标识列表是不可变的,我认为没有人会同样期望它。
ImmutableList<T>
同样应始终保持同一组T
类型的实例。这些实例的属性可能会更改,但不会更改其身份。如果List<Car>
创建了两个Fords,序列号#1234和#4422,两个都是红色的,其中一个Fords被涂成蓝色,那么最初的两个红色汽车列表会发生变化到一个拿着蓝色汽车和一辆红色汽车的清单,但它仍然会保留#1234和#4422。
答案 4 :(得分:1)
我相信,关于该主题的冗长讨论增加了一件事,即不变性/可变性应与范围一起考虑。
例如:
假设我正在使用C#项目,并且定义了具有静态数据结构的静态类,并且没有定义在项目中修改这些结构的方法。实际上,它是我可以使用的只读数据缓存,对于我的程序而言,从我的程序的一次运行到下一次的运行,它是不可变的。我对数据有完全控制权,我/用户无法在运行时对其进行修改。
现在,我在源代码中修改数据并重新运行该程序。数据已更改,因此从字面上看,数据不再是不可变的。从最高级别的角度来看,数据实际上是可变的。
因此,我认为我们需要讨论的不是黑白问题掩盖某些事物是否不变,而应该考虑特定实现的可变性程度(因为实际上很少有东西变化,因此是真正不变的。
答案 5 :(得分:0)
我想说,如果元素是可变的,那么泛型列表不是不可变的,因为它不代表数据状态的完整快照。
要在您的示例中实现不变性,您必须创建列表元素的深层副本,这不是每次都有效。
您可以在IOG library
查看我对此问题的解决方案