.NET中*延迟*特性的几个圈套
核心提示:.NET发展至今,实在各处都有“延迟(Lazy)”的痕迹,1个小小的“Laziness”给我们带来了很多灵活性1。“延迟”的关键就在于“只在需要的时候处理数据”,老赵曾在多篇文章中提到了类似的概念....
.NET发展至今,实在各处都有“延迟(Lazy)”的痕迹,1个小小的“Laziness”给我们带来了很多灵活性1。“延迟”的关键就在于“只在需要的时候处理数据”,老赵曾在多篇文章中提到了类似的概念,如《高阶函数、拜托与匿名方法》及《您善于使用匿名函数吗?》。不过“延迟”本身也会给您带来1些圈套,某些圈套您很有可能也曾碰到过。这篇文章便是总结了延迟特性的集中常见圈套,并给出应对方案。
重复运算
题目
“延迟”的本意是“减少计算”,但是假设您使用不当,很可能反而会造成“重复计算”。例如,我们首先构建1个方法,它接受1个参数n,返回1个Func对象:
以下为援用的内容:
static Func 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对象会根据传进的参数x,返回1个表示x能否被n整除的布尔值。在这进程中,还会向控制台输出1句话,例如:“10 can be divisible by 3? No”。每当看到这句话,则表明“经过了1次判定”。那末您是否是知道,下面的代码会输出甚么结果呢?
以下为援用的内容:
List values = new List(); 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 ToString(IEnumerable source) { if (source == null) { throw new ArgumentNullException("source"); }
foreach (int item in source) { yield return item.ToString(); } }
|
假设您没有看出来的话,不如运行1下这段代码:
以下为援用的内容:
static void Main(string[] args) { IEnumerable values; try { values = ToString(null); } catch (ArgumentNullException) { Console.WriteLine("Passed the null source"); return; }
foreach (var s in values) { } }
|
请问,运行上面的代码是否是会抛出异常?从代码的意图上看,在ToString方法的1开始我们会检查参数是否是为null,然后抛出异常——这本应被catch语句所捕捉。但是事实上,代码直到foreach履行时才真正抛出了异常。这类“延迟”履行背背了我们的实现意图。为甚么会这样呢?您可使用.NET Reflector反编译1下,查看1下yield语句的等价C#实现是甚么样的,1切就清楚了。
1
2
下1页
核心提示:.NET发展至今,实在各处都有“延迟(Lazy)”的痕迹,1个小小的“Laziness”给我们带来了很多灵活性1。“延迟”的关键就在于“只在需要的时候处理数据”,老赵曾在多篇文章中提到了类似的概念....
解决方案
对这个题目,1般我们可使用1对public和private方法配合来使用:
以下为援用的内容:
public static IEnumerable ToString(IEnumerable source) { if (source == null) { throw new ArgumentNullException("source"); }
return ToStringInternal(source); }
private static IEnumerable ToStringInternal(IEnumerable source) { foreach (int item in source) { yield return item.ToString(); } }
|
无妨再往查看1下现在的C#代码实现?
资源治理
题目
由因而延迟履行,1些本来最简单的代码模式可能就破坏了。例如:
以下为援用的内容:
static Func ReadAllText(string file) { using (Stream stream = File.OpenRead(file)) { StreamReader reader = new StreamReader(stream); return reader.ReadToEnd; } }
|
使用using来治理文件的打开封闭是最轻易不过的事情了,不过现在假设您通过ReadAllText(@"C:\abc.txt")方法取得的Func对象,在履行时就会抛出ObjectDisposedException。这是由于本来我们意图中的顺序:
打开文件
读取内容
封闭文件
由于有“延迟”特性,这个顺序已变成:
打开文件
封闭文件
读取内容
这怎样能不出错?
解决方案
有朋友说,这个轻易:
以下为援用的内容:
static Func ReadAllText(string file) { using (Stream stream = File.OpenRead(file)) { StreamReader reader = new StreamReader(stream); string text = reader.ReadToEnd();
return () => text; } }
|
的确没有抛出异常了,但是这也丧失了“延迟”的特点了。我们必须让它能够在调用拜托对象的时候,才往打开文件:
以下为援用的内容:
static Func ReadAllText(string file) { return () => { using (Stream stream = File.OpenRead(file)) { StreamReader reader = new StreamReader(stream); return reader.ReadToEnd(); } }; }
|
值得1提的是,using完全可以配合yield语句使用。也就是说,您可以编写这样的代码:
以下为援用的内容:
static IEnumerable AllLines(string file) { using (Stream stream = File.OpenRead(file)) { StreamReader reader = new StreamReader(stream); while (!reader.EndOfStream) { yield return reader.ReadLine(); } } }
|
由此也可见C#编译器是多么的强大,它帮我们解决了非常重要的题目。
闭包共享
题目
实在这个题目也已被谈过很屡次了,在这里提1下主要是为了保持内容的完全性。您以为,以下代码结果如何?
以下为援用的内容:
List actions = new List(); for (int i = 0; i < 10; i++) { actions.Add(() => Console.WriteLine(i)); }
foreach (var a in actions) a();
|
它打印出来的结果是10个10,具体缘由在《警惕匿名方法酿成的变量共享》1文中已有过描写,概括而来便是:各个action共享1个闭包,导致其中的“i”其实不是独立的。
解决方案
解决这个题目的方法,只需让不同闭包访问的值相互独立即可。如:
以下为援用的内容:
List actions = new List(); for (int i = 0; i < 10; i++) { int j = i; // 新增代码 actions.Add(() => Console.WriteLine(j)); }
foreach (var a in actions) a();
|
关于“延迟”特性,您还有甚么看法呢?
上1页
1
2
http://www.fw8.net/TAG:
方法,
内容,
代码,
文件,
题目