はじめに
C# 3.0では、言語的にいくつかの構文が追加されています。この追加は、かなりの部分において、統合言語クエリ(LINQ)をサポートするために行われた変更と言ってよいでしょう。追加された機能としては、ラムダ式、拡張メソッド、匿名型、暗黙的に型付けされたローカル変数、自動プロパティ、オブジェクト初期化子などがあります(他にも多数の機能があります)。
追加された構文の大部分は、ごく特定のニーズを満たすものであり、これによって既に確立されているコーディング手法やデザイン手法およびガイドラインの重要性が低下するようなことはありません。迷ったときは、新しい構文ではなく従来のガイドラインを優先してください。
MicrosoftのAnson Horton氏は、「
The Evolution Of LINQ And Its Impact On The Design Of C#」という優れた記事の中で、LINQがC# 3.0のデザインにもたらす影響について論じるとともに、新しい言語機能を独特な観点から採り上げています。
ラムダ式
ラムダ式はC# 2.0の匿名メソッドが進化したものと考えられます。ラムダメソッドは、関数型プログラミング(計算を数学関数の評価として扱い、ステートや変動データを避ける枠組み)をC#で採用しようとしたものです。
ラムダ式は
ラムダ計算に基づいています。
使用時のヒント
- 同じコードを繰り返し使用する場合はラムダ式よりもメソッドを優先する。
- C# 2.0では匿名デリゲートが適切と思われる場合はラムダ式を優先する。
拡張メソッド
おそらく大いに賛否が分かれそうな追加構文の1つが拡張メソッドです。拡張メソッドを使用すると、どのクラスにも静的メソッドを「注入」(inject)することができます。拡張メソッドは基本的に、特定の型のインスタンスで動作する静的メソッドを作成するためのいわゆる構文糖(syntactic sugar)です。C# 3.0以前は、一般的なユーティリティメソッドを次のように書いていました。
public static bool IsStringPlural(String text,
CultureInfo cultureInfo)
{/* ... */}
このメソッドは次のような方法で呼び出されます。
String text = "languages";
Boolean isPlural =
StringExtensions.IsStringPlural(
text, new CultureInfo("en"));
このIsStringPluralメソッドはStringオブジェクトで動作します(与えられたコンテキストで対象の語や句が複数形であるかどうかを判断するメソッドであると考えてください)。拡張メソッドを使用すると、同様のメソッドをクラスに関連付け、そのクラスのメンバと同じように呼び出すことができます。たとえば、StringクラスにIsPluralという拡張メソッドを作成するときには、IsStringPluralメソッド宣言の元の構文を大きく変更する必要はありません。パラメータリストにthisキーワードを追加するだけです。
public static bool IsPlural(this String text,
CultureInfo cultureInfo)
{/* ... */}
この拡張メソッドは、次のようにして呼び出します。
String text = "languages";
Boolean isPlural = text.IsPlural(
new CultureInfo("en"));
このメソッド呼び出し構文では、読みやすさは向上しているかもしれませんが、メソッド定義元の追跡は困難になっています。IsPlural拡張メソッドを呼び出すコード行を見ても、IsPluralメソッドが実際にはStringクラスのメンバでないことはわかりませんし、このメソッドが実際にどのクラス内で宣言されているのかもわかりません。
拡張メソッドは通常の静的メソッドとまとめて扱われ、次のように使用できます。
String text = "languages";
Boolean isPlural = StringExtensions.IsPlural(text,
new CultureInfo("en"));
拡張メソッドは、ラムダ式と組み合わせて、簡潔で非常に可読性の高いクエリ式を提供する目的に使用されることが期待されています。
拡張メソッドの主な難点は、名前解決の問題です。基本的にすべての拡張メソッドはグローバルで、名前も引数の数も同じメソッドは今のところ区別できません。そのため、拡張メソッドはもともとネームスペースの適切な運用を妨げる性質を持っており、現時点では、スコープを明確に区切る方法はありません。つまり、ファイルにusingステートメントを追加するだけで、コンパイルエラーが発生する可能性があります。
使用時のヒント
- 拡張メソッドは慎重に使用する。
- 拡張メソッドは固有の静的クラス内に配置する。
- 特定のクラスを拡張するすべての拡張メソッドを1つの静的クラスにまとめ、そのクラスに「<ClassName>Extensions」という名前を付ける。名前の衝突が起き、静的メソッドの呼び出し構文を使わざるを得ない場合は、最終的に可読性が低下しないようにする。
- 名前の衝突が起きる可能性を低くするため、拡張メソッドのクラスを固有のネームスペース内に保存する(名前の衝突が起きると、再び静的メソッドの呼び出しを使用せざるを得なくなる)。
編集部注
匿名型
匿名型を使用すると、クラス宣言を行わずにクラスをインスタンス化できます。言うまでもありませんが、このクラスには名前がないので、varキーワードの使用が必要になります。次に例を示します。
var person = new { Name = "Peter", Age=4};
この構文から推測できるように、匿名型にメソッドを定義する方法はありません。そのため実際のところ、実環境のシナリオでの匿名型の有用性は限られています。型を実際に宣言することはないので、この型には属性を追加できません。したがって、シリアライズ可能にすることもできません。
また、匿名型は不変(immutable)です。これはつまり、匿名型をインスタンス化するときにはフィールドではなく読み取り専用のプロパティを宣言するということです。このようなプロパティはgetのみを持ち、setはありません。
要するに、匿名型は実際にはきわめて一時的なデータのみに有用です。
使用時のヒント
暗黙的に型付けされたローカル変数
賛否が分かれそうなもう1つの追加は、暗黙的に型付けされたローカル変数です。これをvarと呼ぶことにします。この呼び方についてはさておき(varとしておけば、他の型と混同する可能性は非常に低くなるでしょう)、varが導入された主な理由は、LINQクエリで返される型には匿名型が含まれることがあり、必ずしも人間が読み取りやすい名前になるとは限らないからです。
LINQステートメントから返される型は、きわめて複雑になることもあります。匿名型を扱わない場合は、結果の型を推定して、宣言において手動で型付けできる可能性もあります。しかし、たいていの場合、データベースクエリは非常に可変性が高いので、変更のたびに型を再推定する必要があります。
LINQステートメントでの使用を別にしても、varを使用するとソースコードの読みやすさが低下し、検索が難しくなる傾向があります。たとえばMyClassというクラスを宣言し、このクラスをインスタンス化するときに毎回varを使用していると、var変数への代入式の右辺でクラス名を使用していない限り、コード内で「MyClass」を検索して使用箇所を特定することができなくなります。
C#では、大半の式で予測どおりの型が生成されますが、そうでないこともあります。二項演算と整数型がその好例です。整数の二項演算では、一方の項が符号あり(signed)でもう一方の項が符号なし(unsigned)の演算はサポートされません。次のコードではエラーが発生します。
uint unsignedNumber = 42;
int signedNumber = 10;
int result = unsignedNumber * signedNumber;
上記のコードを実行すると、「Cannot implicitly convert type 'long' to 'int'」(long型からint型への暗黙的変換はできません)という少々わかりにくいエラーメッセージが表示されます。このエラーメッセージでlong型と言及されているのは、コンパイラによってuint型からlong型への変換が暗黙的に行われるためです。つまり、コンパイラは以下の処理を実行しています。
int result = (long)unsignedNumber * signedNumber
これは、符号あり整数と符号なし整数で二項演算を行う1つの方法です。しかし、結果をint型にする必要があるのに、コンパイラはlong型をint型に暗黙的に変換できません。そのため警告が生成されます。
結果の型を指定するのにint型の代わりにvarを使用すると、エラーは解決します。
var result = unsignedNumber * signedNumber;
これで正常に実行されますが、結果はuint型でもint型でもなく、long型です。また、これはulong型でもありません。
もう1つ、メソッドの戻り値の型に関するちょっとした例を紹介しましょう。あるメソッドの結果にvarを使用すると、その変数を使用するコードは、メソッドの実装に関連付けられることになります。後でそのメソッドの戻り値の型を変更すると、対応する変数を使うコードの働きも変わってきます。その例を次に示します。
private int GetWeight ( )
{
return 17;
}
private decimal GetQuantity ( )
{
return 11;
}
String Method()
{
var quantity = GetQuantity ();
decimal result = GetWeight() *
quantity / 4;
Trace.WriteLine("result:" + result);
return "result: " + result;
}
上記のコードを実行すると、結果は以下のようになります。
ここでGetQuantityメソッドに手を加え、値をint型で返すようにすると、次のようになります。
static int GetQuantity ( )
{
return 11;
}
結果はこうなります。
変更した部分はごくわずかです。GetQuantityメソッドの本体には手を加えていませんが、出力は戻り値の型に応じて変わります。
厳密な型付けを利用できるというメリットはありますが、暗黙的に型付けされたローカル変数を使用するとコンパイラの選択が先送りされ、コンパイル時にバグを検出できる確率が低下します。
使用時のヒント
- 組み込み型でvarを使用することは避ける。
- 結果が組み込み型ではないLINQステートメントでのみ使用する。
- 明示的な型のローカル変数を優先する。
オブジェクト初期化子
オブジェクト初期化子は、C#を他の多くの高級言語と肩を並べる存在にするために当初から用意すべきだった機能です。CやC++とは異なり、C#はこれによって総合的な初期化機能を持つことになりました。この機能はおそらく、LINQをサポートするために追加された機能の中で最も広く役立つでしょう。Microsoftがオブジェクト初期化子を追加したのは、LINQステートメントが単一ステートメントだからです。これまでは、1つのステートメント内で実行できるものでなければ、オブジェクトをクエリ結果で初期化することはできませんでした。
使用時のヒント
- 1ステートメント1プロパティ方式でオブジェクトまたはエレメントを初期化するよりも、オブジェクト初期化子を優先する。
自動プロパティ
自動プロパティは、バッキングストア用に単一フィールドをラップするプロパティを書くときの面倒な作業を回避するための構文糖です。C# 2.0ではプロパティを次のようにして実装しました。
private int age;
public int Age
{
get
{
return age;
}
set
{
age = value;
}
}
それほど長いコードではありませんが、多くのプロパティを実装する必要がある場合は面倒になるでしょう。C# 3.0では、次のような方法で、バッキングストア用のフィールドを宣言せずにプロパティを作成できます。
public int Age
{
get ;
set ;
}
バッキングストアで使われるフィールドはコンパイラによって自動生成されますが、これを直接操作することはできません。このプロパティへのすべてのアクセスは、プロパティのgetterメソッドとsetterメソッドを介して行う必要があります。
使用時のヒント
- パブリックフィールドよりも自動プロパティを優先する。
- パフォーマンスが重視される場合は自動プロパティを避ける。
Microsoftは特定の理由から多くの新しいC# 3.0の構文を作成しましたが、開発者は適用可能であればいつでも新機能を使用できます。このガイドラインでは、新しいC# 3.0の構文を使用するときに陥りがちな落とし穴とそれを回避するために役立つアドバイスを示しましたが、絶対に守らなければならない規則ではありません。状況によっては、明確な理由を持ってこのガイドラインに違反することもあるでしょう。しかし一般的には、新しいC# 3.0の構文の理解を深め、より良いコードを書くためにこのガイドラインが役に立つはずです。