成问题的是,虽然为了源代码的简洁性,很想用Lambda表达式,但是要写的代码却不能全部都用Lambda表达式来写。
那么, Lambda表达式究竟能做到什么程度呢?
习惯了C/C++编程风格的程序员,一定以为因C#语法与之很相似,所以用C#编写相对复杂的程序应该也没有问题。可是很遗憾,情况不是这样。那是因为C/C++具有能写出复杂功能的表达式的逗号表达式,而C#却没有。
例如,C/C++中,下面的代码是可行的。c = (a = 1 , b = 2 , a + b); // a=1、b=2、c=a + b
printf( " %d\n " ,c);
正因为仅仅一个表达式就能做相当多的处理,所有逗号表达式才有着重要的意义。
但是,C#却没有这样的用法。
但是C#有三元运算符?:和空接合运算符??。通过使用这些运算符,相当数量的代码都可以用Lambda表达式来写了。
例如,“根据参数指定的文件名向文件中写入字符串,参数为null的情况下,用‘default.txt’作为默认文件名”这样的Lambda表达式,像下面那样,用空接合运算符??就可以用Lambda表达式来写。class Program
{
static void Main( string [] args)
{
Action < string > method =
(filename) => System.IO File.WriteAllText(
filename ?? " default.txt " , " Hello! " );
method( null ); // 生成default.txt
method( " hello.txt " ); // 生成hello.txt
}
}
List11 使用了空接合运算符??的Lambda表达式
或者“参数的 flag 如果是 false 的话,文件名就是‘ normal.log ’, true 的话就是‘ system.log ’ ” 的情况下, 使用 三元运算符( ?: ) 按照如下的方式以表达式形式的Lambda来写。class Program
{
static void Main( string [] args)
{
Action < bool > method =
(system) => System.IO.File.AppendAllText(
system ? " system.log " : " normal.log " , " log message\r\n " );
method( false ); // 生成normal.log
method( true ); // 生成system.log
}
}
List12 使用三元运算符(?:)的Lambda表达式
但是,前一章的List10的例子里,其中的if语句中就不能改用三元运算符来替换。如果试图替换的话就是以下情况:
class Program
{
static void Main( string [] args)
{
Action < string > method = (filename) =>
filename == null
? Console.WriteLine( " Hello! " )
: System.IO.File.WriteAllText(filename, " Hello! " );
method( null );
method( " hello.txt " );
}
}
List13 List10中用三元运算符改写后(产生编译错误)
这个代码,会产生以下的编译错误:
error CS0173: 无法确定条件表达式的类型,因为“ void ”和“ void ”之间没有隐式转换。
这并不是说Lambda表达式不能调用具有void返回值的方法。下面的代码就没有问题。
(filename) => System.IO.File.WriteAllText(filename, " Hello! " );
这里产生error的原因是,三元运算符的第二个、第三个运算数不能写成void类型的表达式(因为这样写,void没法隐式转换成第二个、第三个运算数的类型,所以整个表达式的类型就无法判断了。
因为存在这样的问题,有void类型的返回值的表达式情况下,三元运算符使用就很困难。其实,void返回值的表达式很难理解,不能写反而是个好事。
除了void型以外,其它类型的表达式,使用三元运算符就没有问题。下面的代码在编译和执行时就没有问题。
2
3 class Program
4 {
5 static void Main(string[] args)
6 {
7 Func<int, bool> method = (year) =>
8 year < 1994 ? year % 4 == 0 : year % 4 == 2;
9
10 Console.WriteLine("冬奥会年份");
11 for (int i = 1988; i < 1999; i++)
12 {
13 Console.WriteLine("{0}年={1}", i, method(i));
14 }
15 // 输出:
16 // 冬奥会年份
17 // 1988年=True
18 // 1989年=False
19 // 1990年=False
20 // 1991年=False
21 // 1992年=True
22 // 1993年=False
23 // 1994年=True
24 // 1995年=False
25 // 1996年=False
26 // 1997年=False
27 // 1998年=True
28 }
29}
30
List14 使用三元运算符的Lambda表达式
代入变量method的Lambda表达式,判断参数的年份是不是冬奥会举行的年份,1994年以后,因为是两年举行一届,所以判断方式也要改变。这种情况下,当然是三元运算符派上用场的时候。总之,这种程度的问题,用表达式形式的Lambda就比较容易写。
不用类型声明的情况和必须类型声明的情况
大部分情况下,Lambda表达式的参数类型都可以省略。然而,Lambda表达式的使用也存在限制,不能推定出类型的情况下就不能用Lambda表达式。
例如,Lambda表达式就不能代入到使用var关键字隐式类型声明的局部变量:
var Lambda = (int x) =>x *2;
上面的代码会产生“Lambda表达式局部变量隐式的类型声明不会起作用”的错误。避免错误的方法是不要用var,而是明确的进行类型声明。
然而,
下面的例子
2
3 delegate int delegate1( int x );
4 delegate int delegate2( string s );
5
6 class Program
7 {
8 private static void sample(delegate1 method)
9 {
10 Console.WriteLine("void Sample(delegate1 method)");
11 }
12
13 private static void sample(delegate2 method)
14 {
15 Console.WriteLine("void Sample(delegate2 method)");
16 }
17
18 static void Main(string[] args)
19 {
20 sample((int x) => 0);
21 // sample((x) => 0); // 如果没有参数类型声明会产生错误
22 }
23}
24
List 15 必须指定参数类型的情况
这个例子中,“sample((x)=> 0”会产生编译错误。满足条件的sample方法有两个,所有就不能确定究竟应该使用哪个。然而,在参数前加上类型声明“sample((int x) => 0”,就能够编译执行。因为参数类型的指定,在2个sample方法中,有一个与之类型相吻合,所以以此为依据就能够选择了。
什么都不做的Lambda表达式
这个话题说到此,还有盲点。这里先说明一下什么都不做的Lambda表达式的写法。
Lambda表达式没有返回值的情况(void的情况),想使其内容为空的情况下(调用后什么也不执行的Lambda表达式选择使用的情况),可以使用内容为空的Lambda语句。
例如,下面这个的Lambda表达式:这样用Lambda表达式重构,解决了“引入了null值对象”的问题。一句话,不应该用null表示什么也不做的表达式,而是采用调空Lambda表达式的手法。
简单的说,分别用代码来展示能够使用和不能够使用这个技术的场合。
首先,说说不能够使用该技术的场合。下面的代码,因为什么也不需要处理,所以用null值表示的例子。Sample方法的参数action,仅在值不为null的情况下被调用。
class Program
{
private static void Sample(Action < string > action)
{
if (action != null ) action( " Hello! " );
}
static void Main( string [] args)
{
Action < string > action = null ;
Sample(action);
action = (x) => Console.WriteLine(x);
Sample(action); // 输出:Hello!
}
}
List 16 执行时没什么可处理的情况下用null表示的例子
相反,下面的代码,在没什么要执行的情况下,要使用空Lambda表达式表示的情况。没必要判定Sample方法的参数action是否为null。如果需要处理的内容不存在的情况下,仅仅用空的Lambda表达式来执行,什么也不做就返回。
class Program
{
private static void Sample(Action < string > action)
{
action( " Hello! " );
}
static void Main( string [] args)
{
Action < string > action = (x) => { };
Sample(action);
action = (x) => Console.WriteLine(x);
Sample(action); // 输出:Hello!
}
}
List 17 没什么可处理的情况下用空表达式的例子
这样的“什么也不做的Lambda表达式(或是以前的匿名方法)”,是笔者经常使用的技术。
例如,现在正在写的程序,具有用户的操作用报表的形式回放的功能,通过该功能进行自动测试。这个时候,回放中与输出有关的处理会全部禁用,以提高其运行效率。这些操作的实现,并不需要具有输出功能的方法对条件进行一个一个的判断,只要用“空Lambda表达式(匿名方法)”就行了。因此,源代码仍然能够维持其简洁,成功实现了随时都能够执行的自动测试效率的目的。
以下为日文原文:
式形式のラムダの可能性
ここで問題になるのは、ソース・コードを簡潔にするためにはぜひとも式形式のラムダを使いたいが、書きたいコードがすべて式形式のラムダで書けるわけではない、という点である。
では、どこまでなら式形式のラムダで記述できるのだろうか。
C/C++に慣れたプログラマーであれば、構文の似たC#でも式として相当複雑なコードを書けるかもしれないと思うかもしれないが、残念ながらそれはできない。なぜかといえば、C/C++で複雑な機能を持った式を書くための切り札となるカンマ演算子がC#には存在しないためである。
例えば、C/C++であれば、以下のようなコードは有効である。
|
それ故に、たった1つの式にかなり込み入った処理を記述することができ、それはマクロなどを定義する際に重要な意味を持っていた。
しかし、このような使い方はC#ではできないのである。
とはいえ、C#でも三項演算子やnull合体演算子があるので、これらを使うと多少込み入ったコードを式形式のラムダとして記述できる。
例えば、「引数で指定したファイル名のファイルに文字列を書き込むが、引数がnullの場合は『default.txt』をファイル名とする」というラムダ式は、以下のようにnull合体演算子(??)を用いて式形式のラムダとして記述できる。
|
|
リスト11 null合体演算子(??演算子)を用いたラムダ式 |
あるいは「引数のフラグがfalseならファイル『normal.log』に追加、trueならファイル『system.log』に追加」であれば、三項演算子(?:)を使って以下のように式形式のラムダとして記述できる。
|
|
リスト12 三項演算子(?:演算子)を用いたラムダ式 |
しかし、上記のリスト10に記述した例は、if文を三項演算子に置換できない。もし書き換えを試みると以下のような内容になる。
|
|
リスト13 リスト10を三項演算子で書き換えた例(コンパイル・エラーとなる) |
このコードは、以下のようなコンパイル・エラーを発生させる。
|
これは、ラムダ式にvoidを返すメソッド呼び出しを書けないという意味ではない。以下のコードは問題なく記述できる。
|
そうではなく、ここでエラーの原因になっているのは、三項演算子の2番目、3番目にvoid型の式を記述できない(記述しようとしても2番目と3番目の型、つまりvoidの間の暗黙的な変換が存在しないため全体の型を確定できない)という理由による。
このような問題もあるため、void型を返す式に限っては三項演算子を利用しにくいことになる。もっとも、値を返さない式は分かりにくいので、書けない方がよいという考え方もあり得るだろう。
ちなみに、当然のことながら、void型以外の型の式であれば、三項演算子は問題なく有効に機能する。以下のコードは問題なくコンパイルでき、実行できる。
|
|
リスト14 三項演算子を用いたラムダ式 |
変数methodに代入したラムダ式は、引数の年が冬期オリンピックの開催年であるかを判定するが、1994年以降は開催年が2年ずれているので判定式が変わってくる。このようなケースでは、当然のことながら三項演算子は有効である。つまり、この程度であれば、式形式のラムダとして容易に記述できる。
型指定を省略できる場合、できない場合
ほとんどの場合、ラムダ式の引数の型は省略できる。逆にいえば、型の推定ができない状況ではラムダ式が使用できない制限が課せられているともいえる。
例えば、varキーワードを用いて宣言する「暗黙的に型指定されるローカル変数」にラムダ式を代入することはできない。
var lambda = (int x) => x * 2;
このコードは「ラムダ式を暗黙的に型指定されたローカル変数に割り当てることはできません」というエラーを発生させてしまう。回避するにはvarではなく、正しく型の名前を書く必要がある。
しかし、まれにラムダ式が使用できるにもかかわらず、型指定を行わねばならない状況が発生する。
以下はその一例である。
|
|
リスト15 引数の型指定が必須のケース |
このリストで、「sample((x) => 0)」はコンパイル・エラーになる。条件を満たすsampleメソッドが2つあり、どちらを使うべきか判定するだけの情報が与えられていないからである。しかし、引数に型を添えて「sample((int x) => 0)」と記述すればコンパイルでき、実行もできる。引数に型が指定されたことで、2つのsampleメソッドのうち、型が一致する方のデリゲートを明確に選択できるからである。
何もしないラムダ式
盲点になることもある話題なので、「何もしないラムダ式」の書き方も説明しておく。
ラムダ式に戻り値がない場合(voidの場合)でかつ、内容を空としたい場合(呼び出しても何も実行しないラムダ式を選択的に使いたい場合)、ステートメント型のラムダとして空の内容を記述することができる。
例えば、以下のようなラムダ式である。
|
このようなラムダ式は、リファクタリングでいう「ヌル(ナル)オブジェクトの導入」に当たる機能性を持つ。つまり、処理すべき式が存在しないことをnullで示すのではなく、何も処理しないラムダ式を呼ばせるという手法である。
簡単に、このテクニックを使わない場合と使う場合のコード例をそれぞれ示す。
まず、このテクニックを使わない場合である。以下のリストは、実行すべき処理がない場合にnull値でそれを示すケースである。Sampleメソッドの引数actionは、値がnullではない場合に限って呼び出される。
|
|
リスト16 実行すべき処理がない場合をnullで示した例 |
これに対して、以下は実行すべき処理がない場合、空のラムダ式で示したケースである。Sampleメソッドは、引数actionがnullか否かを判定する必要がない。もし、処理すべき内容が存在しない場合は、単純に空文のみのラムダ式が実行されて何もせず戻ってくる。
|
|
リスト17 実行すべき処理がない場合を空文で示した例 |
このような「何もしないラムダ式(あるいは従来は匿名メソッド)」は、筆者が割とよく使うテクニックである。
例えば、いま書いているプログラムでは、ユーザーの操作をジャーナリングしてプレイバックする機能を持っていて、それによって自動テストを行っている。その際、プレイバック中は出力に関する処理をすべて抑止して高速に実行させるようにしているが、それらは、出力を行うメソッド呼び出しにいちいち条件判断を付けるのではなく、呼び出し先を「何もしないラムダ式(匿名メソッド)」に差し替えることで実現している。これにより、ソース・コードの簡潔さを維持したまま、気軽にいつでも実行できる程度に自動テストを高速化することに成功している。