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

时间:2011-07-20 15:22:29

标签: c# .net resharper resharper-6.0

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

原因是,在此版本中,列表仅枚举一次,因此将其更改为IEnumerable<string>不会自动引入另一个警告。 现在,如果我手动更改第一个版本以使用IEnumerable<string>而不是List<string>,我将在list两次出现时收到该警告(“可能多次枚举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)
    {
        // ...
    }
}

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

  1. 方法中没有调用ToList(),因为它会影响性能
  2. 不使用ICollection<T>或更专业的接口/类,因为它们会改变从调用者看到的方法的语义。
  3. IEnumerable<T>上没有多次迭代,因此有多次或类似数据库访问数据库的风险。
  4. 注意:我知道这不是一个Resharper问题,因此,我不想压制这个警告,但是因为警告是合法的,所以要解决根本原因。

    更新 请不要关心Anyforeach。在合并这些语句时,我不需要帮助只有一个枚举的枚举 这个方法中的任何东西都可以多次枚举可枚举的数据!

13 个答案:

答案 0 :(得分:5)

这个类将为您提供一种方法,可以将第一个项目从枚举中分离出来,然后在枚举的其余部分使用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类型设置,则需要捕获否则将在构造函数中抛出的异常。

答案 1 :(得分:4)

您应该选择IEnumerable<T>并忽略“多次迭代”警告。

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

答案 2 :(得分:4)

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

  • enumerable.ToList,您可以通过首先尝试“可枚举为列表”来优化它,只要您不修改列表
  • 在IEnumerable上迭代两次但是为调用者清楚(记录它)
  • 分为两种方法
  • 列出以避免“as”/ ToList的成本和双重枚举的潜在成本

对于可以处理任何Enumerable的公共方法,第一个解决方案(ToList)可能是最“正确的”。

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

答案 3 :(得分:4)

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

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

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

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

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

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

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

      3. 产生当前项目

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

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

    UPDATE:注释中请求的代码实现 - 不确定为什么原始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 :(得分:3)

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

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

似乎比其他答案更直接。

答案 5 :(得分:2)

为什么不:

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

答案 6 :(得分:2)

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

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

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

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

答案 7 :(得分:2)

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

可枚举LINQ运算符的缺点是它们主动枚举源代码而不是接受被推送到它们的项目,因此它们不能满足您的要求。

如果你是只需要1&000; 000&#39; 000整数序列的最小值和最大值,这需要花费1美元和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方式表达。

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

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

答案 8 :(得分:1)

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

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

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

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

答案 9 :(得分:0)

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

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

答案 10 :(得分:0)

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

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

答案 11 :(得分:0)

您只能迭代一次:

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

答案 12 :(得分:0)

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