9

一些程序员认为传递IEnumerable<T>参数比传递实现更好List<T>IEnumerable<T>例如List<T>.

我越是深入研究多线程开发场景,我就越开始认为这IEnumerable<T>也不是正确的类型,我将在下面尝试解释原因。

您在枚举集合时是否收到过以下异常?

收藏已修改;枚举操作可能无法执行。(无效操作异常)

集合被修改异常

基本上造成这种情况的原因是,您在一个线程上获得了集合,而其他人在另一个线程上修改了集合。

为了规避这个问题,我养成了一个习惯,在枚举之前拍摄集合的快照,方法是将集合转换为方法内的数组,如下所示:

static void LookAtCollection(IEnumerable<int> collection)
{   
    foreach (int Value in collection.ToArray() /* take a snapshot by converting to an array */)
    {
        Thread.Sleep(ITEM_DELAY_MS);
    }
}

我的问题是这个。将数组编码而不是可枚举作为一般规则不是更好吗,即使这意味着调用者现在在使用您的 API 时被迫将其集合转换为数组?

这不是更清洁,更防弹吗?(参数类型改为数组)

static void LookAtCollection(int[] collection)
{   
    foreach (int Value in collection)
    {
        Thread.Sleep(ITEM_DELAY_MS);
    }
}

该数组满足可枚举和固定长度的所有要求,并且调用者知道您将在该时间点对集合的快照进行操作,这可以节省更多错误。

我现在能找到的唯一更好的选择是 IReadOnlyCollection ,它将更加防弹,因为该集合在项目内容方面也是只读的。

编辑:

@DanielPryden 提供了一个非常好的文章“数组被认为有些有害”的链接。作者的评论“我很少需要一个集合,它具有完全可变的相当矛盾的特性,同时大小固定”“几乎在每种情况下,都有一个比大批。” 有点说服我,阵列并不像我希望的那样接近银弹,我同意风险和漏洞。我认为现在IReadOnlyCollection<T>接口比数组IEnumerable<T>IReadOnlyCollection<T>方法声明中的参数类型?还是应该允许调用者决定IEnumerable<T>他将什么实现传递给查看集合的方法?

感谢所有的答案。我从这些回复中学到了很多。

4

5 回答 5

4

除非另有明确说明,否则大多数方法都不是线程安全的。

在这种情况下,我会说由调用者负责线程安全。如果调用者将IEnumerable<T>参数传递给方法,那么如果方法枚举了它,他应该不会感到惊讶!如果枚举不是线程安全的,调用者需要以线程安全的方式创建快照,并传入快照。

被调用者不能通过调用ToArray()or来做到这一点ToList()——这些方法还将枚举集合,并且被调用者不知道需要什么(例如锁)以线程安全的方式制作快照。

于 2013-02-20T12:57:56.270 回答
4

这里真正的“问题”是,虽然T[]可能是一个比理想参数类型更具体的参数类型,并且允许接收者自由访问写入任何元素(这可能是可取的,也可能不是可取的),但IEnumerable<T>过于笼统。从类型安全的角度来看,调用者可以提供对任何实现IEnumerable<T>.

ifT是一个引用类型,aT[]本质上对于读、写和枚举都是线程安全的,受某些条件的限制(例如,访问不同元素的线程根本不会干扰;如果一个线程在另一个线程读取或枚举它,后一个线程将看到旧数据或新数据。微软的集合接口都没有提供任何此类保证,也没有提供任何方法来指示集合可以或不能做出什么保证。

我的倾向是要么使用T[],要么定义一个IThreadSafeList<T> where T:class包含CompareExchangeItem允许在项目上的成员的成员,Interlocked.CompareExchange但不包括不能以线程安全方式完成的事情。InsertRemove

于 2013-02-20T21:48:32.293 回答
2

此问题并非特定于线程。尝试在显式或隐式调用的循环中修改集合GetEnumerator。结果是一样的。这是非法操作。

于 2013-02-20T12:13:18.617 回答
1

绝不应允许线程访问与其他线程相同的对象。您应该始终复制变量/对象/数组并在线程空闲时解析它们。

完成后,它应该回调主线程,以便线程可以更新当前值/合并值。

如果两个物体可以同时到达同一个物体,你会得到或拔河,或异常或奇怪的行为。

想象一下这样的线程:你有老板和两名员工。老板将手中的信复印成复印件,交给员工。每个员工都有自己的任务,一个是收集清单上的所有东西,另一个是检查清单上的物品是否需要从供应商那里订购。他们每个人都进入自己的私人房间,老板只是把纸放在桌子上,他有时会检查它,告诉别人此刻有多少。

一名员工将订购的物品返回并交给主管,主管将其传递给后勤部门。一小时后,两名员工带着一份更新的库存清单返回。导演扔掉旧名单,用新名单代替。

现在是一个列表的场景。

老板给出了同样的指示。员工一拿到清单并开始致电第一个供应商。员工 2 拿到清单并开始挑选物品。主管想查看清单,但清单不见了,整个销售漏斗陷入停顿,因为没有准确的信息,因为清单已被占用。第一位员工已打完电话并想要更新他的列表。他不能,因为列表被占用了,他不能继续下一个项目,因为列表已经消失了,所以他一直在玩弄他的拇指或大发雷霆。当员工 2 准备好他的清单时,员工拿起清单并试图完成呼叫订单状态,但主管冲进来并拿走清单并将信息传递给销售团队。

这就是为什么您总是需要在线程中使用独立副本,除非您只知道该线程专门需要访问该资源,因为您永远不知道该特定线程何时对该资源执行某些操作,因为您不控制 CPU 计时.

我希望这个能有一点帮助。

于 2013-02-20T12:22:06.317 回答
0

背后的想法之一IEnumerable是它是LINQ查询的结果。由于查询而不是声明而不是执行代码,因此IEnumerable仅在迭代foreach或将项目复制到一个Array或其他结构时调用结果。现在,如果我们想将结果存储在像 an 这样的静态结构中Array,我们必须在迭代之前对其进行更新,IEnumerable否则我们将收到过期数据。IEnumerable.ToArray意思是:更新集合并制作它的静态副本。所以我的回答是,如果我们必须在对集合进行操作之前进行更新,那就IEnumerable更简洁了。在其他场景中我们可以使用Array以简化代码。

于 2013-02-20T12:24:04.740 回答