從.NET框架中委託寫法的演變談開去(中)

iDotNetSpace發表於2009-08-07

上一篇文章中我們簡單探討了.NET 1.x和.NET 2.0中委託表現形式的變化,以及.NET 2.0中匿名方法的優勢、目的及注意事項。那麼現在我們來談一下.NET 3.5(C# 3.0)中,委託的表現形式又演變成了什麼樣子,還有什麼特點和作用。

.NET 3.5中委託的寫法(Lambda表示式)

  Lambda表示式在C#中的寫法是“arg-list => expr-body”,“=>”符號左邊為表示式的引數列表,右邊則是表示式體(body)。引數列表可以包含0到多個引數,引數之間使用逗號分割。例如,以下便是一個使用Lambda表示式定義了委託的示例1

Func<int, int, int> max = (int a, int b) =>
{
    if (a > b)
    {
        return a;
    }
    else
    {
        return b;
    }
};

  與上文使用delegate定義匿名方法的作用相同,Lambda表示式的作用也是為了定義一個匿名方法。因此,下面使用delegate的程式碼和上面是等價的:

Func<int, int, int> max = delegate(int a, int b)
{
    if (a > b)
    {
        return a;
    }
    else
    {
        return b;
    }
};

  那麼您可能就會問,這樣看來Lambda表示式又有什麼意義呢?Lambda表示式的意義便是它可以寫的非常簡單,例如之前的Lambda表示式可以簡寫成這樣:

Func<int, int, int> max = (a, b) =>
{
    if (a > b)
    {
        return a;
    }
    else
    {
        return b;
    }
};

  由於我們已經註明max的型別是Func,因此C#編譯器可以明確地知道a和b都是int型別,於是我們就可以省下引數之前的型別資訊。這個特性叫做“型別推演”,也就是指編譯器可以自動知道某些成員的型別2。請不要輕易認為這個小小的改進意義不大,事實上,您會發現Lambda表示式的優勢都是由這一點一滴的細節構成的。那麼我們再來一次改變:

Func<int, int, int> max = (a, b) => a > b ? a : b;

  如果Lambda表示式的body是一個表示式(expression),而不是語句(statement)的話,那麼它的body就可以省略大括號和return關鍵字。此外,如果Lambda表示式只包含一個引數的話,則引數列表的括號也可以省略,如下:

Func<int, bool> positive = a => a > 0;

  如今的寫法是不是非常簡單?那麼我們來看看,如果是使用delegate關鍵字來建立的話會成為什麼樣子:

Func<int, bool> positive = delegate(int a)
{
    return a > 0;
};

  您馬上就可以意識到,這一行和多行的區別,這幾個關鍵字和括號的省略,會使得程式設計世界一下子變得大為不同。

  當然,Lambda表示式也並不是可以完全替代delegate寫法,例如帶ref和out關鍵字的匿名方法,就必須使用.NET 2.0中的delegate才能構造出來了。

使用示例一

  Lambda表示式的增強在於“語義”二字。“語義”是指程式碼所表現出來的含義,說的更通俗一些,便是指一段程式碼給閱讀者的“感覺”如何。為了說明這個例子,我們還是使用示例來說明問題。

  第一個例子是這樣的:“請寫一個方法,輸入一個表示整型的字串列表,並返回一個列表,包含其中偶數的平方,並且需要按照平方後的結果排序”。很簡單,不是嗎?相信您一定可以一蹴而就:

static List<int> GetSquaresOfPositive(List<string> strList)
{
    List<int> intList = new List<int>();
    foreach (var s in strList) intList.Add(Int32.Parse(s));

    List<int> evenList = new List<int>();
    foreach (int i in intList)
    {
        if (i % 2 == 0) evenList.Add(i);
    }

    List<int> squareList = new List<int>();
    foreach (int i in evenList) squareList.Add(i * i);

    squareList.Sort();
    return squareList;
}

  我想問一下,這段程式碼給您的感覺是什麼?它給我的感覺是:做了很多事情。有哪些呢?

  1. 新建一個整數列表intList,把引數strList中所有元素轉化為整型儲存起來。
  2. 新建一個整數列表evenList,把intList中的偶數儲存起來。
  3. 新建一個整數列表squareList,把evenList中所有數字的平方儲存起來。
  4. 將squareList排序。
  5. 返回squareList。

  您可能會問:“當然如此,還能怎麼樣?”。事實上,如果使用了Lambda表示式,程式碼就簡單多了:

static List<int> GetSquaresOfPositiveByLambda(List<string> strList)
{
    return strList
        .Select(s => Int32.Parse(s)) // 轉成整數
        .Where(i => i % 2 == 0) // 找出所有偶數
        .Select(i => i * i) // 算出每個數的平方
        .OrderBy(i => i) // 按照元素自身排序
        .ToList(); // 構造一個List
}

  配合.NET 3.5中定義的擴充套件方法,這段程式碼可謂“一氣呵成”(在實際編碼過程中,老趙更傾向於把這種簡短的“遞進式”程式碼寫作一行)。那麼這行程式碼的“語義”又有什麼變化呢?在這裡,“語義”的變化在於程式碼的關注點從“怎麼做”變成了“做什麼”。這就是Lambda表示式的優勢。

  在第一個方法中,我們構造了多個容器,然後做一些轉化,過濾,並且向容器填充內容。其實這些都是“怎麼做”,也就是所謂的“how (to do)”。但是這些程式碼並不能直接表示我們想要做的事情,我們想要做的事情其實是“得到XXX”,“篩選出YYY”,而不是“建立容器”,“新增元素”等操作。

  在使用Lambda表示式的實現中,程式碼變得“宣告式(declarative)”了許多。所謂“宣告式”,便是“聲稱程式碼在做什麼”,而不像“命令式(imperative)”的程式碼在“操作程式碼怎麼做”。換句話說,“宣告式”關注的是“做什麼”,是指“what (to do)”。上面這段宣告式的程式碼,其語義則變成了:

  1. 把字串轉化為整數
  2. 篩選出所有偶數
  3. 把每個偶數平方一下
  4. 按照平方結果自身排序 
  5. 生成一個列表

  至於其中具體是怎麼實現的,有沒有構造新的容器,又是怎麼向容器裡新增元素的……這些細節,使用Lambda表示式的程式碼一概不會關心——這又不是我們想要做的事情,為什麼要關心它呢?

  雖然擴充套件方法功不可沒,但我認為,Lambda表示式在這裡的重要程度尤勝前者,因為它負責了最關鍵的“語義”。試想,“i => i * i”給您的感覺是什麼呢?是構造了一個委託嗎(當然,您一定知道在這裡其實構造了一個匿名方法)?至少對我來說,它的含義是“把i變成i * i”;同樣,“i => i % 2 == 0”給我的感覺是“(篩選標準為)i模2為零”,而不是“構造一個委託,i大於零時返回true,否則返回false”;更有趣的是,OrderBy(i => i)給我的感覺是“把i按照i自身排序”,而不是“一個返回i自身的委託”。這一切,都是在“宣告”這段程式碼在“做什麼”,而不是“怎麼做”。

  沒錯,“型別推演”,“省略括號”和“省略return關鍵字”可能的確都是些“細小”的功能,但也正是這些細微之處帶來了編碼方式上的關鍵性改變。

使用示例二

  使用Lambda表示式還可以節省許多程式碼(相信您從第一個示例中也可以看出來了)。不過我認為,最省程式碼的部分更應該可能是其“分組”和“字典轉化”等功能。因此,我們來看第二個示例。

  這個示例可能更加貼近現實。不知您是否關注過某些書籍後面的“索引”,它其實就是“列出所有的關鍵字,根據其首字母進行分組,並且要求對每組內部的關鍵字進行排序”。簡單說來,我們需要的其實是這麼一個方法:

static Dictionary<char, List<string>> GetIndex(IEnumerable<string> keywords) { ... }

  想想看,您會怎麼做?其實不難(作為示例,我們這裡只關注小寫英文,也不關心重複關鍵字這種特殊情況):

static Dictionary<char, List<string>> GetIndex(IEnumerable<string> keywords)
{
    // 定義字典
    var result = new Dictionary<char, List<string>>();

    // 填充字典
    foreach (var kw in keywords)
    {
        var firstChar = kw[0];
        List<string> groupKeywords;

        if (!result.TryGetValue(firstChar, out groupKeywords))
        {
            groupKeywords = new List<string>();
            result.Add(firstChar, groupKeywords);
        }

        groupKeywords.Add(kw);
    }

    // 為每個分組排序
    foreach (var groupKeywords in result.Values)
    {
        groupKeywords.Sort();
    }

    return result;
}

  那麼如果利用Lambda表示式及.NET框架中定義的擴充套件方法,程式碼又會變成什麼樣呢?請看:

static Dictionary<char, List<string>> GetIndexByLambda(IEnumerable<string> keywords)
{
    return keywords
        .GroupBy(k => k[0]) // 按照首字母分組
        .ToDictionary( // 構造字典
            g => g.Key, // 以每組的Key作為鍵
            g => g.OrderBy(k => k).ToList()); // 對每組排序並生成列表
}

  光從程式碼數量上來看,前者便是後者的好幾倍。而有關“宣告式”,“what”等可讀性方面的優勢就不再重複了,個人認為它比上一個例子給人的“震撼”有過之而無不及。

  試想,如果我們把GetIndexByLambda方法中的Lambda表示式改成.NET 2.0中delegate形式的寫法:

static Dictionary<char, List<string>> GetIndexByDelegate(IEnumerable<string> keywords)
{
    return keywords
        .GroupBy(delegate(string k) { return k[0]; })
        .ToDictionary(
            delegate(IGrouping<char, string> g) { return g.Key; },
            delegate(IGrouping<char, string> g)
            {
                return g.OrderBy(delegate(string s) { return s; }).ToList();
            });
}

  您願意編寫這樣的程式碼嗎?

  因此,Lambda表示式在這裡還是起著決定性的作用。事實上正是因為有了Lambda表示式,.NET中的一些函數語言程式設計特性才被真正推廣開來。“語言特性”決定“程式設計方式”的確非常有道理。這一點上Java是一個很好的反例:從理論上說,Java也有“內聯”的寫法,但是C#的使用快感在Java那邊還只能是個夢。試想GetIndexByLambda在Java中會是什麼情況3

public Dictionary> GetIndexInJava(Enumerable keywords)
{
    return keywords
        .GroupBy(
            new Func {
                public Char execute(String s) { return s.charAt(0); }
            })
        .ToDictionary(
            new Func, Char> {
                public Char execute(IGrouping g) { return g.getKey(); }
            },
            new Func, List> {
                public List execute(IGrouping g)
                {
                    return g
                        .OrderBy(
                            new Func {
                                public String execute(String s) { return s; }
                            })
                        .ToList();
                }
            });
}

  一股語法噪音的氣息撲面而來,讓人無法抵擋。由於Java中的匿名型別語法(即上面這種內聯寫法)連型別資訊(new Func{ ... }這樣的程式碼)都無法省去,因此給人非常繁瑣的感覺。面對這樣的程式碼,您可能會有和我一樣的想法:“還不如最普通的寫法啊”。沒錯,這種函數語言程式設計的風格,由於缺乏語言特性支援,實在不適合在Java語言中使用。事實上,這種內聯寫法很早就出現了(至少在02、03年我還在使用Java的時候就已經有了),但是那麼多年下來一點改進都沒有。而Lambda表示式出現之後,社群中立即跟進了大量專案,如MoqFluent NHibernate等等,充分運用了C# 3.0的這一新特性。難道這還不夠說明問題嗎?

  對了,再次推薦一下Scala語言,它的程式碼可以寫的和C#一樣漂亮。我不是Java平臺的粉絲,更是Java語言的忠實反對者,但是我對Java平臺上的Scala語言和開源專案都抱有強烈的好感。

  既然談到了函數語言程式設計,那麼就順便再多說幾句。其實這兩個例子都有濃厚的函數語言程式設計影子在裡面,例如,對於函式試程式設計來說,Where常被叫做filter,Select常被叫做map。而.NET 3.5中定義的另一些方法在函數語言程式設計裡都有體現(如Aggregate相當於fold)。如果您對這方面感興趣,可以關注Matthew Poswysocki提出的Functional C#類庫。

總結

  既可以提高可讀性,又能夠減少程式碼數量,我實在找不出任何理由拒絕Lambda表示式。

  哦,對了,您可能會提到“效能”,這的確也是一個重要的方面,不過關於這個話題我們下次再談。受篇幅限制,原本計劃的“上”“下”兩篇這次又不得不拆開了。至於其他的內容,也等討論完效能問題之後再說吧。

  當然,世界上沒有東西是完美的,如果您覺得Lambda表示式在某些時候會給您帶來“危害”,那麼也不妨使用delegate代替Lambda表示式。例如,為了程式碼清晰,在某些時候還是顯式地指明引數型別比較好。不過對我而言,在任何情況下我都會使用Lambda表示式——最多使用“(int a, string b) =>”的形式咯,我想總比“delegate(int a, string b)”要統一、省事一些吧。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-611623/,如需轉載,請註明出處,否則將追究法律責任。

相關文章