重复运算
问题
“延迟”的本意是“减少计算”,但是如果您使用不当,很可能反而会造成“重复计算”。例如,我们首先构建一个方法,它接受一个参数n,返回一个Func<int, bool>对象:
以下为引用的内容:
static Func<int, bool> DivideBy(int n)
{
return x =>
{
bool divisible = x % n == 0;
Console.WriteLine(
"{0} can be divisible by {1}? {2}",
x, n, divisible ? "Yes" : "No");
return divisible;
};
}
返回的Func<int, bool>对象会根据传入的参数x,返回一个表示x能否被n整除的布尔值。在这过程中,还会向控制台输出一句话,例如:“10 can be divisible by 3? No”。每当看到这句话,则表明“经过了一次判断”。那么您是否知道,下面的代码会输出什么结果呢?
以下为引用的内容:
List<int> values = new List<int>();
for (int i = 0; i < 10; i++) values.Add(i);
var divideByTwo = values.Where(DivideBy(2));
var divideByTwoAndThree = divideByTwo.Where(DivideBy(3));
var divideByTwoAndFive = divideByTwo.Where(DivideBy(5));
foreach (var i in divideByTwoAndThree) { }
foreach (var i in divideByTwoAndFive) { }
结果如下:
以下为引用的内容:
0 can be divisible by 2? Yes
0 can be divisible by 3? Yes
1 can be divisible by 2? No
2 can be divisible by 2? Yes
2 can be divisible by 3? No
3 can be divisible by 2? No
4 can be divisible by 2? Yes
4 can be divisible by 3? No
5 can be divisible by 2? No
6 can be divisible by 2? Yes
6 can be divisible by 3? Yes
7 can be divisible by 2? No
8 can be divisible by 2? Yes
8 can be divisible by 3? No
9 can be divisible by 2? No
0 can be divisible by 2? Yes
0 can be divisible by 5? Yes
1 can be divisible by 2? No
2 can be divisible by 2? Yes
2 can be divisible by 5? No
3 can be divisible by 2? No
4 can be divisible by 2? Yes
4 can be divisible by 5? No
5 can be divisible by 2? No
6 can be divisible by 2? Yes
6 can be divisible by 5? No
7 can be divisible by 2? No
8 can be divisible by 2? Yes
8 can be divisible by 5? No
9 can be divisible by 2? No
您是否发现,无论是在遍历divideByTwoAndThree和divideByTwoAndFive序列时,都会从原有的values序列里重新判断每个元素是否能够被2整除?这就是.NET 3.5中“Where”的延迟特性,如果您在这里没有意识到这点,就可能会产生重复计算,浪费了计算能力。
解决方案
解决这个问题的方法就是在合适的时候进行“强制计算”。例如:
以下为引用的内容:
var divideByTwo = values.Where(DivideBy(2)).ToList();
var divideByTwoAndThree = divideByTwo.Where(DivideBy(3));
var divideByTwoAndFive = divideByTwo.Where(DivideBy(5));
结果就变成了:
以下为引用的内容:
0 can be divisible by 2? Yes
1 can be divisible by 2? No
2 can be divisible by 2? Yes
3 can be divisible by 2? No
4 can be divisible by 2? Yes
5 can be divisible by 2? No
6 can be divisible by 2? Yes
7 can be divisible by 2? No
8 can be divisible by 2? Yes
9 can be divisible by 2? No
0 can be divisible by 3? Yes
2 can be divisible by 3? No
4 can be divisible by 3? No
6 can be divisible by 3? Yes
8 can be divisible by 3? No
0 can be divisible by 5? Yes
2 can be divisible by 5? No
4 can be divisible by 5? No
6 can be divisible by 5? No
8 can be divisible by 5? No
此时,在获得divideByTwo序列时,就会立即进行计算,这样在遍历后两者时就不会重复计算1,3,5等元素了。
异常陷阱
问题
请问您是否知道下面的代码有什么问题?
以下为引用的内容:
public static IEnumerable<string> ToString(IEnumerable<int> source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
foreach (int item in source)
{
yield return item.ToString();
}
}
如果您没有看出来的话,不如运行一下这段代码:
以下为引用的内容:
static void Main(string[] args)
{
IEnumerable<string> values;
try
{
values = ToString(null);
}
catch (ArgumentNullException)
{
Console.WriteLine("Passed the null source");
return;
}
foreach (var s in values) { }
}
请问,运行上面的代码是否会抛出异常?从代码的意图上看,在ToString方法的一开始我们会检查参数是否为null,然后抛出异常——这本应被catch语句所捕获。但是事实上,代码直到foreach执行时才真正抛出了异常。这种“延迟”执行违反了我们的实现意图。为什么会这样呢?您可以使用.NET Reflector反编译一下,查看一下yield语句的等价C#实现是什么样的,一切就清楚了。