Hello World
Spiga

Why Java Sucks and C# Rocks(5):匿名方法

2010-07-06 14:22 by 老赵, 6673 visits

确切地说,这里的标题应该是“C#中的匿名方法”,因为这是C#中特有的功能。在之前的文章里,虽然我都用长篇文字加代码示例来说明问题,但总有朋友认为我谈的只是C#和Java的“区别”,算不上优势。不过从这篇文章开始,我们将正式进入C# 2.0的时代,这也是C#大步甩开Java语言的开端——可以看出,Anders Hejlsberg从此开始实现他对于编程语言的各种理想,而并非纠缠于与Java所谓的“竞争”中。例如这篇文章要讨论的“匿名方法”特性,以及随之而来的“函数式编程”痕迹,便开始引领C#在开发理念上的进步。

委托

委托(Delegate),事实上这是在.NET 1.0(请注意不是C#,而是.NET平台的概念)时代便有的东西。不过,因为在C# 1.0中并没有提供一个“改变编程思维”的特性来体现这一概念,便没有多提。不过到了C# 2.0,既然我们要开始谈匿名方法了,便不得不提“委托”这个非常关键的概念。如果您没有接触过这个概念,不妨可以简单地将“委托”理解为一种“类型安全”的函数指针:

// C#
public delegate void Action<T>(T arg);

public delegate T Func<T>();

public delegate TResult Func<T, TResult>(T arg);

public delegate void MouseEventHandler(object sender, MouseEventArgs e);

在C#中定义委托对象时需要用到delegate关键字,然后便像声明一个方法那样指定委托名称,参数名和返回值得名称等等。委托可以带有泛型参数,这样便可以定义十分通用的委托类型,如上面的Action委托及两个Func委托。提供这种通用的委托类型对于某些编程实践有着十分重要的意义,这点在以后的文章中也会提到。不过,在还没有提供泛型支持的.NET 1.0,或者说是在C# 1.0时代,所有的委托都是如上面MouseEventHandler那样拥有的具体类型委托。

在.NET中,委托作用是引用一个“方法”,以及其调用时所需要的完整上下文,换句话说,有了一个委托对象之后,我们便可以直接“调用”这个方法了。自然,委托所引用的方法必须与委托的签名完全相同,这也是上文中“类型安全函数指针”所表示的含义。委托在调用时的开销和一个虚方法差不多,可以说它的性能非常高,因此它也是在很多情况下优化“反射调用”性能的常用手段。

在.NET中,“事件”是委托的一个重要使用场景,最近有人质疑.NET的事件是个设计上的错误,它完全应该像Java那样基于普通的接口来实现“事件”概念。对此我有不同的看法,不过这是一个较大的话题,因此我将其从现在这篇文章中剥离开来,独立成篇。而现在我先讨论其他一些委托的典型使用场景。

匿名方法及其典型使用场景

在.NET中,我们可以将委托对象作为方法的参数或是返回值来使用,例如:

// C#
static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult> f) { ... }

您可以已经意识到了,这便是所谓的“高阶函数”。高阶函数的优势有许多,简单概括一下便是“更好的抽象和组合能力”。只是在C# 1.0中,我们必须独立定义一个方法之后,才能将其构造为一个委托对象,不过从C# 2.0开始,我们可以使用“匿名方法”来构造一个委托对象,例如上面的Curry方法可以实现为:

// C#
static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult> f)
{
    // in C# 3.0: x => y => f(x, y)
    return delegate(T1 x)
    {
        return delegate(T2 y) { return f(x, y); };
    };
}

在代码中使用delegate关键字可在代码中内联地创建一个委托对象,并会在需要时形成一个闭包,您可以简单理解为调用这个匿名方法所需要的完整上下文。例如在上面这段代码中,内层的匿名函数可以访问到外层匿名函数的参数x,以及Curry方法的参数f。在C#中使用匿名函数时,可以访问字面范围内(lexical scope)的所有成员,这也逐渐让C#有了函数式编程的意味,当然这一切都还得等到C# 3.0阶段才会真正发扬光大,目前还只是C# 2.0。

匿名方法是语言的特性,和运行时没有任何关系,完全是编译器施展的魔法,于是有些人便认为这就是个无足轻重的语法糖。语法糖没错,但是“无足轻重”的评价我无法赞同。匿名函数带来了许多编程模式上的改变。由于语法特性的缺失,这些编程模式在C# 1.0或是Java语言中是麻烦到几乎无法使用的,更别提“推广”开来。关于这方面的文章我写过不少,它们都是真正用于产品开发的案例:

  • 简化回调:在异步编程中回调函数是十分常见的。有了匿名方法之后,创建一个回调函数十分容易,并且可以利用闭包直接使用回调函数中所需要的成员,在简化开发的同时,依旧保证了强类型的静态检查能力。
  • 延迟初始化器:我们可以使用匿名函数提供一个对象的初始化逻辑,并交由一个线程安全的初始化器使用。这里利用了高阶函数来封装逻辑,在传统的面向对象语言中实现这点,则往往需要利用工厂方法模式,这需要创建各种抽象类及具体类。事实上,利用匿名方法及高阶函数之后,GoF23中的许多模式,如“工厂方法”、“策略”及“模板方法”等等,都有了更加简单的实现方式,甚至完全成为自然而然的编程方法。
  • 缓存容器辅助方法:使用缓存容器时往往有着固定的模式,如“检查缓存,如果没有则访问数据库,将结果放入缓存后并返回”。有了匿名方法之后,我们可以将“访问数据库”这个操作通过参数交由缓存容器的辅助方法,辅助方法仅仅在缓存失效的情况下采取执行这个操作,这样既封装了重复的逻辑,又保证了代码的流畅性。
  • AsyncTaskDispatcher:这是一个用于简化多个异步操作之间协作关系的组件,我们只要将异步操作之间的依赖关系提供给Dispatcher,则Dispatcher便会自动调配异步操作的执行顺序。这里使用利用到匿名函数来表示各个异步操作,并利用闭包在多个异步操作之间共享状态。

自然,利用高阶函数或是匿名方法也会带来一些额外的问题,例如延迟带来的陷阱,但瑕不掩瑜,匿名方法依旧是C#中最重要的语言特性之一,也是如Scala,Python,Ruby等高级语言中的标准配置。

C#的匿名方法与Java的匿名类型

说起来,Java语言从1.4版本开始也加入了匿名类型的特性。简单地说,匿名类型是指以“内联”的方式在代码中定义一个抽象类型(即接口、抽象类甚至任何非final类)的具体实例。例如之前某篇文章中用Java实现了生成一个minInclusive到maxExclusive之间数列的迭代器:

// Java
public class Range implements Iterable<Integer> {

    private int m_maxExclusive;
    private int m_current;
    
    public Range(int minInclusive, int maxExclusive) {
        this.m_maxExclusive = maxExclusive;
        this.m_current = minInclusive;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            public boolean hasNext() {
                return m_current < m_maxExclusive;
            }
            
            public Integer next() {
                int current = m_current;
                m_current = m_current + 1;
                return current;
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }
}

在Range类的iterator方法中,我们直接返回了一个Iterator<Integer>接口的实例,这个实例直接内联地提供了接口中hasNext、next和remove三个方法的实现,并且使用了外部的m_maxInclusive及m_current字段。那么这不也是个闭包吗?没错,Java中的匿名类型的确也有一定这方面的特性,虽然使用起来比较麻烦,也不利于单元测试等等,因此一些开发实践中都不太提倡使用匿名类型(某些标准场景除外)。平心而论,我并不觉得这是个没有意义的特性,毕竟它提供了另一种选择,而且在C# 2.0之前我有时也会怀念Java语言的这个特性。

既然C#中的匿名方法和Java的匿名类型有一定的共性,那么我们便可以寻找两者之间的差异。除了语法之外,我认为两者最大的区别在于对匿名方法(类型)外的“局部变量”的操作能力上。闭包的典型使用场景之一是支持简单的并行计算。例如.NET 4.0提供了一个并行库,其中包含类似于如下接口的Parallel.For方法:

// C#
static void ParallelFor(int minInclusive, int maxExclusive, Action<int> body) { ... }

显然在.NET 2.0中我们便可以自行编写这样的方法,并配合匿名方法可以很轻松的开展简单的并行计算。例如一个并行的n * n的矩阵加法,我们便可以写作:

// C#
static int ParallelSum(int[,] array, int n)
{
    var processorCount = Environment.ProcessorCount;
    var sum = 0;

    ParallelFor(0, processorCount, delegate(int part)
    {
        var minInclusive = part * n / processorCount;
        var maxExclusive = minInclusive + n / processorCount;
        var partSum = 0;

        for (int x = minInclusive; x < maxExclusive; x++)
        {
            for (int y = 0; y < n; y++)
            {
                partSum += array[x, y];
            }
        }

        Interlocked.Add(ref sum, partSum);
    });

    return sum;
}

从代码上看,sum是ParallelSum方法的“局部变量”,不过在匿名方法内部也可以对它进行修改,例如上面的代码中就对其进行了CAS加法,因此我们可以认为在C#中的闭包在使用上是完全透明的。在Java中,如果要在匿名类型里访问外部的局部变量,则必须在局部变量声明时增加final关键字,这意味着这个局部变量是无法修改的。这么做可以避免错误共享之类的问题,但也限制我们在需要的时候必须用一点特殊的方式回避这种限制。例如在编写之前的并行矩阵相加以及AsyncTaskDispatcher代码时,则可能需要借助于这样一个包装类:

// Java
public class Wrapper<T> {
    public T value;
}

这样即便是引用Wrapper对象的局部变量不能修改,我们也能修改Wrapper对象的value字段的值。我不喜欢这样的设计,我认为这部分灵活性交由程序员来控制。C#虽然理论上有着误用的可能,但这也只是十分少见的情况,而且有了检查工具之后,误用几乎可以完全避免了。

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

40 条回复

  1. 链接

    Ivony 2010-07-06 14:51:32

    闭包是一个如此重要的特性,可以说很多语言离了闭包基本就废了。。。。

  2. 老赵
    admin
    链接

    老赵 2010-07-06 15:27:38

    @Ivony

    闭包已经可以说是标配了。

  3. 链接

    twty1988 2010-07-06 15:28:56

    目前只是用过javascript的,python了解过一点,用的不多。

  4. 链接

    twty1988 2010-07-06 15:36:39

    看了老赵这么多文章了,有个想法,可以出书了。

  5. 链接

    Ivony 2010-07-06 15:37:17

    @twty1988

    javascript没有闭包基本就废了。

  6. yemg
    60.191.246.*
    链接

    yemg 2010-07-06 15:41:50

    上次看到微软做的一个项目(Issuevission),是04年的,算是比较老了吧,里面就是把委托当成Command模式使用,非常巧妙。

  7. 老赵
    admin
    链接

    老赵 2010-07-06 15:58:15

    @Ivony

    说到这个,我最近正好给《程序员》写了篇关于Reactive Framework方面的文章。Reactive Framework有.NET版和JavaScript两种,.NET版用LINQ和Lambda最优雅好用,JavaScript还算可以接受。

    我的总体感觉是,其实Reactive Framework移植到Ruby,Python,Scala等有匿名函数和闭包的语言上都不算困难,Java的话用匿名类总是不行的了,新的Lambda语法没法省去参数的类型还是挺伤的,还有就是Checked Exception问题。总之还是挺杯具的。

  8. 老赵
    admin
    链接

    老赵 2010-07-06 15:59:28

    @yemg

    嗯,文章里也提到了,像工厂方法,策略,命令模式等等都可以用匿名函数来简化的。

  9. 老赵
    admin
    链接

    老赵 2010-07-06 16:00:53

    @twty1988: 看了老赵这么多文章了,有个想法,可以出书了。

    我算过一笔帐,如果我每篇文章都能卖给媒体的话,一个月也有几千块钱了,哈哈。不过我实在不太想受到时间约束等等,还有命题作文也不太喜欢。

  10. MagicBoy
    218.108.100.*
    链接

    MagicBoy 2010-07-06 16:02:29

    没有回复通知功能还是蛮不方便的! 为什么不用一些成熟的博客系统直接用呢? 譬如BlogEngine!

  11. zhangle
    124.130.192.*
    链接

    zhangle 2010-07-06 16:23:46

    唉,写了两三年的C#,发现自己果然是纯粹的打字员,看不大懂这些代码。找点书好好学习....

  12. 链接

    twty1988 2010-07-06 16:35:48

    @老赵 我算过一笔帐,如果我每篇文章都能卖给媒体的话...

    这么好的文章,当然希望更多人看到了。

    虽然我还是个Java程序员,但看你的文章真的收获颇多。

    你的文章不仅仅局限语言这个层次,非常的受用。

  13. 老赵
    admin
    链接

    老赵 2010-07-06 16:43:53

    @MagicBoy

    要修改现有的系统感觉太麻烦了,而且反正我的需求也不多。还有其实我想顺便做点练习,比如后台我用了MongoDB,不是关系型数据库。

  14. lee51076008
    124.117.249.*
    链接

    lee51076008 2010-07-06 18:40:09

    希望老赵出一本关于ASP.NET MVC的书,呵呵,这方面的书现在很少啊,目前我在书店只见到一本:《Web开发新体验 APS.NET 3.5 MVC架构与实战》

  15. 老赵
    admin
    链接

    老赵 2010-07-06 18:48:59

    @lee51076008

    这本书我见过:http://blog.zhaojie.me/2009/08/1552389.html

    话说我基本不会写特定框架的书的……

  16. 链接

    yyliuliang 2010-07-06 22:01:53

    要是能不光与java比较,能引申到其他语言就更好了 期待

  17. qualle2008
    77.6.4.*
    链接

    qualle2008 2010-07-07 03:26:30

    Java的一个目标是尽量减少陷阱,和C#的发展思路不一样的。

  18. 老赵
    admin
    链接

    老赵 2010-07-07 09:21:12

    @qualle2008

    说的好像C#是无所谓陷阱一样的。其实要说到陷阱,我估计最令人发指的就是Java的这个问题了:

    int a = 1000, b = 1000;
    System.out.println(a == b); // true
    
    Integer c = 1000, d = 1000;
    System.out.println(c == d); // false
    
    Integer e = 100, f = 100;
    System.out.println(e == f); // true
    
  19. 链接

    麒麟.NET 2010-07-07 10:43:33

    @老赵: 我算过一笔帐,如果我每篇文章都能卖给媒体的话,一个月也有几千块钱了,哈哈。不过我实在不太想受到时间约束等等,还有命题作文也不太喜欢。

    可以出本博文精华集,类似Joel谈软件那样的《姐夫谈.NET》

  20. Knuth
    58.246.62.*
    链接

    Knuth 2010-07-07 13:51:13

    Anders Hejlsberg从此开始实现他对于编程语言的各种理想

    这正是Anders Hejlsberg离开Borland的原因 在Borland,他无法实现自己的理想和抱负

  21. 链接

    Ivony 2010-07-07 14:24:15

    应该是《姐夫赵实战刀奈特》

    老少咸宜,不排除一些家长当作《奥特曼打小怪兽》买回家给学龄前儿童看。

  22. 链接

    Tommy 2010-07-07 14:53:09

    老赵。

    无疑和盆友谈起你的年龄。

    so。 冰天雪地裸体跪求你的真实年龄

  23. infinte
    60.166.104.*
    链接

    infinte 2010-07-07 17:40:59

    @tommy: 23

  24. 链接

    mfjt55 2010-07-08 09:14:44

    说的好像C#是无所谓陷阱一样的。其实要说到陷阱,我估计最令人发指的就是Java的这个问题了:

    这个是怎么回事,感觉很奇怪啊,第二条代码和第三条的不一样吗。

  25. Duron800
    207.46.92.*
    链接

    Duron800 2010-07-08 17:03:23

    我记得C# 2.0好像没有Func,Action之类的?

  26. 老赵
    admin
    链接

    老赵 2010-07-08 17:57:18

    @Duron800

    类库不自带,自己声明就行了。

  27. 四有青年
    210.13.101.*
    链接

    四有青年 2010-07-09 10:16:31

    汗死,打开eclipse一试,还真是,看来对JAVA了解还是很肤浅,有没人解释下?

  28. 四有青年
    210.13.101.*
    链接

    四有青年 2010-07-09 10:49:02

    下面这个地址貌似给出了解释,如果里面是真的,那就有点搞笑了

    http://jessdy.javaeye.com/blog/174011

  29. 老赵
    admin
    链接

    老赵 2010-07-09 11:14:59

    @四有青年

    本来就是这个原因么,呵呵

  30. 链接

    liny4cn 2010-07-15 16:56:21

    java的匿名类很不错。允许从接口生成实例。 .net的匿名类很自由,但完完全全不知道什么类型。

    希望.net能够更进一步,把java的匿名类的实现也加进来,这样就完美了。还可以同时解决无法直接使用从页面上用dynamic来使用action传过来的匿名类的问题。

  31. Jason
    119.6.78.*
    链接

    Jason 2010-08-04 15:25:56

    我觉得泛型,以及泛型委托,是2.0最为强大的地方。

  32. Will Meng
    124.205.74.*
    链接

    Will Meng 2010-08-06 16:47:46

    如果没有匿名方法,不知道我的代码要多写多少,维护起来要多麻烦

  33. eflay
    61.171.25.*
    链接

    eflay 2012-07-14 18:58:28

    最近转公司可能要转JAVA,看到java连匿名方法和扩展方法都没有的时候,看到访问器还只是方法,快哭了。。LINQ也就不去想了,或许是我看的书太老了, 幸好java7.0算是有了lambda,是不是该去看看怎么结合groovy

  34. 老赵
    admin
    链接

    老赵 2012-07-14 22:15:50

    @eflay

    Java 8采用lambda……不过还是缺了好多。

  35. wpp
    106.187.88.*
    链接

    wpp 2013-05-22 14:09:02

    做一个简单的定时器,回调函数,但是当 回调函数的对象被删掉的时候,怎么能优雅的删除掉定时器管理器中相关的记录呢???

  36. wpp
    106.187.88.*
    链接

    wpp 2013-05-22 14:09:51

    其实也就是说,怎么防止,匿名函数中用到的上下文被删除的情况??

  37. 老赵
    admin
    链接

    老赵 2013-05-22 21:45:48

    @wpp

    匿名函数是个委托,会自己保留上下文的,除非把引用都摘掉,否则不会自动被删掉之类的。

  38. RobinHan
    124.42.38.*
    链接

    RobinHan 2013-06-03 17:45:27

    委托在多线程中可以自动切换线程上下文, 老赵对这个有什么研究吗? 另现在越来越感觉c#从静态语言(javascript)吸取了很多东西, 象linq 对集合的一些操作象透了underscore.js

  39. 老赵
    admin
    链接

    老赵 2013-06-03 23:52:55

    @RobinHan

    不懂什么叫做“委托在多线程中可以自动切换线程上下文”,而且JavaScript是静态语言?还有你看看LINQ跟underscore.js是哪个先出现的再说吧……

  40. RobinHan
    76.164.231.*
    链接

    RobinHan 2013-06-07 00:01:08

    打错了哥哥 ,javascript 是动态语言 其他的不解释了

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我