[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)

时间:2022-11-09 19:06:58

Lambda表达式带来了什么
连载地址:http://www.atmarkit.co.jp/fdotnet/csharp30/index/index.html
原文地址:http://www.atmarkit.co.jp/fdotnet/csharp30/csharp30_01/csharp30_01_02.html

本次的内容,主要是针对已经学习过C# 2.0的程序员读者的,前提是已经知道什么是匿名方法。如果还不清楚,请先阅读“
连载:C# 2.0入门”(这篇会在今后翻译)

好的,现在进入正题。

Lambda表达式(λ表达式),用一句话来解释,(不算很严谨)就是使匿名方法文字上更短的语法。虽然这样说,单“仅仅是文字上的变化,源代码的性质没有变”这样的想法也是一种误解。规模变化了,其性质也会变化。例如,实验室的烧杯中产生的现象,不一定会在大型的工厂里产生。同样的道理也适用于源代码。

那么,就体验一下Lambda表达式吧。

以下,使用具体的code来说明一下,不是实际工程中的代码,而是实际代码使用C# 2.0重写的。

前一阵子笔者实际写的code中,有一个菜单,能够选择的下拉菜单。菜单项是下面这样定义的:

[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)Code
List 1 菜单项的定义

与之相对应,以下是菜单项的数组。

1 [C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)private   static  MenuItemA[] Menu Items1  =
2 [C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)   {
3[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)    new MenuItemA("选择项1", 执行方法),
4[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)    new MenuItemA("选择项2", 执行方法),
5[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)    new MenuItemA("选择项3", 执行方法),
6[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)  }
;
7 [C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)
List 2 菜单项目数组

 实际上,当时认为这样就足够了,谁知中途又被要求加入一种菜单项,这种菜单项在19点以后才可以看到。如果只有这一个的话,用if语句括起来判断一下例外条件就能够处理,可是要求是2个,而且还可能增加。于是,就想在这个表中添加条件语句。

最simple的解决方案,应该就是在MenuItem类里,保存“几点以后有效”的“几点”的整数值。

首先在MenuItemA类中,加上保存时间的整数字段“FromHour”。

[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)Code
List 3 加上时间字段的菜单项定义

菜单项数组也改写成以下:

[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)Code

List 4 对List 3的菜单项进行修正 
这样,需要的信息都能够包含在数组里了。

构建菜单的方法是,通过检查被选择的菜单对象的FromHour的值,如果与现在时间相比小,就把该菜单项显示出来。

这么看来,这个code如果按照YAGNI*的原则来看的话就比较完善了,这样的代码也属于良品了。

* YAGNI是“You Aren't Going to Need It.”的简写,意思是:或许是必要的功能实际上并不必要的可能性非常高。一句话,为未知的未来而事先准备的代码,基本上是没用的。这样的教训很多。

 

但是,这个代码来应对需求变更的要求,恐怕还太脆弱。例如,条件如果从19点改为19点半,就没法办了。或者要求设置个结束时间,或者是个时间段的话,或是根据星期几而变动,这种要求实在是太多了。

琢磨了一下要求,加入能够指定条件的代码,修正后如下:

[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)Code

List5 加上条件的菜单定义
如果使用匿名方法,数组就要改写成如下:

Menu [C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)Code
List 6 List 5中对应的菜单项

可是,这样的代码如果要为将来可能需要也可能不需要的变化准备的话,将会变得相当臃肿。本质上完全没有意义的delegate和return显得非常刺眼,看上去很难理解意图。这种代码就是YAGNI原则所说的那种应该避免的代码的典型例子。

因此,如果使用C# 2.0,这种code应该就不会被采用。笔者虽然属于那种对匿名方法使用得挥金如土的类型,在这个case上,(用匿名方法)恐怕优势要小于劣势。

然而,下面这并不是匿名方法,打眼一看就是较少的文字就能描述的Lambda表达式。下面的代码,“()=>true”和“()=>DateTime.Now.Hour > 19”,一看就是Lambda表达式的样子。


[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)Code

List 7 对List 6改用Lambda表达式后
说实话,code写成这样应该可以通过了。虽然违反了YAGNI的原则,但在不损害code的可理解性的范围内,对于未知修改来说也是上了保险了。

事实上这个保险也确实起作用。很快,菜单的有效时间从“19点以后”变成“19点后22点前”。来吧,数组做如下修正:

 

[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)[C# 3.0 入门] [第一章 Lambda表达式] 第二节:Lambda表达式带来了什么(日文翻译)Code
List 8 List 7中第四个菜单项的修改


这个变更,只用改写一个Lambda表达式这样的局部变更就能搞定,一瞬间的事。但是,如果采用最初的List 4的code,菜单项类还要加上结束时间,菜单的构建方法中还要加上判断,相当的费事。然而,不仅如此,如果不是Lambda表达式,而是使用匿名方法的前提下,或许这种费事的方法也会得到采用。总之,匿名方法与Lambda表达式的长度上的差别对code会有质的影响。




以下是原文:

ラムダ式は何をもたらすか

 今回の内容は、C# 2.0を習得したプログラマーを読者に想定しているので、匿名メソッドをすでに知っているという前提で書いている。もしそうでなければ、まずは「連載:C# 2.0入門」の方を一読いただいた方がよいだろう。

 さて、本題に入ろう。

 ラムダ式(λ式)とは、ひと言でいってしまうと、(厳密には正しくないが)匿名メソッドをより短く記述するための構文である。と書くと、単に書き方が変わるだけでソースコードの性質が変わるわけではないと思うかもしれないが、これは誤解である。規模が変わればそのものの性質が変化することがある。例えば、実験室のビーカーの中で起きた現象が、そのまま巨大プラントでも同じように起こるとは限らない。同様の現象はソースコード上でも起こり得る。

 実際に、ラムダ式を使うことで体験した出来事をまずは紹介しよう。

 以下、具体的なコードで説明していくが、実際に書いたコードそのままではなく、C# 2.0の知識で読み取れるように全面的に書き直したコードであることをお断りしておく(以下のコードを“鈍くさい”と思う読者もいると思うが、実際のコードはもっと簡潔でかつC# 3.0の機能を活用している)。

 さて、少し前に実際に筆者が書いていたコードの中に、メニューとして選択可能な項目のリストがあった。メニュー項目は以下のように定義されていた。

public delegate bool SimpleMenuAction();

public class
メニュー項目ItemA
{
  public readonly string Name;
// 名前
  public readonly SimpleMenuAction Action; // 実行内容

  public
メニュー項目ItemA(string name, SimpleMenuAction action)
  {
    Name = name;
    Action = action;
  }
}

リスト1 メニュー項目の定義

 これに対して、以下のようなメニュー項目のテーブルがあった。

private static メニュー項目ItemA[] メニューItems1 =
  {
    new メニュー項目ItemA("選択項目1", 実行メソッド),
    new メニュー項目ItemA("選択項目2", 実行メソッド),
    new メニュー項目ItemA("選択項目3", 実行メソッド),
  };

リスト2 メニュー項目のテーブル

 さて、当初はこれで十分と思われていたが、途中で「19時以降にのみ見せるメニューを追加したい」という要求が出てきた。それが1つなら、if文で例外条件を判定して特別処理を挟んでもよいのだが、要求は2つであり、しかも増える可能性もあった。そこで、このテーブル中に条件も含めて記述できるようにしたいと考えた。

 最もシンプルな解決策は、メニュー項目クラスに、「何時以降有効にする」という「時」の整数を保存可能にすることだろう。

 まずメニュー項目ItemAのクラスに、その整数を保持するフィールド「FromHour」を追加する。

public class メニュー項目ItemB
{
  public readonly string Name;
  public readonly SimpleMenuAction Action;
  public readonly int FromHour;

  public メニュー項目ItemB(string name, SimpleMenuAction action, int fromHour)
  {
    Name = name;
    Action = action;
    FromHour = fromHour;
  }
}

リスト3 FromHourフィールドを追加したメニュー項目の定義

 テーブルは以下のように書き直す。

private static メニュー項目ItemB[] メニューItems2 =
  {
    new メニュー項目ItemB("選択項目1", 実行メソッド, 0),
    new メニュー項目ItemB("選択項目2", 実行メソッド, 0),
    new メニュー項目ItemB("選択項目3", 実行メソッド, 0),
    new メニュー項目ItemB("選択項目4", 実行メソッド, 19),
  };

リスト4 リスト3用のメニュー項目のテーブル

 これで、必要な情報をテーブルに埋め込むことができた。

 メニューを構築するメソッドは、選択されたメニューオブジェクトのFromHourを調べることで、現在の「時」が与えられた数値以上であれば表示することができる。

 さて、このコードはYAGNI*の原則からいえばこれで十分であり、これ以上凝った仕掛けを入れる意味はない。そういう意味で、これは良いコードである。

* YAGNIとは「You Aren't Going to Need It.」の略で、もしかしたら必要とされるかもしれない機能は実際には必要とされない可能性が非常に高いことを意味する。つまり、未知の未来に備えるためのコードをあらかじめ書く行為は、たいていの場合無駄になるという教訓である。

 しかし、このコードは仕様変更の要求に対して、あまりにももろい。例えば、条件が19時から1930分になったらもう対応できない。あるいは、終了時刻が指定された場合や、時間帯が2つのケース、あるいは曜日によって時間が変動するなど、いくらでも込み入った要求が想定できる。

 そのような要求を想定し、条件をデリゲートで指定するようにコードを修正することができる。

public delegate bool SimpleMenuAvailability();

public class
メニュー項目ItemC
{
  public readonly string Name;
  public readonly SimpleMenuAction Action;
 
// 現在有効なメニューか?
  public readonly SimpleMenuAvailability IsAvailable;

  public
メニュー項目ItemC(string name, SimpleMenuAction action, SimpleMenuAvailability isAvailable)
  {
    Name = name;
    Action = action;
    IsAvailable = isAvailable;
  }
}

リスト5 条件をデリゲートで指定するメニュー項目の定義

 匿名メソッドを使うとすれば、テーブルは以下のように書き直すことになる。

private static メニュー項目ItemC[] メニューItems3 =
  {
    new メニュー項目ItemC(
      "選択項目1", 実行メソッド, delegate() { return true; }),
    new メニュー項目ItemC(
      "選択項目2", 実行メソッド, delegate() { return true; }),
    new メニュー項目ItemC(
      "選択項目3", 実行メソッド, delegate() { return true; }),
    new メニュー項目ItemC(
      "選択項目4", 実行メソッド,
        delegate() { return DateTime.Now.Hour >= 19; } ),
  };

リスト6 リスト5用のメニュー項目のテーブル

 しかし、このコードは将来必要になるか否かも定かではない変更に備えるにしては、あまりにもコードが肥大化しすぎている。本質的にほとんど意味を持たないdelegateキーワードやreturnキーワードが目立ちすぎ、パッと見て意図も読み取りにくい。これは、明らかにYAGNIの原則によって戒められるべき悪いコードの典型例だろうと思う。

 それ故に、もしC# 2.0を使っていれば、このコードは採用しなかっただろう。いくら、筆者が匿名メソッドを湯水のように使うタイプだとしても、このケースはメリットに対するデメリットが大きすぎる。

 だが、これを匿名メソッドではなく、はるかに少ない文字数で記述できるラムダ式で書いたらどうなるだろうか? 以下のコードでは、「()=>true」や「()=>DateTime.Now.Hour >= 19」がラムダ式に当たる。

private static メニュー項目ItemC[] メニューItems4 =
  {
    new メニュー項目ItemC("選択項目1", 実行メソッド, ()=>true),
    new メニュー項目ItemC("選択項目2", 実行メソッド, ()=>true),
    new メニュー項目ItemC("選択項目3", 実行メソッド, ()=>true),
    new メニュー項目ItemC("選択項目4", 実行メソッド,
                                       ()=>DateTime.Now.Hour >= 19),
  };

リスト7 リスト6にラムダ式を用いたメニュー項目のテーブル

 正直、この程度なら許してよいと思った。YAGNIの原則には反するが、コードの分かりやすさを決定的に損なわない範囲で、未知の修正に対する保険をかけることができている。

 事実としてこの保険は役立った。すぐに、メニューの有効期間が「19時以降」から「19時以降22時未満」へと変更されたのだ。それに伴い、テーブルは以下のように修正された。

private static メニュー項目ItemC[] メニューItems5 =
  {
    new メニュー項目ItemC("選択項目1", 実行メソッド, ()=>true),
    new メニュー項目ItemC("選択項目2", 実行メソッド, ()=>true),
    new メニュー項目ItemC("選択項目3", 実行メソッド, ()=>true),
    new メニュー項目ItemC("選択項目4", 実行メソッド,
            ()=>DateTime.Now.Hour >= 19 && DateTime.Now.Hour < 22),
  };

リスト8 リスト7の選択項目4の有効期間を変更

 この変更は、たった1つのラムダ式を書き換える局所的な変更で収まったので、一瞬で完了した。しかし、もしも最初のリスト4のコードを採用していたら、メニュー項目クラスに終了時刻の情報を追加したり、メニューを構築するメソッドに終了時刻の判定を追加したり、手間のかかる修正が要求されたことだろう。だが、それにもかかわらず、ラムダ式ではなく匿名メソッドを使うという前提であったとしたら、その手間のかかるコードの方を採用していたかもしれない。つまり、匿名メソッドとラムダ式の長さの差がコードの質に影響を与えたのである。