一尘不染

Enumerable.Single的错误实现?

algorithm

我通过反射器在Enumerable.cs中遇到了此实现。

public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    //check parameters
    TSource local = default(TSource);
    long num = 0L;
    foreach (TSource local2 in source)
    {
        if (predicate(local2))
        {
            local = local2;
            num += 1L;
            //I think they should do something here like:
            //if (num >= 2L) throw Error.MoreThanOneMatch();
            //no necessary to continue
        }
    }
    //return different results by num's value
}

我认为如果有两个以上的项目满足条件,他们应该打破循环,为什么它们总是遍历整个集合?如果反射器错误地分解了dll,我将编写一个简单的测试:

class DataItem
{
   private int _num;
   public DataItem(int num)
   {
      _num = num;
   }

   public int Num
   {
      get{ Console.WriteLine("getting "+_num); return _num;}
   }
} 
var source = Enumerable.Range(1,10).Select( x => new DataItem(x));
var result = source.Single(x => x.Num < 5);

对于此测试用例,我认为它将打印“ getting 0,getting 1”,然后引发异常。但事实是,它始终“获取0
…获取10”并抛出异常。他们采用这种方法是否有任何算法上的原因?

编辑 你们中有些人认为这是由于 谓词表达的副作用 ,经过深思熟虑和一些测试用例之后,我得出的结论是, 在这种情况下
副作用并不重要 。如果您不同意此结论,请提供示例。


阅读 224

收藏
2020-07-28

共1个答案

一尘不染

是的,我确实发现它有点奇怪,尤其是因为没有谓词的重载(即仅对序列起作用) 确实 具有快速抛出的“优化”。


在BCL的辩护中,我要说的是
,Single引发InvalidOperation异常是
头脑异常 ,通常不应将其用于控制​​流。 此类情况无需通过库进行优化。

使用Single零或多个匹配项的代码是完全 有效的 可能性,例如:

try
{
     var item = source.Single(predicate);
     DoSomething(item);
}

catch(InvalidOperationException)
{
     DoSomethingElseUnexceptional();    
}

应该重构为 不对 控制流使用异常的代码,例如(仅作为示例;可以更有效地实现):

var firstTwo = source.Where(predicate).Take(2).ToArray();

if(firstTwo.Length == 1) 
{
    // Note that this won't fail. If it does, this code has a bug.
    DoSomething(firstTwo.Single()); 
}
else
{
    DoSomethingElseUnexceptional();
}

换句话说,Single当我们期望序列 包含一个匹配项时,应保留to情况的使用。它的行为应与序列相同,First但带有附加的
运行时断言 ,即该序列不包含多个匹配项。像任何其他断言一样,
失败(即Single抛出异常的情况)应用于表示程序中的错误(在运行查询的方法中或在调用方传递给它的参数中)。

这给我们留下了两种情况:

  1. 断言成立:只有一个匹配项。在这种情况下,我们 无论如何 都要Single消耗整个序列来声明我们的主张。“优化”没有任何好处。实际上,有人可能会争辩说,由于要检查循环的每次迭代,因此OP提供的“优化”的示例实现实际上会变慢。 __
  2. 断言失败:有零个或多个匹配项。在这种情况下,我们 确实可能要 晚地抛出,但这并不是什么大不了的事情,因为该异常是顽固的: 它表示必须修复的错误。

综上所述,如果“性能较差的实施”在生产中影响了您的性能,则可以:

  1. 您使用Single不正确。
  2. 程序中有一个错误。错误修复后,此特定性能问题将消失。

编辑:澄清了我的观点。

编辑:这是Single 的 有效 用法,其中失败指示 调用 代码中的错误(错误的参数):

public static User GetUserById(this IEnumerable<User> users, string id)
{
     if(users == null)
        throw new ArgumentNullException("users");

     // Perfectly fine if documented that a failure in the query
     // is treated as an exceptional circumstance. Caller's job 
     // to guarantee pre-condition.        
     return users.Single(user => user.Id == id);    
}
2020-07-28