发表于:2009-11-16 浏览:82 作者: 来源:站长资讯网
 关键字:NET,特性,陷阱,延迟
 描述:.NET发展至今,其实各处都有“延迟(Lazy)”的痕迹,一个小小的“Laziness”给我们带来了不少灵活性1。“延迟”的关键就在于“只在需要的时候处理数据”,老赵曾经在多篇文章中提到了类似的概念,如《高阶
.NET发展至今,其实各处都有“延迟(Lazy)”的痕迹,一个小小的“Laziness”给我们带来了不少灵活性1。“延迟”的关键就在于“只在需要的时候处理数据”,老赵曾经在多篇文章中提到了类似的概念,如《高阶函数、委托与匿名方法》及《您善于使用匿名函数吗?》。不过“延迟”本身也会给您带来一些陷阱,某些陷阱您很有可能也曾经遇到过。这篇文章便是总结了延迟特性的集中常见陷阱,并给出应对方案。
重复运算
问题
“延迟”的本意是“减少计算”,但是如果您使用不当,很可能反而会造成“重复计算”。例如,我们首先构建一个方法,它接受一个参数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? Yes0 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? Yes1 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#实现是什么样的,一切就清楚了。
{[csc:pagelist]}
解决方案
对于这个问题,一般我们可以使用一对public和private方法配合来使用:
| 以下为引用的内容: public static IEnumerable<string> ToString(IEnumerable<int> source){
 if (source == null)
 {
 throw new ArgumentNullException("source");
 }
     return ToStringInternal(source);}
 private static IEnumerable<string> ToStringInternal(IEnumerable<int> source){
 foreach (int item in source)
 {
 yield return item.ToString();
 }
 }
 | 
不妨再去查看一下现在的C#代码实现?
资源管理
问题
由于是延迟执行,一些原本最简单的代码模式可能就破坏了。例如:
| 以下为引用的内容: static Func<string> ReadAllText(string file){
 using (Stream stream = File.OpenRead(file))
 {
 StreamReader reader = new StreamReader(stream);
 return reader.ReadToEnd;
 }
 }
 | 
使用using来管理文件的打开关闭是最容易不过的事情了,不过现在如果您通过ReadAllText(@"C:\abc.txt")方法获得的Func<string>对象,在执行时就会抛出ObjectDisposedException。这是因为原本我们意图中的顺序:
打开文件 
读取内容 
关闭文件
因为有“延迟”特性,这个顺序已经变为:
打开文件 
关闭文件 
读取内容
这怎么能不出错?
解决方案
有朋友说,这个容易:
| 以下为引用的内容: static Func<string> ReadAllText(string file){
 using (Stream stream = File.OpenRead(file))
 {
 StreamReader reader = new StreamReader(stream);
 string text = reader.ReadToEnd();
         return () => text;}
 }
 | 
的确没有抛出异常了,但是这也丧失了“延迟”的特点了。我们必须让它能够在调用委托对象的时候,才去打开文件:
| 以下为引用的内容: static Func<string> ReadAllText(string file){
 return () =>
 {
 using (Stream stream = File.OpenRead(file))
 {
 StreamReader reader = new StreamReader(stream);
 return reader.ReadToEnd();
 }
 };
 }
 | 
值得一提的是,using完全可以配合yield语句使用。也就是说,您可以编写这样的代码:
| 以下为引用的内容: static IEnumerable<string> AllLines(string file){
 using (Stream stream = File.OpenRead(file))
 {
 StreamReader reader = new StreamReader(stream);
 while (!reader.EndOfStream)
 {
 yield return reader.ReadLine();
 }
 }
 }
 | 
由此也可见C#编译器是多么的强大,它帮我们解决了非常重要的问题。
闭包共享
问题
其实这个问题也已经被谈过很多次了,在这里提一下主要是为了保持内容的完整性。您认为,以下代码结果如何?
| 以下为引用的内容: List<Action> actions = new List<Action>();for (int i = 0; i < 10; i++)
 {
 actions.Add(() => Console.WriteLine(i));
 }
 foreach (var a in actions) a(); | 
它打印出来的结果是10个10,具体原因在《警惕匿名方法造成的变量共享》一文中已经有过描述,概括而来便是:各个action共享一个闭包,导致其中的“i”并不是独立的。
解决方案
解决这个问题的方法,只需让不同闭包访问的值相互独立即可。如:
| 以下为引用的内容: List<Action> actions = new List<Action>();for (int i = 0; i < 10; i++)
 {
 int  j = i; // 新增代码
 actions.Add(() => Console.WriteLine(j));
 }
 foreach (var a in actions) a(); | 
关于“延迟”特性,您还有什么看法呢?