【问题标题】:"Possible multiple enumeration of IEnumerable" vs "Parameter can be declared with base type"“IEnumerable 的可能多重枚举”与“参数可以用基类型声明”
【发布时间】:2011-10-09 12:16:07
【问题描述】:

在 Resharper 5 中,以下代码导致 list 的警告“参数可以使用基本类型声明”:

public void DoSomething(List<string> list)
{
    if (list.Any())
    {
        // ...
    }
    foreach (var item in list)
    {
        // ...
    }
}

在 Resharper 6 中,情况并非如此。但是,如果我将方法更改为以下内容,我仍然会收到该警告:

public void DoSomething(List<string> list)
{
    foreach (var item in list)
    {
        // ...
    }
}

原因是,在这个版本中,list 只枚举了一次,所以将其更改为IEnumerable&lt;string&gt; 不会自动引入另一个警告。 现在,如果我手动更改第一个版本以使用IEnumerable&lt;string&gt; 而不是List&lt;string&gt;,我将在方法主体中list 的两次出现时收到该警告(“IEnumerable 的可能多重枚举”):

public void DoSomething(IEnumerable<string> list)
{
    if (list.Any()) // <- here
    {
        // ...
    }
    foreach (var item in list) // <- and here
    {
        // ...
    }
}

我明白,为什么,但我想知道如何解决这个警告,假设该方法真的只需要 IEnumerable&lt;T&gt; 而不是 List&lt;T&gt;,因为我只想枚举项目而我不想要更改列表。
在方法的开头添加list = list.ToList(); 会使警告消失:

public void DoSomething(IEnumerable<string> list)
{
    list = list.ToList();
    if (list.Any())
    {
        // ...
    }
    foreach (var item in list)
    {
        // ...
    }
}

我明白,为什么这会使警告消失,但对我来说它看起来有点像黑客......
有什么建议,如何更好地解决该警告并在方法签名中仍然使用最通用的类​​型?
好的解决方案应该解决以下问题:

  1. 不要在方法内部调用ToList(),因为它会影响性能
  2. 没有使用ICollection&lt;T&gt; 甚至更专业的接口/类,因为它们会改变从调用者看到的方法的语义。
  3. IEnumerable&lt;T&gt; 上没有多次迭代,因此存在多次访问数据库或类似情况的风险。

注意:我知道这不是 Resharper 问题,因此,我不想取消此警告,但要解决根本原因,因为警告是合法的。

更新: 请不要关心Any 和foreach。我不需要帮助将这些语句合并为只有一个可枚举的枚举。
在这种方法中,它真的可以是多次枚举可枚举的任何东西!

【问题讨论】:

    标签: c# .net resharper resharper-6.0


    【解决方案1】:

    您可能应该接受IEnumerable&lt;T&gt; 并忽略“多次迭代”警告。

    此消息警告您,如果将惰性可枚举(例如迭代器或代价高昂的 LINQ 查询)传递给您的方法,部分迭代器将执行两次。

    【讨论】:

    • 这是否意味着遵循 Resharper 5 的建议可能会使您处于潜在的不安全位置(如果您传入“惰性可枚举”或其他什么)?
    • 正如我所说,我知道为什么会出现这些警告,并且我认为它们是合法的。如果有人真的将 LINQ2SQL 查询传递给该方法,我将执行两次。我不知道,人们传入了什么,所以这是一种可能。
    • @kekekela:不是不安全,只是不是最优的。
    • @Daniel:那么您可以使用ICollection&lt;T&gt; 并检查Count 而不是调用Any()。但是,这会不必要地减慢需要传递简单 LINQ(对象)查询的调用者的速度。
    • @SLaks:由于我只想枚举项目并且不想更改集合,因此将参数更改为ICollection&lt;T&gt; 不是正确的做法,因为它为调用者提供了印象我的方法可能会改变集合。
    【解决方案2】:

    您可以使用ICollection&lt;T&gt;(或IList&lt;T&gt;)。它不如List&lt;T&gt; 具体,但不会受到多重枚举问题的影响。

    在这种情况下,我仍然倾向于使用IEnumerable&lt;T&gt;。您也可以考虑重构代码以仅枚举一次。

    【讨论】:

    • 这不是一个好主意。两者都允许方法更改内容。调用者不能确定其可枚举的内容没有改变。但是,使用IEnumerable&lt;T&gt; 作为参数类型清楚地表明该方法不想更改内容...
    【解决方案3】:

    使用 IList 作为参数类型而不是 IEnumerable - IEnumerable 与 List 具有不同的语义,而 IList 具有相同的语义

    IEnumerable 可能基于不可搜索的流,这就是您收到警告的原因

    【讨论】:

    • 这就是我不想拥有List 或IList 的原因。语义错误。
    【解决方案4】:

    在你的方法中接受枚举时要小心。基本类型的“警告”只是一个提示,枚举警告是真正的警告。

    但是,您的列表将至少被枚举两次,因为您先执行任何操作,然后再执行一次 foreach。如果您添加 ToList() 您的枚举将被枚举 3 次 - 删除 ToList()。

    我建议将基类型的 resharpers 警告设置设置为提示。所以你仍然有一个提示(绿色下划线)和快速修复它的可能性(alt+enter)并且你的文件中没有“警告”。

    如果枚举 IEnumerable 是一项昂贵的操作(例如从文件或数据库加载某些内容),或者如果您有一个计算值并使用收益返回的方法,您应该小心。在这种情况下,首先执行 ToList() 或 ToArray() 以仅加载/计算所有数据一次。

    【讨论】:

    • 以前没有人说过的话。 Any() 已经迭代尝试查找元素。如果您调用 ToList(),它也会迭代以创建一个列表。使用 IEnumerable 的最初想法只是进行迭代,其他任何它都会引发迭代来执行。
    【解决方案5】:

    你只能迭代一次:

    public void DoSomething(IEnumerable<string> list)
    {
        bool isFirstItem = true;
        foreach (var item in list)
        {
            if (isFirstItem)
            {
                isFirstItem = false;
                // ...
            }
            // ...
        }
    }
    

    【讨论】:

    • 在我的具体情况下你是对的。但是这个解决方案不能用于所有情况。我想要一个通用的方法。
    • 我不是在为我的特定样本寻找解决方案。请查看我的问题的更新(最后的粗体部分)...
    【解决方案6】:

    以前没有人说过的话(@Zebi)。 Any() 已经迭代尝试查找元素。如果您调用 ToList(),它也会迭代以创建一个列表。使用 IEnumerable 的最初想法只是进行迭代,其他任何事情都会引发迭代以执行。您应该尝试在一个循环中完成所有操作。

    并在其中包含您的 .Any() 方法。

    如果你在你的方法中传递一个 Action 列表,你将有一个更清晰的迭代代码

    public void DoSomething(IEnumerable<string> list, params Action<string>[] actions)
    {
        foreach (var item in list)
        {
            for(int i =0; i < actions.Count; i++)
            {
               actions[i](item);
            }
        }
    }
    

    【讨论】:

    • 我不是在为我的特定样本寻找解决方案。请查看我的问题的更新(最后的粗体部分)...
    【解决方案7】:

    为什么不:

    bool any;
    
    foreach (var item in list)
    {
        any = true;
        // ...
    }
    if(any)
    {
        //...
    }
    

    更新:就我个人而言,我不会为了绕过这样的警告而彻底更改代码。我会禁用警告并继续。该警告建议您更改代码的一般流程以使其更好;如果您没有使代码变得更好(并且可以说使代码变得更糟)来解决警告;那么就错过了警告的重点。

    例如:

    // ReSharper disable PossibleMultipleEnumeration
            public void DoSomething(IEnumerable<string> list)
            {
                if (list.Any()) // <- here
                {
                    // ...
                }
                foreach (var item in list) // <- and here
                {
                    // ...
                }
            }
    // ReSharper restore PossibleMultipleEnumeration
    

    【讨论】:

    • 我不是在为我的特定样本寻找解决方案。请查看我的问题的更新(最后的粗体部分)...
    • @Peter Ritchie 在哪里可以找到 ReSharper 警告的名称以禁用它们? ReSharper 选项中是否有某处,或某处的列表?
    • @RJCuthbertson 在 Alt+Enter 菜单中有一个选项可以“使用评论禁用一次”
    【解决方案8】:

    没有完美的解决方案,根据情况选择一个。

    • enumerable.ToList,只要不修改列表,可以先尝试“enumerable as List”进行优化
    • 在 IEnumerable 上迭代两次,但让调用者清楚(记录)
    • 分成两种方法
    • 获取 List 以避免“as”/ToList 的成本和双重枚举的潜在成本

    第一个解决方案(ToList)对于可以在任何 Enumerable 上工作的公共方法来说可能是最“正确”的。

    您可以忽略 Resharper 问题,警告在一般情况下是合法的,但在您的具体情况下可能是错误的。特别是如果该方法供内部使用并且您可以完全控制调用者。

    【讨论】:

      【解决方案9】:

      UIMS* - 从根本上说,没有很好的解决方案。 IEnumerable 曾经是“非常基本的东西,代表一堆相同类型的东西,所以在方法 sigs 中使用它是正确的”。它现在也变成了“可能在幕后评估的事情,可能需要一段时间,所以现在你总是要担心这个。”

      就好像 IDictionary 突然扩展为支持延迟加载值,通过 Func 类型的 LazyLoader 属性。实际上,这很好用,但添加到 IDictionary 中就不那么整洁了,因为现在每次我们收到一个 IDictionary 时,我们都必须担心这一点。但这就是我们所处的位置。

      因此,“如果一个方法采用 IEnumerable 并对其进行两次评估,则始终通过 ToList() 强制评估”似乎是您能做的最好的事情。 Jetbrains 的出色工作给了我们这个警告。

      *(除非我遗漏了什么……只是编的,但看起来很有用)

      【讨论】:

        【解决方案10】:

        存在解决 Resharper 警告的通用解决方案:IEnumerable 的可重复性缺乏保证,以及 List 基类(或可能成本高昂的 ToList() 解决方法)。

        创建一个专门的类,即“RepeatableEnumerable”,实现 IEnumerable,并使用以下逻辑大纲实现“GetEnumerator()”:

        1. 从内部列表中获取到目前为止已收集的所有项目。

        2. 如果被包装的枚举器有更多的项目,

          • 虽然被包装的枚举器可以移动到下一项,

            1. 从内部枚举器中获取当前项。

            2. 将当前项添加到内部列表中。

            3. 输出当前项目

        3. 将内部枚举器标记为没有更多项目。

        在包装参数已经可重复的地方添加扩展方法和适当的优化。 Resharper 将不再在以下代码中标记指示的警告:

        public void DoSomething(IEnumerable<string> list)
        {
            var repeatable = list.ToRepeatableEnumeration();
            if (repeatable.Any()) // <- no warning here anymore.
              // Further, this will read at most one item from list.  A
              // query (SQL LINQ) with a 10,000 items, returning one item per second
              // will pass this block in 1 second, unlike the ToList() solution / hack.
            {
                // ...
            }
        
            foreach (var item in repeatable) // <- and no warning here anymore, either.
              // Further, this will read in lazy fashion.  In the 10,000 item, one 
              // per second, query scenario, this loop will process the first item immediately
              // (because it was read already for Any() above), and then proceed to
              // process one item every second.
            {
                // ...
            }
        }
        

        通过一点工作,您还可以将RepeatableEnumerable 变成LazyList,这是IList 的完整实现。不过,这超出了这个特定问题的范围。 :)

        更新:cmets 中请求的代码实现——不知道为什么原始 PDL 还不够,但无论如何,以下忠实地实现了我建议的算法(我自己的实现实现了完整的 IList 接口;这有点超出了我想在这里发布的范围... :) )

        public class RepeatableEnumerable<T> : IEnumerable<T>
        {
            readonly List<T> innerList;
            IEnumerator<T> innerEnumerator;
        
            public RepeatableEnumerable( IEnumerator<T> innerEnumerator )
            {
                this.innerList = new List<T>();
                this.innerEnumerator = innerEnumerator;
            }
        
            public IEnumerator<T> GetEnumerator()
            {
                // 1. Yield all items already collected so far from the inner list.
                foreach( var item in innerList ) yield return item;
        
                // 2. If the wrapped enumerator has more items
                if( innerEnumerator != null )
                {
                    // 2A. while the wrapped enumerator can move to the next item
                    while( innerEnumerator.MoveNext() )
                    {
                        // 1. Get the current item from the inner enumerator.
                        var item = innerEnumerator.Current;
                        // 2. Add the current item to the inner list.
                        innerList.Add( item );
                        // 3. Yield the current item
                        yield return item;
                    }
        
                    // 3. Mark the inner enumerator as having no more items.
                    innerEnumerator.Dispose();
                    innerEnumerator = null;
                }
            }
        
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }
        
        // Add extension methods and appropriate optimizations where the wrapped parameter is already repeatable.
        public static class RepeatableEnumerableExtensions
        {
            public static RepeatableEnumerable<T> ToRepeatableEnumerable<T>( this IEnumerable<T> items )
            {
                var result = ( items as RepeatableEnumerable<T> )
                    ?? new RepeatableEnumerable<T>( items.GetEnumerator() );
                return result;
            }
        }
        

        【讨论】:

        • 不错的答案和好主意。但是有一个很大的缺点:OrderBy 的可重复枚举不一定按照 OrderBy 定义的顺序返回项目。考虑以下情况:var items = new List&lt;int&gt; { 5, 7, 3 }; var enumerable = items.OrderBy(x =&gt; x); var repeatable = enumerable.AsRepeatable(); repeatable.Any(); list.Add(1); foreach(var item in repeatable) Console.WriteLine(item); 结果将是3, 1, 5, 7。而不是1, 3, 5, 7。
        • 我在我的实现中添加了一个单元测试。按预期工作;用 Assert.IsTrue( new[]{ 3, 5, 7 }.SequenceEqual( repeatable ) ); 替换了最后一行您是否真的看到了这种情况下的意外行为?另请注意,在枚举时更改集合并没有明确定义;例如,许多集合会抛出“集合已修改”异常。
        • 不,我没有看到这种行为,但从您的描述来看,听起来应该是这样的。您能分享一下您的实现吗?
        【解决方案11】:

        此类将为您提供一种方法,将第一项从枚举中分离出来,然后为枚举的其余部分使用 IEnumerable,而无需给您双重枚举,从而避免潜在的令人讨厌的性能损失。它的用法是这样的(其中 T 是您要枚举的任何类型):

        var split = new SplitFirstEnumerable(currentIEnumerable);
        T firstItem = split.First;
        IEnumerable<T> remaining = split.Remaining;
        

        这是类本身:

        /// <summary>
        /// Use this class when you want to pull the first item off of an IEnumerable
        /// and then enumerate over the remaining elements and you want to avoid the
        /// warning about "possible double iteration of IEnumerable" AND without constructing
        /// a list or other duplicate data structure of the enumerable. You construct 
        /// this class from your existing IEnumerable and then use its First and 
        /// Remaining properties for your algorithm.
        /// </summary>
        /// <typeparam name="T">The type of item you are iterating over; there are no
        /// "where" restrictions on this type.</typeparam>
        public class SplitFirstEnumerable<T>
        {
            private readonly IEnumerator<T> _enumerator;
        
            /// <summary>
            /// Constructor
            /// </summary>
            /// <remarks>Will throw an exception if there are zero items in enumerable or 
            /// if the enumerable is already advanced past the last element.</remarks>
            /// <param name="enumerable">The enumerable that you want to split</param>
            public SplitFirstEnumerable(IEnumerable<T> enumerable)
            {
                _enumerator = enumerable.GetEnumerator();
                if (_enumerator.MoveNext())
                {
                    First = _enumerator.Current;
                }
                else
                {
                    throw new ArgumentException("Parameter 'enumerable' must have at least 1 element to be split.");
                }
            }
        
            /// <summary>
            /// The first item of the original enumeration, equivalent to calling
            /// enumerable.First().
            /// </summary>
            public T First { get; private set; }
        
            /// <summary>
            /// The items of the original enumeration minus the first, equivalent to calling
            /// enumerable.Skip(1).
            /// </summary>
            public IEnumerable<T> Remaining
            {
                get
                {
                    while (_enumerator.MoveNext())
                    {
                        yield return _enumerator.Current;
                    }
                }
            }
        }
        

        这确实假设 IEnumerable 至少有一个元素要开始。如果你想做更多的 FirstOrDefault 类型设置,你需要捕获否则会在构造函数中抛出的异常。

        【讨论】:

        • 好像你将无法枚举Remaining 两次。
        • 弗拉德:当然不是。目标是两个消除关于双重枚举的警告,而不是启用双重枚举。
        【解决方案12】:

        我意识到这个问题已经过时并且已经被标记为已回答,但我很惊讶没有人建议手动迭代枚举器:

        // NOTE: list is of type IEnumerable<T>.
        //       The name was taken from the OP's code.
        var enumerator = list.GetEnumerator();
        if (enumerator.MoveNext())
        {
            // Run your list.Any() logic here
            ...
        
            do
            {
                var item = enumerator.Current;
                // Run your foreach (var item in list) logic here
                ...
            } while (enumerator.MoveNext());
        }
        

        似乎比这里的其他答案更直接。

        【讨论】:

        • 切换到枚举器是可行的,但它破坏了许多使 IEnumerable 如此强大的函数组合。您不能在更深的嵌套代码中应用过滤器或其他 LINQ 语句。此外,您的调用者必须将列表传递给您,这意味着 a) 授予您修改列表的权限 b) 将您绑定到特定的实现(即 List 甚至 IList 对于未来的重构可能过于严格)。
        • @srm 我不明白你评论的第二部分,因为这个逻辑不需要调用者向我传递 List。这就是重点(根据 OP 的代码,上述代码中变量“list”的类型是 IEnumerable)。至于你的第一点,是的,这是正确的。在开始手动迭代列表后,您不能在其上调用任何 LINQ 表达式,否则您将进行多次迭代。启用此类代码的唯一方法是根据 T.Tobler 的回答缓存可枚举的结果,或使用 ToList()。但是,这超出了所提问题的范围。
        【解决方案13】:

        一般来说,您需要的是一些状态对象,您可以将项目推入其中(在 foreach 循环中),然后从中获得最终结果。

        可枚举 LINQ 运算符的缺点是它们会主动枚举源而不是接受推送给它们的项目,因此它们不符合您的要求。

        如果你例如只需要 1'000'000 个整数序列的最小值和最大值,这些整数需要花费 1'000 美元的处理器时间来检索,你最终会写出这样的东西:

        public class MinMaxAggregator
        {
            private bool _any;
            private int _min;
            private int _max;
        
            public void OnNext(int value)
            {
                if (!_any)
                {
                    _min = _max = value;
                    _any = true;
                }
                else
                {
                    if (value < _min) _min = value;
                    if (value > _max) _max = value;
                }
            }
        
            public MinMax GetResult()
            {
                if (!_any) throw new InvalidOperationException("Sequence contains no elements.");
                return new MinMax(_min, _max);
            }
        }
        
        public static MinMax DoSomething(IEnumerable<int> source)
        {
            var aggr = new MinMaxAggregator();
            foreach (var item in source) aggr.OnNext(item);
            return aggr.GetResult();
        }
        

        实际上,您只是重新实现了 Min() 和 Max() 运算符的逻辑。当然这很容易,但它们只是任意复杂逻辑的示例,否则您可能很容易以 LINQish 方式表达。

        昨天晚上散步时,我想到了解决方案:我们需要推动……那是反应!所有心爱的运算符也存在于为推送范例构建的反应式版本中。它们可以随意链接在一起,达到您需要的任何复杂程度,就像它们的可数对应物一样。

        所以最小/最大示例归结为:

        public static MinMax DoSomething(IEnumerable<int> source)
        {
            // bridge over to the observable world
            var connectable = source.ToObservable(Scheduler.Immediate).Publish();
            // express the desired result there (note: connectable is observed by multiple observers)
            var combined = connectable.Min().CombineLatest(connectable.Max(), (min, max) => new MinMax(min, max));
            // subscribe
            var resultAsync = combined.GetAwaiter();
            // unload the enumerable into connectable
            connectable.Connect();
            // pick up the result
            return resultAsync.GetResult();
        }
        

        【讨论】:

        • 好吧,OP(你,正如我现在注意到的)要求 1./2.不需要实现可枚举并且 3. 不会多次枚举它。
        • 确实,现在我明白了。这个答案太棒了! :-)
        猜你喜欢
        • 1970-01-01
        • 2014-06-13
        • 2010-12-28
        • 1970-01-01
        • 1970-01-01
        • 2012-01-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多