一起谈.NET技术,从.NET中委托写法的演变谈开去(上):委托与匿名方法

在《关于最近面试的一点感想》一文中,Michael同学谈到他在面试时询问对方“delegate在.net framework1.1,2.0,3.5各可以怎么写”这个问题。于是乎,有朋友回复道“请问楼主,茴香豆的茴有几种写法”,“当代孔乙己”,独乐,众乐。看了所有的评论,除了某些朋友认为“的确不该不知道这个问题”之外,似乎没有什么人在明确支持楼主。

不过我支持,为什么?因为我也提过出这样的问题。

这样,我们暂且不提应聘“高级开发人员”的人,在“自称熟悉各版本.NET框架”的前提下,是否应该知道这个答案。我们也暂且不提Michael同学提问的“目的”是什么。老赵就先单独针对这个问题进行解释,然后谈谈自己为什么会提出这个问题吧。

可能有一件事情需要说在前面,那就是:委托本身其实从来没有改变过,改变的一直都是委托的“写法”。因此更确切地说,改变的只是“编译器”。而本文所有内容都用C#来实现,其实谈得也都是C#编译器本身——但是其实VB.NET也有变化啊。再由于.NET版本和C#版本的关系也是非常密切的,因此全文就使用.NET版本进行指代了。

.NET 1.x中委托的写法

委托,如果不追究细节,从表面上来看我们可以将其通俗地理解为一个安全的“函数指针”。当然,这个函数指针其实也是一个对象,有自己的成员,也会封装了被调用方的上下文等等。至于委托的定义和使用方式,则是这样的:

public delegate int SomeDelegate(string arg1, bool arg2);
public static int SomeMethod(string arg1, bool arg2) { return 0; }
public class SomeClass
{
    public int SomeMethod(string a1, bool a2) { return 0; }
    public event SomeDelegate SomeEvent;
}
static void Main(string[] args)
{
    SomeClass someClass = new SomeClass();
    SomeDelegate someDelegate = new SomeDelegate(someClass.SomeMethod);
    someClass.SomeEvent += new SomeDelegate(SomeMethod);
}

可见,在.NET 1.x中需要使用new DelegateType(...)的方式来创建一个委托对象。不过,作为委托对象内部的方法它既可以是实例方法,也可以是静态方法。此外,方法只需要匹配委托类型的签名和返回值即可,方法参数的名称不会成为约束。

嗯,就是这么简单。

.NET 2.0中委托的写法

.NET中的委托引入了范型,且写法略有简化:

public delegate TResult MyFunc<T1, T2, TResult>(T1 a1, T2 a2);
public static int SomeMethod(string a1, bool a2) { return 0; }
static void Main(string[] args)
{
    MyFunc<string, bool, int> myFunc = SomeMethod;
}

在.NET 2.0中,new DelegateType已经可以省略,开发人员可以直接将方法赋值给一个委托对象的引用。当然,这个改进不值一提,.NET 2.0中委托写法的关键在于引入了“匿名方法”:

public static void TestRequest(string url)
{
    WebRequest request = HttpWebRequest.Create(url);
    request.BeginGetResponse(delegate(IAsyncResult ar)
    {
        using (WebResponse response = request.EndGetResponse(ar))
        {
            Console.WriteLine("{0}: {1}", url, response.ContentLength);
        }
    },
    null);
}

匿名方法,简单地说就是内联在方法内部的委托对象,它的关键便在于形成了一个闭包(委托执行时所需的上下文)。如上面的代码中,BeginGetResponse的第一个参数(委托)可以直接使用TestRequest方法的参数url,以及方法内的“局部”变量request。如果没有匿名函数这个特性的话,代码写起来就麻烦了,例如在.NET 1.x中您可能就必须这么写:

展开
折叠
public static void TestRequest(string url)
{
    WebRequest request = HttpWebRequest.Create(url);
    object[] context = new object[] { url, request };
    request.BeginGetResponse(TestAsyncCallback, context);
}
public static void TestAsyncCallback(IAsyncResult ar)
{
    object[] context = (object[])ar.AsyncState;
    string url = (string)context[0];
    WebRequest request = (WebRequest)context[1];
    using (WebResponse response = request.EndGetResponse(ar))
    {
        Console.WriteLine("{0}: {1}", url, response.ContentLength);
    }
}

此时,我们往往会发现,开发人员需要花费大量的精力,为一小部分代码维护一大段上下文。例如在这段代码中,我们会将url和request对象塞入一个object数组中,在回调函数中再通过危险的Cast操作恢复数据。如果您希望“强类型”,那么只能为每个回调创建一个新的上下文对象,维护起来可能更加麻烦——要知道,在并行编程,异步调用越来越重要的今天,如果没有匿名方法自动保留上下文的特性,开发人员会为这些“额外工作”疲于奔命的。

可能您会说,匿名方法的可读性不佳,因为需要“内联”。一个方法中内联太多,维护成本就上去了,所以匿名方法并不推荐使用。我想说的是,您错了。如果为了可维护性,要将方法独立拆开,也可以利用匿名方法的优势:

public static void TestRequest(string url)
{
    WebRequest request = HttpWebRequest.Create(url);
    request.BeginGetResponse(delegate(IAsyncResult ar)
    {
        TestAsyncCallback(ar, request, url);
    }, null);
}
public static void TestAsyncCallback(IAsyncResult ar, WebRequest request, string url)
{
    using (WebResponse response = request.EndGetResponse(ar))
    {
        Console.WriteLine("{0}: {1}", url, response.ContentLength);
    }
}

如果借助.NET 3.5中的Lambda表达式,代码可以写的更简单易读:

public static void TestRequest(string url)
{
    WebRequest request = HttpWebRequest.Create(url);
    request.BeginGetResponse(ar => TestAsyncCallback(ar, request, url), null);
}

匿名方法的作用

千万不要小看匿名方法的作用,有些时候您认为它的作用仅限于上文描述,只是因为没有在某些问题上踏前一步。例如,对于那些只需要“按需创建”,且要“线程安全”的对象,您会怎么做呢?没错,可以使用Double Check:

private object m_mutex = new object();
private bool m_initialized = false;
private BigInstance m_instance = null;
public BigInstance Instance
{
    get
    {
        if (!this.m_initialized)
        {
            lock (this.m_mutex)
            {
                if (!this.m_initialized)
                {
                    this.m_instance = new BigInstance();
                    this.m_initialized = true;
                }
            }
        }
        return this.m_instance;
    }
}

嗯,做的很漂亮!那么……这样的属性再来一个,再来三个,再来五个呢?可能有些朋友就会开始大段地Copy & Paste,于是错误便难免了。这里有一件真人真事,以前某位同学在一堆这样的代码中迷茫了,说为什么用了这种方法,还是初始化了多次对象了?检查了半天没有看出问题来。最后发现,原因是访问了错误的initialized变量(例如,在某个应该访问artistInitialized的地方访问了articleInitialized)。可惜,大段时间已经被浪费了——更糟的是,心情也随之变差了。

其实,Copy & Paste很明显没有遵守DRY原则啊。为什么不把它们封装在一处呢?例如:

展开
折叠
public class Lazy<T>
{
    public Lazy(Func<T> func)
    {
        this.m_initialized = false;
        this.m_func = func;
        this.m_mutex = new object();
    }
    private Func<T> m_func;
    private bool m_initialized;
    private object m_mutex;
    private T m_value;
    public T Value
    {
        get
        {
            if (!this.m_initialized)
            {
                lock (this.m_mutex)
                {
                    if (!this.m_initialized)
                    {
                        this.m_value = this.m_func();
                        this.m_func = null;
                        this.m_initialized = true;
                    }
                }
            }
            return this.m_value;
        }
    }
}

于是,之前的代码就可以简化成这样了:

private Lazy<BigInstance> m_lazyInstance =
    new Lazy<BigInstance>(delegate { return new BigInstance(); });
public BigInstance Instance { get { return this.m_lazyInstance.Value; } }

还是太丑,上Lambda表达式!

private Lazy<BigInstance> m_lazyInstance =
    new Lazy<BigInstance>(() => new BigInstance());
public BigInstance Instance { get { return this.m_lazyInstance.Value; } }

如果没有匿名方法,许多容易使用的编程模型和方式都难以开展。例如,我们就不会有CacheHelper,也不会有AsyncTaskDispatcher(上),也很难利用“延迟”所带来的便利,更难以出现微软并行扩展、CCR等优秀框架。可以这么说,如果您不善于使用委托,您如果不知道如何合适地使用匿名方法,您在不自知的情况下可能就已经编写了大量额外的代码了。

老赵平时的工作之一,便是为项目提供各种扩展API,可以让程序员们更愉快地进行开发工作,得到更好的生产力,让代码变得更加美好。如今C#有了匿名方法、Lambda表达式、表达式树、扩展方法等优秀的语言特性,真让我有“如鱼得水”的感觉。因此,我对于Java这样不思进取的语言可以说深恶痛绝(Java朋友们赶快学习Scala吧)。在看阅读大量Java开源项目代码时,我常有这样的感觉:“如果是C#的话,利用匿名方法,这个类不就可以不写,那个类就可以省略……”。没错,为了保留回调函数的上下文而创建一些类,对于C#程序员来说,的确是一件有些不可思议的事情。

至于Lambda表达式以及其他话题,我们下次再说吧。

匿名方法的缺点

匿名方法的优势在于自动形成闭包,而它的缺点也是让程序员“不自觉”地创建了闭包,这会让某些对象的生命周期加长。例如在一开始的TestRequest方法中,表面上看起来url是参数,request是局部变量,有些朋友可能会认为它们在方法退出后就已经准备回收了。不过因为形成了闭包,url和request已经“升级”为一个对象的域变量,它的生命周期延长了,延长至回调函数执行完毕。因此,一不注意可能就会产生一些莫名其妙的情况

其实,这些都是“延迟”所带来的陷阱,作为一个优秀的开发人员,除了知道某个东西的作用和优势,也要知道它的问题,不是吗?

总结

您现在还觉得.NET中的“委托”是一个简单的,只适合“初学者”,一搜就都知道的概念吗?

您可能会说“是”,不过对我来说这真不简单,我也是慢慢才意识到这些的。虽然关于委托的大量内容可以在互联网上搜索而来,但还是有太多东西是需要在大量编程实践中积累下来的——等一下,这不就是“高级开发人员”和“初学者”的主要区别之一吗?

嘲笑孔乙己的朋友们,你们在一味鄙视“茴”的四种写法的同时,说不定也失去了一个了解中国传统文化的机会呢!

剩下的下次再说吧,Lambda表达式还等着我们。

相关文章

时间: 2024-09-26 20:11:26

一起谈.NET技术,从.NET中委托写法的演变谈开去(上):委托与匿名方法的相关文章

一起谈.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).参数列表

从.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).参数列表

从.NET中委托写法的演变谈开去(上):委托与匿名方法

在<关于最近面试的一点感想>一文中,Michael同学谈到他在面试时询问对方"delegate在.net framework1.1,2.0,3.5各可以怎么写"这个问题.于是乎,有朋友回复道"请问楼主,茴香豆的茴有几种写法","当代孔乙己",独乐,众乐.看了所有的评论,除了某些朋友认为"的确不该不知道这个问题"之外,似乎没有什么人在明确支持楼主. 不过我支持,为什么?因为我也提过出这样的问题. 这样,我们暂且不提应

一起谈.NET技术,C#中的委托,匿名方法和Lambda表达式

简介 在.NET中,委托,匿名方法和Lambda表达式很容易发生混淆.我想下面的代码能证实这点.下面哪一个First会被编译?哪一个会返回我们需要的结果?即Customer.ID=5.答案是6个First不仅被编译,并都获得正确答案,且他们的结果一样.如果你对此感到困惑,那么请继续看这篇文章. class Customer { public int ID { get; set; } public static bool Test(Customer x) { return x.ID == 5; }

一起谈.NET技术,.NET中通过代理实现面向方面编程(AOP)

上篇文章我说到了在代码中可以利用泛型委托来封装异常处理,这样可以让程序看起来更加清晰,要想完成功能需要调用者调用指定的工厂方法才行,但要想改变某些程序员的编码习惯我想是一件比较困难的事情.有朋友说利用委托来实现异常处理并不算是真正意义上的AOP,因为传统的AOP并不需要客户端做代码结构的变更,最多也就是配置上的问题.但在.net中要想实现AOP,我想最方便的实现机制要属代理机制了,但只要利用代理,在性能上就会造成一定的影响. 如果开发过分布式服务,像remotion,wcf等,消息都是它们通信的

一起谈.NET技术,.NET中的委托

1.1.1 定义 委托是一种引用方法的类型.一旦为委托分配了方法,委托将与该方法具有完全相同的行为.委托方法的使用可以像其他任何方法一样,具有参数和返回值,如下面的示例所示: //Code in C# public delegate int PerformCalculation(int x, int y); 与委托的签名(由返回类型和参数组成)匹配的任何方法都可以分配给该委托. 简单理解Delegate委托(或代理)是一种数据类型:它的变量可以引用到某一个符合要求的方法上,通过委托可以间接地调用

一起谈.NET技术,C#中的语言特性都是从何而来?

前几天推荐一个同事用"可选参数",推荐完了我还画蛇添足的说这是.Net 4中的新特性.但是事后才发现这个新特性是C# 4.0的语言特性,与.Net 4无关.其实也不只这一次,我平时也经常把语言.框架.运行时,有时甚至还有开发工具混为一谈.于是今天就总结一下C#中我感兴趣的几个语言特性是从何而来的. 1.可选参数 可选参数是C# 4.0中的新特性,其作用在于在调用者不提供参数值时给参数一个默认值,用起来是这样的: static void Main(string[] args){ Test

一起谈.NET技术,.NET 中的二进制浮点类型

大多数人会对他们在.NET中的算术的"出错"首先感到惊讶.使用一些称为"浮点"算术来表示非整型数字不是.NET 相比其他大多数语言/平台特殊的地方.在.NET 内部是没问题的,但是你需要知道一些底层正在发生什么,否则你将会对一些结果感到惊讶. 我在这个事情上不是一个专家这不重要.虽然写了这篇文章,我也发现了另外一篇 - 这次是一个真正的专家写的,杰弗里 萨克斯(Jeffrey Sax).我强烈建议你也同时读他的浮点文章. 什么是浮点数? 计算机总是需要一些表示数据的

一起谈.NET技术,.NET中的异步编程(二)- 传统的异步编程

在上一篇文章中,我们从构建响应灵敏的界面以及构建高可伸缩性的服务应用来讨论我们为什么需要异步编程,异步编程能给我们带来哪些好处.那么知道了好处,我们就开始吧,但是在异步编程这个方面,说总是比做简单.套用那句不是名言的名言:编写异步程序是困难的,编写可靠的异步程序尤其困难.因为异步程序非常难以编写,而且非常容易出错,很多基本的构造元素在异步编程中都无法使用,这让我们这些开发人员更愿意编写同步的代码,虽然我们知道有些地方真的应该使用异步. 如何实现异步 对于很多人来说,异步就是使用后台线程运行耗时的