首页 文章

“可能的多个枚举IEnumerable”vs“参数可以用基类型声明”

提问于
浏览
15

在Resharper 5中,以下代码导致 list 的警告"Parameter can be declared with base type":

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)
    {
        // ...
    }
}

原因是,在此版本中,列表仅枚举一次,因此将其更改为 IEnumerable<string> 将不会自动引入另一个警告 . 现在,如果我手动更改第一个版本以使用 IEnumerable<string> 而不是 List<string> ,我将在方法正文中的两次出现 list 上收到警告("Possible multiple enumeration of IEnumerable"):

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

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

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

我理解,为什么这会让警告消失,但它看起来有点像我的黑客......
任何建议,如何更好地解决该警告仍然使用方法签名中可能的最一般类型?
一个好的解决方案应该解决以下问题:

  • 方法内部没有调用 ToList() ,因为它会对性能产生影响

  • 不使用 ICollection<T> 或甚至更专业的接口/类,因为它们改变了从调用者看到的方法的语义 .

  • IEnumerable<T> 上没有多次迭代,因此有多次或类似地访问数据库的风险 .

注意:我知道这不是一个Resharper问题,因此,我不想压制这个警告,但是因为警告是合法的,所以要解决根本原因 .

UPDATE: 请不要关心 Anyforeach . 我不需要帮助合并这些语句只有一个可枚举的枚举 .
这个方法中的任何东西都可以枚举可枚举的多次!

13 回答

  • 5

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

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

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

    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方式轻松表达 .

    解决方案在昨天晚上散步时来到我身边:我们需要推......这是反应性的!所有心爱的运营商也存在于为推送范例构建的反应式版本中 . 它们可以随意链接到您需要的任何复杂程度,就像它们的可枚举对应物一样 .

    所以min / max示例归结为:

    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();
    }
    
  • 0

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

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

    这是 class 本身:

    /// <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类型设置,则需要捕获否则将在构造函数中抛出的异常 .

  • 4

    您应该采取 IEnumerable<T> 并忽略"multiple iterations"警告 .

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

  • 0

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

    • enumerable.ToList,只要不修改列表,首先尝试"enumerable as List"可以优化它

    • 在IEnumerable上迭代两次,但为调用者清楚(记录它)

    • 分为两种方法

    • 列表以避免"as" / ToList的成本和双重枚举的潜在成本

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

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

  • 4

    存在一种解决Resharper警告的一般解决方案:缺乏对IEnumerable的重复能力的保证,以及List基类(或可能昂贵的ToList()变通方法) .

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

    • 从内部列表中收集到目前为止已收集的所有项目 .

    • 如果包装的枚举器有更多的项目,

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

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

    • 将当前项添加到内部列表 .

    • 收益当前项目

    • 将内部枚举器标记为不再有项目 .

    添加扩展方法和适当的优化,其中包装参数已经可重复 . 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的完整实现 . 但这超出了这个特定问题的范围 . :)

    更新:注释中请求的代码实现 - 不确定为什么原始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;
        }
    }
    
  • 4

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

    // 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());
    }
    

    看起来比这里的其他答案简单得多 .

  • 1

    为什么不:

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

    Update: 就个人而言,我不会让代码更好(并且可能会使情况变得更糟)来解决警告;然后错过警告点 .

    例如:

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

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

    这就好像IDictionary通过类型为Func <TKey,TValue>的LazyLoader属性突然扩展为支持延迟加载值 . 实际上,它很好,但不是那么整洁,不能添加到IDictionary,因为现在每次收到IDictionary我们都要担心 . 但那就是我们所处的位置 .

    所以看起来“如果一个方法需要一个IEnumerable并且它可以避免两次,那么总是通过ToList()强制eval”是你能做的最好的事情 . Jetbrains的出色工作给了我们这个警告 .

    *(除非我错过了某些东西......刚刚完成它但看起来很有用)

  • 2

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

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

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

    如果枚举IEnumerable是一个昂贵的操作,如从文件或数据库加载某些东西,或者如果你有一个计算值并使用yield return的方法,你应该小心 . 在这种情况下,首先执行 ToList()ToArray() 来加载/计算仅ONCE的所有数据 .

  • 2

    你可以使用 ICollection<T> (或 IList<T> ) . 它不如 List<T> 具体,但不会受到多枚举问题的影响 .

    在这种情况下,我仍倾向于使用 IEnumerable<T> . 您还可以考虑重构代码以仅枚举一次 .

  • 0

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

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

  • 2

    您只能迭代一次:

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

    之前没有人说过(@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);
            }
        }
    }
    

相关问题