C#委托,匿名方法和Lambda表达式(译)

在.net中,委托,匿名方法和Lambda表达式是三个很容易让人混淆的概念.以下代码或许可见一斑:对First的调用中,哪些(个)会被编译?哪些(个)将会返回我们所期待的答案?(ID号为5的Customer).事实上,答案就是:所有的6种方法不令都将编译,而且它们都能够返回正常的customer,它们在功能上是相同的.如果你还在问自己:为什么是这样呢?那么,这篇文章将为你解答.

class Customer
{
    public int ID { get; set; }
    public static bool Test(Customer x)
    {
        return x.ID == 5;
    }
}
...
List custs = new List ();
custs.Add( new Customer() { ID = 1 });
custs.Add( new Customer() { ID = 5 });

custs.First( new Func bool>( delegate(Customer x) { return x.ID == 5; }));
custs.First( new Func bool>((Customer x) => x.ID == 5));
custs.First( delegate(Customer x) { return x.ID == 5; });
custs.First((Customer x) => x.ID == 5);
custs.First(x => x.ID == 5);
custs.First(Customer.Test);

一,什么是委托?

举例来说吧,假如有一个购物篮类(Shoppingcart)用于处理顾客(Customer)的订单(Order).经理决定对超过一定金额或购买量等的顾客打折.于是,你必须使用他们制订的策略来计算订单.这并不是什么难事:你简单的声明了一个变量来保存折扣将在计算订单金额时调用它.

class Program 
{ 
    static void Main(string[] args) 
    { 
        new ShoppingCart().Process(); 
    } 
} 
 
class ShoppingCart 
{ 
    public void Process() 
    { 
        int magicDiscount = 5; 
        // ... 
    }
}

然而,第二天,精明的经理又决定要根据一天当中的时间来打折.呵呵,这也不难,你只需要简单的改变一下代码就可以了:

class ShoppingCart 
{ 
    public void Process() 
    { 
        int magicDiscount = 5; 
        if (DateTime.Now.Hour < 12) 
        { 
            magicDiscount = 10; 
        } 
    } 
}

接下来几天里,经理一再增加或修改折扣的计算方法.晕倒!我怎么才能处理和维护这近似荒谬的逻辑呀?其实,你所需要做的只是"移交",或者称之为"委托"责任给别人就可以了.在.net中,就有这么一种机制,我不说你也一定猜到了,那就是"委托".

二,委托

如果大家有C/C++背景的话,那么最佳描述委托的就是函数指针.而对于一般人来说,可以认为它是一种把方法像普通参数一样传递的方式.例如,以下三行代码体现了同样的基本原则:向Process方法传递一条数据,而不使用它.

// passing an integer value for the Process method to use 
Process( 5 ); 
// passing a reference to an ArrayList object for the Process method to use 
Process( new ArrayList() ); 
// passing a method reference for the Process method to call 
Process( discountDelegate ); 

那么,上面的discountDelegate是什么呢?我们该怎么去创建它呢?Process方法又是怎样使用它的呢?首先,我们需要做的是声明一个delegate类型就像我们声明一个类一样.

delegate int DiscountDelegate(); 

这名代码让我们拥有了一个DiscountDelegate委托,我们可以像类和结构一样使用它.这个委托没有参数,只有一个int型的返回值.和类一样,在创建它的任何实例之前,我们不能使用它.在实例化委托的时候,需要特别注意:委托只是某方法的一个引用而已,即使DiscountDelegate没有任何构造函数,当实例化的时候,会有一个隐藏的构造函数(无参数但返回int值).怎样给这个构造函数一个方法呢?OK,在.net中,我们只需要简单的输入想要调用的方法的名称就可以了,省略了方法名后面的括号.

DiscountDelegate discount = new DiscountDelegate(class.method); 

在进一步探讨之前,让我们回过头来看看之前的例子.我们将增加一个Calculator 类来帮助我们计算折扣,并设计一些方法来提供给委托引用.

delegate int DiscountDelegate(); 
 
class Program 
{ 
    static void Main(string[] args) 
    { 
        Calculator calc = new Calculator(); 
        DiscountDelegate discount = null; 
        if (DateTime.Now.Hour < 12) 
        { 
            discount = new DiscountDelegate(calc.Morning); 
        } 
        else if (DateTime.Now.Hour < 20) 
        { 
            discount = new DiscountDelegate(calc.Afternoon); 
        } 
        else 
        { 
            discount = new DiscountDelegate(calc.Night); 
        } 
        new ShoppingCart().Process(discount); 
    } 
} 
 
class Calculator 
{ 
    public int Morning() 
    { 
        return 5; 
    } 
    public int Afternoon() 
    { 
        return 10; 
    } 
    public int Night() 
    { 
        return 15; 
    } 
} 
 
class ShoppingCart 
{ 
    public void Process(DiscountDelegate discount) 
    { 
        int magicDiscount = discount(); 
        // ... 
    } 
}

正如我们所看到的,我们在Calculator 类中为每一个折扣的计算逻辑创建了一个方法.在Main方法中,我们为Calculator类和DiscountDelegate 委拖分别创建了一个实例,它们将共同为我们即将调用的目标方法服务.

到目前为止,我们不再为Process 方法里的计算逻辑而担忧啦,我们可以简单的调用委托来完成.不过要记住:我们并不关心这个委托是如何,何时创建的.我们只是在需要它的时候像调用其它方法一样调用它.可见,委托也可以理解为延缓方法执行.真正的计算方法是在我们调用discount()方法的时候才得以执行.再看看上面的代码,还有一些重复的地方.在Calculator 类中,我们是不是可以用一个方法来提供所有返回值?当然可以,那就让我们一起来改进吧.

delegate int DiscountDelegate(); 
 
class Program 
{ 
    static void Main(string[] args) 
    { 
        new ShoppingCart().Process(new DiscountDelegate(Calculator.Calculate)); 
    } 
} 
 
class Calculator 
{ 
    public static int Calculate() 
    { 
        int discount = 0; 
        if (DateTime.Now.Hour < 12) 
        { 
            discount = 5; 
        } 
        else if (DateTime.Now.Hour < 20) 
        { 
            discount = 10; 
        } 
        else 
        { 
            discount = 15; 
        } 
        return discount; 
    } 
} 
 
class ShoppingCart 
{ 
    public void Process(DiscountDelegate discount) 
    { 
        int magicDiscount = discount(); 
         // ... 
    } 
}

这下好啦,我们做到啦.静态的Calculate 方法让类变得清爽了很多.Main方法也不再有那么多的对DiscountDelegate 的引用啦.好,现在,我们已经对委托有所了解了.

三,我们需要这样的功能!

在.NET 2.0中,引入了泛型的概念.MS小心翼翼的通过提供Action 类来迈向泛型委拖.然后,我认为,一段时间之后,它被我们大多数人所遗忘了.后来,到了3.5,MS很友好的为我们预定义了一些常见的委托来使用,所以我们不再需要不断的定义是我们自己的委托啦.他们还扩展了Action并增加了Func.Action和Func唯一的不同就是前者没返回值而后者有.

也就是说,我们不再需要去声明DiscountDelegate ,我们可以使用Func 来代替它.为们演示参数的工作方法,让我们假设经理再一次改变了折扣的计算逻辑:现在,我们需要记录一个特别的折扣.这也是小菜一碟,我们只需要在Calculate 方法中调用一个boolean类型的值就可以啦.

这样,我们的原本无参委托就变成了Func 的形式了.注意,现在的 Calculate 方法有一个boolean类型的参数,我们在调用 discount的时候要带上它.

class Program 
{ 
    static void Main(string[] args) 
    { 
        new ShoppingCart().Process(new Func<bool, int>(Calculator.Calculate)); 
    } 
} 
 
class Calculator 
{ 
    public static int Calculate(bool special) 
    { 
        int discount = 0; 
        if (DateTime.Now.Hour < 12) 
        { 
            discount = 5; 
        } 
        else if (DateTime.Now.Hour < 20) 
        { 
            discount = 10; 
        } 
        else if (special) 
        { 
            discount = 20; 
        } 
        else 
        { 
            discount = 15; 
        } 
        return discount; 
    } 
} 
 
class ShoppingCart 
{ 
    public void Process(Func<bool,int> discount) 
    { 
        int magicDiscount = discount(false); 
        int magicDiscount2 = discount(true); 
    } 
}

这主意不错,我们又节省了几行代码,不是吗?当然不是,如果利用类型接口,我们还可以节省更多的代码和时间..net允许我们完全省略Func 的创建过程,只要这个方法具有和我们期望的委托同样的参数和返回值.

// works because Process expects a method that takes a bool and returns int 
new ShoppingCart().Process(Calculator.Calculate); 

基于这一点,我们通过省略自定义委托并在随后略去Func委托的直接创建,又省下了不少代码.还有什么方法可以减少代码量的吗?当然有,因为我们才说了本文的一半.

四,匿名方法

匿名方法允许我们声明一个没有名字的方法.在后台,有一个名叫'normal'的方法.然而,在代码中我们不能显式的调用这个方法,匿名方法只能在使用委托,并且使用Delegate关键字创建时创建和使用.如:

class Program 
{ 
    static void Main(string[] args) 
    { 
        new ShoppingCart().Process( 
            new Func<bool, int>(delegate(bool x) { return x ? 10 : 5; } 
        )); 
    } 
}

从上面我们可以看到:我们完全不用Calculator 类.你可以输入更多的小逻辑到花括号的分枝中,就像在其它方法中使用的一样.如果你觉得难以理解这段代码的工作过程,就请假设声明delegate(bool x) 是一个方法的签名而不是一个委托的关键字.把这段代码放到一个类中,delegate(bool x) { return 5; }是一个特别的逻辑计算的方法的声明(本应有返回值类型的).这方面来理解,就好办啦.delegate 只是使原本方法的名称隐藏起来了而已.

好,我相信到目前为止,我们可以省略更多的代码啦.自然的,我们可以忽略对Func委托的显示声明,当我们使用delegate关键字的时候,.net会替我们处理好一切.

class Program 
{ 
    static void Main(string[] args) 
    { 
        new ShoppingCart().Process( 
          delegate(bool x) { return x ? 10 : 5; } 
        ); 
    } 
}

在.net中,当方法期望一个委托作为参数并响应事件的时候,才能看见匿名方法的真正威力.以前,我们必须为每一个可能的动作创建方法.而现在,我们只需要一行语句就搞定了,而且不会"污染"命名空间.

// creates an anonymous comparer 
custs.Sort(delegate(Customer c1, Customer c2) 
{ 
    return Comparer<int>.Default.Compare(c1.ID, c2.ID); 
}); 
 
// creates an anonymous event handler 
button1.Click += delegate(object o, EventArgs e) 
                      { MessageBox.Show("Click!"); }; 

五,Lambda表达式

(引自MSDN)Lambda表达式是一个能够包含表达式和语句,并且能够用来创建委托或表达式树类型的匿名函数.我们需要理解"用来创建委托"部分,Lambda表达式到底扮演着怎么的着色呢?好,其实上,表达式和表达式树已超出了本方的阐述范围.我们只需要知道:表达式就是.net程序中所谓的数据或对象本身的代码而已.表达式树是一种其它代码可以"询问"的表达逻辑的方式.当lambda 表达式被转换成表达式树时,编辑器并没生成新的IL代码,表达式树和lambda 表达式代表了相同的逻辑.

我们所需要关注的是:用lambda 表达式代替匿名方法并增加新的特性.回顾我们最后的那个例子,我们已经在最初创建的基础上去掉了很多代码,将把折扣计算逻辑写到了一行上.

class Program 
{ 
    static void Main(string[] args) 
    { 
        new ShoppingCart().Process( 
            delegate(bool x) { return x ? 10 : 5; } 
        ); 
    }
}

你相信我们还能让它变得更简短一些吗?Lambda 表达式使用"=>"符号来代表"参数被传递到表达式中",编译器遇到这个符号的时候会允许我们忽略参数的类型,并且为我们推断出类型来.如果有两个或两个以上的参数,可以使用括号:(x,y) =>. 如果只有一个,那就这样:x =>.

static void Main(string[] args) 
{ 
    Func<bool, int> del = x => x ? 10 : 5; 
    new ShoppingCart().Process(del); 
} 
// even shorter... 
static void Main(string[] args) 
{ 
    new ShoppingCart().Process(x => x ? 10 : 5); 
}

对,就是这样的.x被推断成boolean,返回值类型也一样.因为Process 有一个Func 类型的参数,如果你想像前面一样执行这段代码,我们只需要增加分枝就可以了.

static void Main(string[] args) 
{ 
    new ShoppingCart().Process( 
    x => { 
        int discount = 0; 
        if (DateTime.Now.Hour < 12) 
        { 
            discount = 5; 
        } 
        else if (DateTime.Now.Hour < 20) 
        { 
            discount = 10; 
        } 
        else if(x) 
        { 
            discount = 20; 
        } 
        else 
        { 
             discount = 15; 
        } 
        return discount; 
    }); 
} 

六,最后的事

在是否使用分枝的问题上,一个很大的不同.当你使用的时候,相当于是创建了一个"语句式的lambda'",反之,它就是一个"表达式式的lambda",前者可以根据分枝需要执行多行语句,但不能创建表达式树.当我们在使用IQueryable 接口的时候,有可能会陷入这个问题中, 下面的例子展示了这一问题"

List<string> list = new List<string>(); 
IQueryable<string> query = list.AsQueryable(); 
list.Add("one"); 
list.Add("two"); 
list.Add("three"); 
 
string foo = list.First(x => x.EndsWith("o")); 
string bar = query.First(x => x.EndsWith("o")); 
// foo and bar are now both 'two' as expected 
foo = list.First(x => { return x.EndsWith("e"); }); //no error 
bar = query.First(x => { return x.EndsWith("e"); }); //error 
bar = query.First((Func<string,bool>)(x => { return x.EndsWith("e"); })); //no error 

第二次计算bar值的时候发生了编辑时错误.这是因为IQueryable.First 期望一个表达式做为参数,然而,表达式方法List .First 则期望一个委托.你可以强制将lambda 表达式的值转换为delegate,像上面第三次一样.

写到这里,其实还有很多未言的东西,但是我想我必须要结束本文了.lambda 基本上被分为两类:一类是创建匿名方法和委托,另一类是创建表达式.

七,总结

希望这编文章可以达到解答最前面的六个易混淆的有关委托,匿名方法和lambda表达式调用问题的目标.

<完>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值