IEnumerable不保证枚举两次会产生相同的结果。实际上,创建一个myEnumerable.First()
执行两次时返回不同值的示例非常容易:
class A {
public A(string value) { Value = value; }
public string Value {get; set; }
}
static IEnumerable<A> getFixedIEnumerable() {
return new A[] { new A("Hello"), new A("World") };
}
static IEnumerable<A> getDynamicIEnumerable() {
yield return new A("Hello");
yield return new A("World");
}
static void Main(string[] args)
{
IEnumerable<A> fix = getFixedIEnumerable();
IEnumerable<A> dyn = getDynamicIEnumerable();
Console.WriteLine(fix.First() == fix.First()); // true
Console.WriteLine(dyn.First() == dyn.First()); // false
}
这不仅仅是一个学术性的例子:使用流行的from ... in ... select new A(...)
会产生这种情况。这可能会导致意外行为:
fix.First().Value = "NEW";
Console.WriteLine(fix.First().Value); // prints NEW
dyn.First().Value = "NEW";
Console.WriteLine(dyn.First().Value); // prints Hello
我理解为什么会发生这种情况。我也知道这可以通过在Enumerable上执行ToList()
或通过覆盖==
类A
来解决。这不是我的问题。
问题是:当你编写一个采用任意IEnumerable的方法并且你想要一次只评估序列的属性(然后引用是&#34;固定&#34;)时,是什么?这样做的规范方法? ToList()
似乎主要用于,但如果源已经修复(例如,如果源是一个数组),引用将被复制到一个列表(不必要的,因为我需要的只是固定属性)。是否有更合适的东西或ToList()
&#34;规范&#34;这个问题的解决方案?
答案 0 :(得分:3)
ToList
绝对是最佳选择。
它有一些优化:如果输入枚举是ICollection
(例如数组或列表),它将调用ICollection.CopyTo
将集合复制到数组而不是实际枚举 - 所以成本是除非收藏量很大,否则不大可能是重要的。
恕我直言,一般情况下,大多数方法最好返回ICollection<T>
或IList<T>
而不是IEnumerable<T>
,,除非要提示给消费者实现可以使用延迟评估(例如,产量)的方法。
如果方法应返回不可变列表,则返回只读包装器(ReadOnlyCollection<T>
),例如通过调用ToList().AsReadOnly()
,但仍返回接口类型IList<T>
。
如果您遵循此指南,该方法的消费者将不需要对ToList
进行不必要的调用。
答案 1 :(得分:2)
你的意思是你的函数是否需要参数并且你想确保参数不是惰性的,因为你需要在方法中多次迭代它?在这种情况下,我通常会将参数设置为ICollection
,如果它是惰性的,则调用者可以负责调整枚举。
答案 2 :(得分:2)
IEnumerable
接口只是给出一种迭代元素的“方法”(从linq查询创建IEnumerable是一个很好的例子,因为IEnumerable
就像数据库的SQL查询一样)。在将结果存储到ICollection
(ToList,ToArray)之前,它始终是动态的,因此迭代将被处理直到结束并且结果以“固定”方式存储。
答案 3 :(得分:1)
Joel Spolsky在他的Law of Leaky Abstractions中说出了最常见的问题。在您的方法中使用IEnumerable
参数,您期望一个只能枚举的对象,仅此而已。但是,无论是“固定”还是“动态”,你都会关注传递的集合。您可以看到IEnumerable
所做的抽象被泄露了。没有适合所有情况的解决方案。在您的情况下,您可以传递List<T>
或T[]
(或您期望作为方法中参数的实际类型的其他类型),而不是IEnumerable
。最常见的建议是实现抽象并根据它设计代码。
答案 4 :(得分:0)
从你解释问题的方式来看,似乎你想多次枚举一个序列并且每次都得到相同的顺序。这意味着您要么保留对序列的引用以便稍后执行迭代,要么您在同一方法中多次进行比较。
一般来说,当接受一个IEnumerable<T>
意味着不止一次地引用或操纵时,将序列复制到内部表示会更安全。我的偏好是使用ToList()
扩展名,因为它非常容易理解,可能是最简单的可预测排序的集合。
在内部,只要需要“常规”集合,.NET Framework就会使用List<T>
。