程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> .NET實例教程 >> .NET中*延遲*特性的幾個陷阱

.NET中*延遲*特性的幾個陷阱

編輯:.NET實例教程

.

.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? 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#實現是什麼樣的,一切就清楚了。

.

解決方案

對於這個問題,一般我們可以使用一對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();

關於“延遲”特性,您還有什麼看法呢?

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved