假设你有一个方法:
abstract class ProviderBase<T>
{
public IEnumerable<T> Results
{
get
{
List<T> list = new List<T>();
using(IDataReader rdr = GetReader())
while(rdr.Read())
list.Add(Build(rdr));
return list;
}
}
protected abstract IDataReader GetReader();
protected T Build(IDataReader rdr);
}
使用各种实现。其中之一用于:
public bool CheckNames(NameProvider source)
{
IEnumerable<string> names = source.Results;
switch(names.Count())
{
case 0:
return true;//obviously none invalid.
case 1:
//having one name to check is a common case and for some reason
//allows us some optimal approach compared to checking many.
return FastCheck(names.Single());
default:
return NormalCheck(names)
}
}
现在,这些都不是特别奇怪。我们没有假设 IEnumerable 的特定实现。事实上,这适用于数组和许多常用的集合(在 System.Collections.Generic 中想不出一个与我的头脑不匹配的集合)。我们只使用了普通方法和普通扩展方法。对单项集合进行优化案例甚至并不罕见。例如,我们可以将列表更改为数组、HashSet(自动删除重复项)、LinkedList 或其他一些东西,它会继续工作。
虽然我们不依赖于特定的实现,但我们依赖于特定的功能,特别是可重绕的功能(Count() 将调用 ICollection.Count 或通过枚举枚举,之后的名称- 将进行检查。
虽然有人看到 Results 属性并认为“嗯,这有点浪费”。他们将其替换为:
public IEnumerable<T> Results
{
get
{
using(IDataReader rdr = GetReader())
while(rdr.Read())
yield return Build(rdr);
}
}
这又是完全合理的,并且在许多情况下确实会带来相当大的性能提升。如果CheckNames 在相关编码器完成的即时“测试”中没有被命中(可能在很多代码路径中都没有命中),那么 CheckNames 将出错(并可能在超过 1 个名称的情况,如果会带来安全风险,情况可能会更糟)。
任何在 CheckNames 上命中且结果大于零的单元测试都会捕获它。
顺便提一下,类似的(如果更复杂的话)更改是 NPGSQL 中向后兼容功能的原因。并不像仅仅将 List.Add() 替换为 return yield 那样简单,而是对 ExecuteReader 工作方式的改变给出了从 O(n) 到 O(1) 的类似变化,以获得第一个结果。然而,在此之前,NpgsqlConnection 允许用户在第一个仍然打开时从连接中获取另一个阅读器,之后它没有。 IDbConnection 的文档说您不应该这样做,但这并不意味着没有运行代码可以这样做。幸运的是,这样的一段运行代码是 NUnit 测试,并添加了向后兼容功能,以允许此类代码只需更改配置即可继续运行。