15

在 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<string>不会自动引入另一个警告。现在,如果我手动更改第一个版本以使用 aIEnumerable<string>而不是 a ,我将在方法主体中的List<string>两次出现时都收到该警告(“IEnumerable 的可能多重枚举”) :list

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

我明白,为什么,但我想知道如何解决这个警告,假设该方法真的只需要 aIEnumerable<T>而不是 a List<T>,因为我只想枚举项目并且我不想更改列表。在方法的开头
添加 a会使警告消失: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>,因此有多次或类似访问数据库的风险。

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

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

4

13 回答 13

5

您可能应该采取IEnumerable<T>并忽略“多次迭代”警告。

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

于 2011-07-20T15:26:09.237 回答
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 类型设置,则需要捕获否则会在构造函数中抛出的异常。

于 2012-12-10T19:33:50.163 回答
5

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

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

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

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

于 2011-07-21T08:17:12.357 回答
4

有一个通用的解决方案来解决 Resharper 警告:IEnumerable 的可重复性缺乏保证,以及 List 基类(或可能昂贵的 ToList() 解决方法)。

创建一个专门的类,即 IE “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 的完整实现。不过,这超出了这个特定问题的范围。:)

更新:评论中要求的代码实现——不知道为什么原始 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;
    }
}
于 2012-10-03T19:39:56.457 回答
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());
}

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

于 2015-01-29T20:36:05.150 回答
2

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

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

所以看起来“如果一个方法需要一个 IEnumerable 并对其进行两次评估,总是通过 ToList() 强制评估”是你能做的最好的事情。Jetbrains 的出色工作给了我们这个警告。

*(除非我遗漏了一些东西......只是弥补了它,但它似乎很有用)

于 2012-05-26T15:01:52.627 回答
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
于 2011-07-20T20:29:38.887 回答
2

一般来说,您需要一些状态对象,您可以将项目推入其中(在 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();
}
于 2017-02-14T11:03:49.177 回答
1

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

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

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

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

于 2011-07-20T15:31:31.440 回答
0

您可以使用ICollection<T>(或IList<T>)。它不如 特定List<T>,但不受多重枚举问题的影响。

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

于 2011-07-20T15:26:10.253 回答
0

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

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

于 2011-07-20T15:27:32.843 回答
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);
        }
    }
}
于 2011-07-20T17:04:22.313 回答
0

您只能迭代一次:

public void DoSomething(IEnumerable<string> list)
{
    bool isFirstItem = true;
    foreach (var item in list)
    {
        if (isFirstItem)
        {
            isFirstItem = false;
            // ...
        }
        // ...
    }
}
于 2011-07-20T15:47:53.357 回答