首页 文章

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

提问于
浏览
309

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

示例代码:

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 ,然后避免可能的多次枚举,但是我没有得到我能处理的最高对象 .

  • 我能做的另一件事是在方法开头将 IEnumerable 转换为 List


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

但这只是 awkward .

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

7 回答

  • 1

    IEnumerable 作为参数的问题在于它告诉呼叫者"I wish to enumerate this" . 它没有告诉他们你想要枚举多少次 .

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

    采取最高目标的目标是高尚的,但它为太多的假设留下了空间 . 你真的希望有人将LINQ to SQL查询传递给这个方法,只为你枚举它两次(每次得到可能不同的结果吗?)

    这里缺少的语义是,调用者可能没有花时间阅读方法的细节,可能会假设您只迭代一次 - 因此他们会传递给您一个昂贵的对象 . 您的方法签名不表示任何一种方式 .

    通过将方法签名更改为 IList / ICollection ,您至少可以使调用者更清楚您的期望是什么,并且可以避免代价高昂的错误 .

    否则,大多数查看该方法的开发人员可能会假设您只迭代一次 . 如果 IEnumerable 非常重要,您应该考虑在方法开始时执行 .ToList() .

    遗憾的是.NET没有IEnumerable Count Indexer的接口,没有Add / Remove等方法,这是我怀疑会解决这个问题的方法 .

  • 4

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

    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 项,而不是添加到要返回的列表中 .

  • 26

    在方法签名中使用 IReadOnlyCollection<T>IReadOnlyList<T> 而不是 IEnumerable<T> ,具有明确的优势,即您可能需要在迭代之前检查计数,或者由于某些其他原因而多次迭代 .

    但是,如果您尝试重构代码以使用接口,它们会产生很大的缺点,例如使其更易于测试并且对动态代理更友好 . 关键点是 IList<T> does not inherit from IReadOnlyList<T> ,类似于其他集合及其各自的只读接口 . (简而言之,这是因为.NET 4.5希望保持ABI与早期版本的兼容性.But they didn't even take the opportunity to change that in .NET core.

    这意味着如果你从程序的某个部分得到一个 IList<T> 并希望将它传递给另一个需要 IReadOnlyList<T> 的部分,你就不能! You can however pass an IList<T> as an IEnumerable<T> .

    最后, IEnumerable<T> 是所有.NET集合支持的唯一只读接口,包括所有集合接口 . 任何其他选择都会回来咬你,因为你意识到你把自己锁定在某些架构选择之外 . 所以我认为在函数签名中使用它是正确的类型来表示你只需要一个只读集合 .

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

    一如既往,这不是绝对的,如果你正在编写数据库密集的代码,意外的多次枚举将是一场灾难,你可能更喜欢不同的权衡 .

  • 414

    首先,这个警告并不总是意味着那么多 . 在确定它不是性能瓶颈后,我通常会禁用它 . 它只是意味着 IEnumerable 被评估两次,除非 evaluation 本身需要很长时间,否则通常不会有问题 . 即使它确实需要很长时间,在这种情况下,您第一次只使用一个元素 .

    在这种情况下,您还可以进一步利用强大的linq扩展方法 .

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

    在这种情况下,只能评估 IEnumerable 一次有一些麻烦,但首先查看配置文件并查看它是否确实存在问题 .

  • -2

    在这种情况下,我通常使用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重载处理它 .

  • 4

    如果目的是为了防止多次枚举而不是Marc Gravell的答案是读者,但保持相同的语义,你可以简单地删除多余的 AnyFirst 调用,并使用:

    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 不是通用的,或者至少被约束为引用类型 .

  • 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;
                }
            }
        }
    }
    

相关问题