zoukankan      html  css  js  c++  java
  • 一起谈.NET技术,从.NET中委托写法的演变谈开去(中):Lambda表达式及其优势 狼人:

    上一篇文章中我们简单探讨了.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<int, int, int>,因此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等于零”,而不是“构造一个委托,XXX时返回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<Char, List<String>> GetIndexInJava(Enumerable<String> keywords)
    {
        return keywords
            .GroupBy(
                new Func<String, Char> {
                    public Char execute(String s) { return s.charAt(0); }
                })
            .ToDictionary(
                new Func<Grouping<Char, String>, Char> {
                    public Char execute(IGrouping<Char, String> g) { return g.getKey(); }
                },
                new Func<Grouping<Char, String>, List<string>> {
                    public List<String> execute(IGrouping<Char, String> g)
                    {
                        return g
                            .OrderBy(
                                new Func<String, String> {
                                    public String execute(String s) { return s; }
                                })
                            .ToList();
                    }
                });
    }

    一股语法噪音的气息扑面而来,让人无法抵挡。由于Java中的匿名类型语法(即上面这种内联写法)连类型信息(new Func<String, Char>{ ... }这样的代码)都无法省去,因此给人非常繁琐的感觉。面对这样的代码,您可能会有和我一样的想法:“还不如最普通的写法啊”。没错,这种函数式编程的风格,由于缺乏语言特性支持,实在不适合在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)”要统一、省事一些吧。

    相关文章

    注1:严格说来,这里的body是一个“语句(statement)”,而不是“表达式(expression)”。因为一个委托其实是一个方法,因此使用Lambda来表示一个委托,其中必然要包含“语句”。不过在目前的C#中,Lambda表达式还有一个作用是构造一颗“表达式树”,而目前的C#编译器只能构造“表达式树”而不是“语句树”。

    注2:事实上,在.NET 2.0使用delegate关键字定义匿名方法时已经可以有些许“类型推演”的意味了——虽然还是必须写明参数的类型,但是我们已经可以省略委托的类型了,不是吗?

    注3:除非我们补充Func、Enumerable,Dictionary,Grouping等类型及API,否则这段代码在Java中是无法编译通过的。事实上,这段Java代码是我在记事本中写出来的。不过这个形式完全正确。

  • 相关阅读:
    平衡二叉树之RB树
    平衡二叉树之AVL树
    实现哈希表
    LeetCode Median of Two Sorted Arrays
    LeetCode Minimum Window Substring
    LeetCode Interleaving String
    LeetCode Regular Expression Matching
    PAT 1087 All Roads Lead to Rome
    PAT 1086 Tree Traversals Again
    LeetCode Longest Palindromic Substring
  • 原文地址:https://www.cnblogs.com/waw/p/2158688.html
Copyright © 2011-2022 走看看