处理可能的IEnumerable多个枚举的警告


358

因此在我的代码中需要使用IEnumerable<>几次,从而得到“可能的多重枚举IEnumerable” 的Resharper错误。

样例代码:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null || !objects.Any())
        throw new ArgumentException();

    var firstObject = objects.First();
    var list = DoSomeThing(firstObject);        
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}
  • 我可以将objects参数更改为List,然后避免可能的多重枚举,但随后我没有得到可以处理的最高对象。
  • 另一件事,我可以做的就是将转换IEnumerableList在方法的开头:

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

但是,这很尴尬

在这种情况下您会做什么?

Answers:


470

IEnumerable使用参数作为问题是它告诉调用者“我希望枚举”。它不会告诉他们您希望枚举多少次。

我可以将objects参数更改为List,然后避免可能的多重枚举,但是这样我并没有得到能处理的最高对象

追求最高目标的目标是崇高的,但它为太多的假设留有余地。您是否真的要有人将LINQ to SQL查询传递给此方法,只让您枚举两次(每次都可能得到不同的结果?)。

此处缺少的语义是,调用者可能不花时间来读取方法的详细信息,因此可能会假设您仅迭代一次-因此,它们将为您传递一个昂贵的对象。您的方法签名不指示任何一种方式。

通过将方法签名更改为IList/ ICollection,至少可以使调用者更清楚自己的期望,并且它们可以避免代价高昂的错误。

否则,大多数使用该方法的开发人员可能会假设您仅迭代一次。如果采用IEnumerable是如此重要,则应.ToList()在方法开始时考虑进行。

真可惜.NET没有IEnumerable + Count + Indexer接口,没有添加/删除等方法,这是我怀疑可以解决此问题的方法。


31
ReadOnlyCollection <T>是否满足您所需的接口要求? msdn.microsoft.com/en-us/library/ms132474.aspx
Dan在Firelight面前摆弄

73
@DanNeely我建议IReadOnlyCollection(T)(.net 4.5新增)作为最好的接口,以传达它是一个IEnumerable(T)打算多次枚举的想法。正如该答案所指出的那样,IEnumerable(T)它本身是如此通用,以至于它甚至可能引用无法重置的内容,而这些内容又不会产生副作用。但是IReadOnlyCollection(T)暗示可重复性。
binki 2013年

1
如果.ToList()在方法开始时执行,则首先必须检查参数是否不为null。这意味着您将在两个地方引发ArgumentException:如果参数为null且没有任何项目。
comecme 2015年

我阅读了有关vs 语义的这篇文章,然后发布有关用作参数以及在哪种情况下使用的问题。我认为我最终得出的结论是可以遵循的逻辑。应该在需要枚举的地方使用它,不必在可能存在多个枚举的地方使用它。IReadOnlyCollection<T>IEnumerable<T>IReadOnlyCollection<T>
Neo

32

如果您的数据总是可以重复的,也许不用担心。但是,您也可以将其展开-如果传入数据可能很大(例如,从磁盘/网络读取),这将特别有用:

if(objects == null) throw new ArgumentException();
using(var iter = objects.GetEnumerator()) {
    if(!iter.MoveNext()) throw new ArgumentException();

    var firstObject = iter.Current;
    var list = DoSomeThing(firstObject);  

    while(iter.MoveNext()) {
        list.Add(DoSomeThingElse(iter.Current));
    }
    return list;
}

注意,我稍微改变了DoSomethingElse的语义,但这主要是为了展示展开的用法。例如,您可以重新包装迭代器。您也可以使其成为迭代器块,这可能很好。那么就没有了list-您将yield return在获得项目时将其保留,而不是添加到要返回的列表中。


4
+1好吧,这是一个非常不错的代码,但是-我失去了代码的美感和可读性。
gdoron在2011

5
@gdoron并非一切都很漂亮; p我不是上面的东西不可读。尤其是将其制成迭代器块时,IMO非常可爱。
马克·格雷韦尔

我可以这样写:“ var objectList = objects.ToList();” 并保存所有枚举器处理。
gdoron在2011

10
@gdoron在您明确表示您确实不想这样做的问题中; p此外,如果数据很大(可能是无限的,则序列不必是有限的),则ToList()方法可能不适合。仍然可以迭代)。最终,我只是提出一个选择。只有您知道完整的上下文,才能了解是否有必要。如果您的数据是可重复的,则另一个有效的选择是:不要更改任何内容!而是忽略R#...这完全取决于上下文。
马克·格拉韦尔

1
我认为Marc的解决方案相当不错。1.非常清楚“列表”是如何初始化的,非常清楚列表是如何迭代的,并且很清楚对列表的哪些成员进行了操作。
基思·霍夫曼

8

在方法签名中使用IReadOnlyCollection<T>IReadOnlyList<T>来代替IEnumerable<T>,其优点是可以明确表明您可能需要在进行迭代之前检查计数,或出于某些其他原因而进行多次迭代。

但是,它们有很大的缺点,如果您尝试将代码重构为使用接口,例如会使其对动态代理更具可测试性和友好性,则会导致问题。关键点是它IList<T>不继承自IReadOnlyList<T>,并且对于其他集合及其各自的只读接口也类似。(简而言之,这是因为.NET 4.5希望保持与早期版本的ABI兼容性。但是他们甚至没有抓住机会在.NET Core中进行更改。

这意味着,如果您IList<T>从程序的某个部分得到一个并将其传递给期望一个的另一部分IReadOnlyList<T>,那么您将无法!不过,您可以将传递IList<T>IEnumerable<T>

最后,IEnumerable<T>是所有.NET集合(包括所有集合接口)支持的唯一只读接口。当您意识到自己将自己锁定在某些架构选择之外时,任何其他选择都会再次吸引您。因此,我认为在函数签名中使用它来表达您只想要一个只读集合是正确的类型。

(请注意,IReadOnlyList<T> ToReadOnly<T>(this IList<T> list)如果基础类型同时支持两个接口,则始终可以编写一个简单的强制转换方法,但是在重构时,IEnumerable<T>必须始终在任意位置手动添加扩展方法,因为它们始终是兼容的。)

与往常一样,这不是绝对的,如果您正在编写大量数据库的代码,而意外的多次枚举将成为灾难,那么您可能会希望采用其他折衷方法。


2
+1对于使用.NET平台提供的新功能(即IReadOnlyCollection <T>)来解决旧问题并添加有关解决此问题的新方法的文档
凯文·阿维尼翁

4

如果目标是防止多个枚举,则可以阅读Marc Gravell的答案,但要保持相同的语义,您可以简单地删除多余的内容Any并进行First调用:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null)
        throw new ArgumentNullException("objects");

    var first = objects.FirstOrDefault();

    if (first == null)
        throw new ArgumentException(
            "Empty enumerable not supported.", 
            "objects");

    var list = DoSomeThing(first);  

    var secondList = DoSomeThingElse(objects);

    list.AddRange(secondList);

    return list;
}

注意,这假设您IEnumerable不是通用的,或者至少被限制为引用类型。


2
objects仍传递给DoSomethingElse,该函数将返回一个列表,因此仍然存在您两次枚举的可能性。无论哪种方式,如果集合是LINQ to SQL查询,您都会两次击中数据库。
Paul Stovell,

1
@PaulStovell,请阅读以下内容:“如果要真正防止多重枚举,那要比Marc Gravell的答案
更有意义

在其他一些关于为空列表引发异常的stackoverflow帖子中已经提出了建议,我将在这里提出建议:为什么在一个空列表引发异常?获取一个空列表,给一个空列表。作为范例,这很简单。如果呼叫者不知道他们要传入一个空列表,那就是问题所在。
基思·霍夫曼

@Keith Hoffman,示例代码与OP发布的代码保持相同的行为,在其中使用First会抛出一个空列表异常。我认为不可能说永远不要在空列表上抛出异常或总是在空列表上抛出异常。这取决于...
若昂·安杰洛

够公平的乔奥。更多值得思考的东西,而不是批评您的帖子。
基思·霍夫曼

3

在这种情况下,我通常用IEnumerable和IList重载我的方法。

public static IEnumerable<T> Method<T>( this IList<T> source ){... }

public static IEnumerable<T> Method<T>( this IEnumerable<T> source )
{
    /*input checks on source parameter here*/
    return Method( source.ToList() );
}

我会在方法的摘要注释中说明调用IEnumerable将执行.ToList()的方法。

如果要串联多个操作,程序员可以选择更高级别的.ToList(),然后调用IList重载或让我的IEnumerable重载来解决。


20
太恐怖了
Mike Chamberlain

1
@MikeChamberlain是的,真的很恐怖,而且完全干净。不幸的是,一些繁重的场景需要这种方法。
Mauro Sampietro

1
但是,每次将其传递给IEnumerable parameratized方法时,您将重新创建该数组。在这方面,我同意@MikeChamberlain的观点,这是一个可怕的建议。
Krythic

2
@Krythic如果不需要重新创建列表,请在其他位置执行ToList并使用IList重载。这就是解决方案的重点。
Mauro Sampietro

0

如果只需要检查第一个元素,则可以在不迭代整个集合的情况下窥视它:

public List<object> Foo(IEnumerable<object> objects)
{
    object firstObject;
    if (objects == null || !TryPeek(ref objects, out firstObject))
        throw new ArgumentException();

    var list = DoSomeThing(firstObject);
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}

public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    IEnumerator<T> enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        first = default(T);
        source = Enumerable.Empty<T>();
        return false;
    }

    first = enumerator.Current;
    T firstElement = first;
    source = Iterate();
    return true;

    IEnumerable<T> Iterate()
    {
        yield return firstElement;
        using (enumerator)
        {
            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
            }
        }
    }
}

-3

首先,该警告并不总是那么重要。在确定它不是性能瓶颈之后,通常会禁用它。这仅表示IEnumerable两次对它进行了评估,除非evaluation本身花费很长时间,否则通常这不是问题。即使花费很长时间,在这种情况下,您还是第一次只使用一个元素。

在这种情况下,您还可以利用功能强大的linq扩展方法。

var firstObject = objects.First();
return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList();

IEnumerable在这种情况下,可能只评估一次就有些麻烦了,但首先进行分析,看看是否确实有问题。


15
我与IQueryable进行了大量合作,因此关闭此类警告非常危险。
gdoron在2011

5
在我的书中,两次评估是一件坏事。如果该方法采用IEnumerable,我可能会尝试向它传递LINQ to SQL查询,这将导致多个数据库调用。由于方法签名并不表示它可以枚举两次,因此它应该更改签名(接受IList / ICollection)或仅重复一次
Paul Stovell,

4
提前优化,破坏可读性,从而可能做优化,做一个diffrence后来是在我的书的方式更糟。
vidstige

当您进行数据库查询时,确保只对它进行一次评估可以了。这不仅是理论上的未来收益,而且现在是可衡量的实际收益。不仅如此,仅进行一次评估不仅对性能有所帮助,而且对于正确性也是必需的。两次执行数据库查询可能会产生不同的结果,因为有人可能在两次查询评估之间发出了更新命令。

1
@hvd我不推荐没有这种东西。当然,您不应该枚举IEnumerables每次迭代都会产生不同的结果,或者迭代会花费很长时间。参见马克·格雷夫尔斯(Marc Gravells)的答案-他还首先声明“也许不用担心”。问题是-很多次您看到此内容后,都无需担心。
vidstige,2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.