Enumerable.Single的执行不好?

Che*_*hen 34 .net c# linq 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
}
Run Code Online (Sandbox Code Playgroud)

我认为如果有超过2个项目符合条件,他们应该打破循环,为什么他们总是遍历整个集合?如果反射器不正确地拆卸了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);
Run Code Online (Sandbox Code Playgroud)

对于这个测试用例,我认为它将打印"获得0,获得1"然后抛出异常.但实际情况是,它保持"变为0 ......变为10"并抛出异常.是否有任何算法原因他们实现这样的方法?

编辑有些人认为这是因为谓词表达的副作用,经过深思熟虑和一些测试用例后,我得出结论,在这种情况下副作用并不重要.如果您不同意这个结论,请举例说明.

Ani*_*Ani 23

是的,我觉得它有点奇怪,特别是因为不带谓词的重载(即只对序列起作用)似乎确实有快速投射'优化'.


但是,在BCL的防御中,我会说Single抛出InvalidOperation异常是一个骨头异常,通常不应该用于控制流.这种情况不必由图书馆进行优化.

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

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

catch(InvalidOperationException)
{
     DoSomethingElseUnexceptional();    
}
Run Code Online (Sandbox Code Playgroud)

应该重构为使用控制流异常的代码,例如(只是一个样本;这可以更有效地实现):

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();
}
Run Code Online (Sandbox Code Playgroud)

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

这给我们留下了两个案例:

  1. 断言成立:有一场比赛.在这种情况下,我们要Single消耗整个序列反正断言我们的要求."优化"没有任何好处.实际上,人们可能会争辩说OP提供的"优化"的示例实现实际上会因为检查循环的每次迭代而变慢.
  2. 断言失败:有零个或多个匹配.在这种情况下,我们晚于我们抛出可能,但由于异常是愚蠢的,这不是什么大不了的:它表示必须修正了一个错误的.

总而言之,如果"糟糕的实施"在生产中以性能为导向,那么:

  1. 您使用Single不当.
  2. 你有一个错误程序.一旦修复了错误,这个特定的性能问题就会消失.

编辑:澄清我的观点.

编辑:这是Single 的有效使用,其中failure表示调用代码中的错误(错误参数):

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);    
}
Run Code Online (Sandbox Code Playgroud)

  • 虽然我确实同意用户的代码应该被修复,但是没有理由告诉我为什么库方法不应该尽快失败而不需要额外的努力.这对我来说似乎是一件微不足道的事情. (5认同)
  • @Jeff M:这并不意味着失败 - 失败是一个**错误**.当它没有失败时,它执行*更快*而没有'优化'. (4认同)
  • 我想这是正确的答案.抛出后期是针对成功案例进行优化的,这似乎是公平的,因为(a)Single基本上是第一个断言它是唯一的断言预期会成功,并且(b)SEH痛苦地缓慢所以很少有点优化始终导致抛出异常的路径. (4认同)
  • @stakx:你不是在重述我的观点吗? (2认同)

sta*_*ica 8

更新:
我得到了一些非常好的反馈,这让我重新思考.因此,我将首先提供说明我的"新"观点的答案; 你仍然可以在下面找到我原来的答案.请务必阅读中间的评论,以了解我的第一个答案错过了哪一点.

新答案:

我们假设Single在不满足前置条件时应抛出异常; 也就是说,当Single检测到比none或者集合中的多个项目与谓词匹配时.

Single只有通过整个集合抛出异常才能成功.它必须确保只有一个匹配的项目,因此它必须检查集合中的所有项目.

这意味着尽早抛出异常(一旦找到第二个匹配项),本质上就是一种优化,只有在Single无法满足前提条件和抛出异常时才能从中受益.

正如用户CodeInChaos在下面的评论中清楚地说明的那样,优化不会是错误的,但它没有意义,因为人们通常会引入优化,这将使正确工作的代码受益,而不是有利于故障代码的优化.

因此,实际上Single可以提前抛出异常; 但它没有必要,因为实际上没有额外的好处.


老答案:

我无法给出技术原因,为什么该方法按照它的方式实现,因为我没有实现它.但是我可以说明我对Single运营商目的的理解,并从那里得出我的个人结论,它确实实施得很糟糕:

我的理解Single:

它的目的是什么Single,它与eg FirstLast?有何不同?

使用Single运算符基本上表达了一个假设,即必须从集合中返回一个项目:

  • 如果您没有指定谓词,那么它应该意味着该集合应该只包含一个项目.

  • 如果确实指定了谓词,那么它应该意味着集合中的一个项目应该满足该条件.(使用谓词应该具有相同的效果 items.Where(predicate).Single().)

这是什么使得Single从其他运营商如不同First,LastTake(1).这些运营商都没有要求应该只有一个(匹配)项目.

什么时候应该Single抛出异常?

基本上,当它发现你的假设是错误的; 即当底层集合不能恰好产生一个(匹配)项目时.也就是说,当零个或多个项目时.

什么时候应该Single使用?

Single当程序的逻辑可以保证集合只产生一个项目和一个项目时,使用是合适的.如果抛出异常,那应该意味着程序的逻辑包含错误.

如果处理"不可靠"集合(例如I/O输入),则应首先验证输入,然后再将其传递给Single.Single与异常catch块一起,适合确保集合只有一个匹配项.在您调用时Single,您应该已经确保只有一个匹配项.

结论:

上面说明了我对SingleLINQ运算符的理解.如果你遵循并同意这种理解,你应该得出结论,Single应该尽早抛出异常.没有理由等到(可能非常大)集合结束,因为Single一旦检测到集合中的第二个(匹配)项目就会违反前置条件.

  • @stakx我并不反对这种优化.我只是说它在实践中无关紧要.在你的无限序列中,`Single`也不会在非异常情况下终止.而这无法进行优化. (3认同)
  • 但是,由于它抛出异常的情况是你的程序有bug的情况,在这种情况下的性能并不重要. (2认同)
  • *@阿图尔:*这不是我的意思.早点断绝肯定是可能的:事实上它在找到一场比赛之后无法停止,但它肯定*当它找到第二场比赛时.例如,如果您有100个项目的序列,并且第1个和第6个项目都匹配,那么您可以在第6个项目之后停止,而不是迭代剩余的94个项目.**但是**我现在会采用CodeInChaos和Peter Lillevold的立场,即提前破解是一种只有错误情况才能获益的优化. (2认同)