次の方法で共有


動的な .NET

C# 4 の dynamic キーワードについて

Alexandra Rusina

dynamic キーワードと動的言語ランタイム (DLR) は、C# 4 と Microsoft .NET Framework 4 での大きな新機能です。この 2 つの機能が発表されると、多くの興味がかき立てられると同時に、多くの疑問が湧き上がりました。こうした疑問に多くの回答が寄せられましたが、現時点では、ドキュメントおよびさまざまな技術ブログや記事に散在しています。このため、フォーラムやカンファレンスで、同じ疑問が繰り返し投げかけられ続けています。

今月のコラムでは、C# 4 の新しい動的機能について概説し、リフレクションや暗黙に型指定される変数などの言語機能やフレームワーク機能と、C# 4 の新しい動的機能がどのように連携するかを詳しく説明します。既に多くの情報が提供されているため、ここでは、ところどころで既存の例を再利用します (元の情報源へのリンクも記載しています)。また、今後の参考資料へのリンクも多数掲載しています。

動的とは

プログラミング言語は、静的に型指定される言語と動的に型指定される言語に分類されることがあります。C# や Java は静的に型指定される言語と見なされることが多く、Python、Ruby、および JavaScript は動的に型指定される言語の例です。

一般に、動的言語ではコンパイル時に型チェックが行われず、実行時にのみオブジェクトの型が特定されます。動的言語には長所と短所があります。多くの場合、格段に短時間で容易にコードを作成できますが、同時に、コンパイル エラーが発生しないため、単体テストやその他の手法を使用して、アプリケーションの動作が正しくなるようにすることが必要です。

本来、C# は純粋な静的言語として開発されましたが、C# 4 で、動的言語やフレームワークとの相互運用性を向上するために動的要素が追加されました。C# チームはいくつかの設計案を検討しましたが、最終的に dynamic という新しいキーワードを追加して動的機能をサポートすることにしました。

dynamic キーワードは、C# 型システムで静的型宣言として機能します。この方法であれば、C# でも動的機能を使用しながら、これまでどおり静的に型指定される言語とすることができます。この設計に決まった理由とその過程については、PDC09 (microsoftpdc.com/2009/FT31、英語) における Mads Torgersen によるプレゼンテーション「Dynamic Binding in C# 4」で説明されています。中でも、動的オブジェクトは C# 言語の標準要素とすることが決められたため、動的機能の有効と無効を切り替えるオプションは存在せず、Visual Basic の Option Strict On/Off のような機能は C# には追加されませんでした。

dynamic キーワードを使用すると、コンパイラによるコンパイル時のチェックが無効になります。このキーワードの使用方法については多くの例が Web や MSDN ドキュメント (msdn.microsoft.com/library/dd264736) で紹介されています。一般的な例を次に示します。

dynamic d = "test";
Console.WriteLine(d.GetType());
// Prints "System.String".

d = 100;
Console.WriteLine(d.GetType());
// Prints "System.Int32".

ご覧のように、dynamic として宣言した 1 つの変数に、型の異なる複数のオブジェクトを代入することができます。コードは正常にコンパイルされ、オブジェクトの型は実行時に特定されます。ただし、次のコードもコンパイルされますが、実行時に例外がスローされます。

dynamic d = "test";

// The following line throws an exception at run time.
d++;

理由は同じです。コンパイラはオブジェクトの実行時の型を認識できないため、この場合はインクリメント操作がサポートされないかどうかを判断できません。

コンパイル時に型チェックが無効になるということは、IntelliSense も無効になります。C# コンパイラがオブジェクトの型を特定しないため、オブジェクトのプロパティとメソッドを設定できません。この問題は、Visual Studio の IronPython ツールと同様に型の推定を追加することで解決できる可能性がありますが、現時点では C# にはこの機能がありません。

ただし、動的機能を使用することでメリットがあると思われる多くのシナリオでは、コードでリテラル文字列が使用されていたため、いずれにしても IntelliSense は利用できませんでした。この問題については、後で詳しく説明します。

dynamic、object、および var の違い

では、dynamic、object、および var の本当の違いは何でしょうか。また、どのようなときにこれらを使用すべきでしょうか。各キーワードの簡単な定義と使用例を以下に示します。

object キーワードは、C# クラス階層のルート型である System.Object を表します。このキーワードは、通常、コンパイル時にオブジェクトの型を特定する手段がないときに使用します。このような状況は、多くの場合、さまざまな相互運用性のシナリオで発生します。

次のように明示的なキャストを使用すると、object で宣言した変数を特定の型に変換できます。

object objExample = 10;
Console.WriteLine(objExample.GetType());

このコードでは明らかに System.Int32 が出力されます。ただし、静的型が System.Object のため、明示的なキャストはここでは必要ありません。

objExample = (int)objExample + 10;

どの値も System.Object から継承されるため、型の違う値を代入することができます。

objExample = "test";

var キーワードは C# 3.0 から導入されたキーワードで、暗黙に型指定されるローカル変数と匿名型に使用します。このキーワードは、多くの場合、LINQ と併せて使用します。var キーワードを使用して変数を宣言すると、その変数の型はコンパイル時の初期化文字列から推論されます。この変数の型を実行時に変更することはできません。コンパイラが型を推論できなければ、コンパイル エラーが発生します。

var varExample = 10;
Console.WriteLine(varExample.GetType());

上記のコードでは System.Int32 が出力されます。これは静的型と同じです。

次の例では、varExample の静的型が System.Int32 のため、キャストは不要です。

varExample = varExample + 10;

次のコード行は、varExample には整数しか代入できないため、コンパイルされません。

varExample = "test";

dynamic キーワードは C# 4 から導入されたキーワードです。このキーワードを使用すると、従来 object キーワードを利用していた特定のシナリオをより容易に記述およびメンテナンスできます。実際、dynamic 型では内部で System.Object 型を使用しますが、実行時にしか型を特定しないため、object と異なり、コンパイル時に明示的なキャスト操作が必要ありません。

dynamic dynamicExample = 10;
Console.WriteLine(dynamicExample.GetType());

このコードでは System.Int32 が出力されます。

次のコード行では、型が実行時にしか特定されないため、キャストが不要です。

dynamicExample = dynamicExample + 10;

dynamicExample には型の違う値を代入できます。

dynamicExample = "test";

object キーワードと dynamic キーワードの違いの詳細については、C# の FAQ ブログ (bit.ly/c95hpl、英語) に詳細な記事があります。

ときおり混乱の元になるのは、これらのキーワードのすべてを一緒に使用できることです。この 3 つのキーワードは相互に排他的ではありません。例として、次のコードを見てみましょう。

dynamic dynamicObject = new Object();
var anotherObject = dynamicObject;

anotherObject の型は何でしょう。答えは dynamic です。dynamic は、実際に C# 型システムでは静的型であることを思い出してください。したがって、コンパイラは dynamic を anotherObject の型として推論します。var キーワードは、コンパイラに変数の初期化式から型を推論する指示に過ぎないことを理解することが重要です。var は型ではありません。

動的言語ランタイム

C# 言語で "dynamic (動的)" という語が使われる場合、通常は C# 4 または DLR の dynamic キーワードのいずれかの概念を指します。この 2 つの概念は関連していますが、違いを理解することも重要です。

DLR には 2 つの主な目的があります。1 つは、動的言語と .NET Framework 間の相互運用を実現することです。もう 1 つは、C# と Visual Basic で動的な動作を使用できるようにすることです。

DLR は、.NET Framework に初めて実装された動的言語である IronPython (ironpython.net、英語) の開発中に得た教訓を基に開発されました。IronPython の開発中に、開発チームは IronPython の実装を複数の言語に再利用できることを発見しました。そこで、.NET 動的言語の共通の基盤となるプラットフォームを開発したのです。IronPython と同様に、DLR はオープン ソース プロジェクトになりました。DLR のソース コードは現在 dlr.codeplex.com (英語) で公開されています。

その後 DLR も .NET Framework 4 に組み込まれ、C# と Visual Basic で動的機能がサポートされるようになりました。C# 4 の dynamic キーワードしか必要でない場合は、.NET Framework を使用するだけで、ほとんどの場合は、DLR によりすべての相互作用が自動的に処理されます。しかし、新しい動的言語を .NET に実装または移植する場合は、オープン ソース プロジェクトの、言語実装者向けの機能やサービスがより充実している追加のヘルパー クラスを利用すると便利です。

静的に型指定される言語での dynamic の使用

静的型宣言の代わりに、とにかく可能な限り dynamic を使用するということが期待されているのではありません。コンパイル時のチェックは強力な機能であり、活用できるに越したことはありません。また、繰り返しになりますが、C# の動的オブジェクトは IntelliSense をサポートしません。このため、全体的な生産性にはある種の影響がある可能性があります。

しかし同時に、dynamic キーワードと DLR が導入される前は、C# で実装が難しいシナリオがありました。ほとんどの場合、このようなシナリオでは System.Object 型と明示的なキャストが使用されていて、いずれにしてもコンパイル時のチェックと IntelliSense から多くのメリットは得られませんでした。次にいくつか例を示します。

最も悪名高いシナリオは、object キーワードを使用して他の言語やフレームワークとの相互運用性を図ることです。通常、リフレクションを使用してオブジェクトの型を取得し、そのオブジェクトのプロパティとメソッドにアクセスします。構文が判読しにくい場合があり、このため、コードのメンテナンスが困難になります。この場合に dynamic を使用すると、リフレクションよりもはるかに簡単で便利になる可能性があります。

Anders Hejlsberg が、PDC08 (channel9.msdn.com/pdc2008/TL16、英語) において次のような素晴らしい例を紹介しています。

object calc = GetCalculator();
Type calcType = calc.GetType();
object res = calcType.InvokeMember(
  "Add", BindingFlags.InvokeMethod, 
  null, new object[] { 10, 20 });
int sum = Convert.ToInt32(res);

この関数は、計算オブジェクトを返しますが、システムはこの計算オブジェクトの正確な型をコンパイル時に特定できませんコードが利用できる唯一の情報は、このオブジェクトには Add メソッドがあるということだけです。このメソッド名をリテラル文字列で指定していないため、このメソッドには IntelliSense 機能が適用されないことに注意してください。

dynamic キーワードを使用すると、このコードは次のように簡潔になります。

dynamic calc = GetCalculator();
int sum = calc.Add(10, 20);

この場合も、Add メソッドを含むと思われる不明な型のオブジェクトがあるという推論は同じです。また、前の例と同様に、このメソッドに IntelliSense 機能は適用されません。しかし、構文ははるかに読みやすくて使いやすく、通常の .NET メソッドの呼び出しのような構文です。

動的メソッド バッグ

dynamic が便利なもう 1 つの例は、動的メソッド バッグの作成です。動的メソッド バッグは、実行時にプロパティやメソッドを追加および削除できるオブジェクトです。

.NET Framework 4 には System.Dynamic という新しい名前空間があります。この名前空間は、実は DLR の一部です。System.Dynamic.ExpandoObject クラスおよび System.Expando.DynamicObject クラスと、新しい dynamic キーワードを組み合わせると、明瞭で読みやすい動的な構造と階層を作成できます。

たとえば、次のコードでは、ExpandoObject クラスを使用してプロパティとメソッドを追加できます。

dynamic expando = new ExpandoObject();
expando.SampleProperty = 
  "This property was added at run time";
expando.SampleMethod = (Action)(
  () => Console.WriteLine(expando.SampleProperty));
expando.SampleMethod();

詳細なシナリオについては、ExpandoObject クラスおよび DynamicObject クラスについての MSDN ドキュメントを参照してください。また、Bill Wagner による「動的メソッド バッグ」(msdn.microsoft.com/library/ee658247、英語)、C# の FAQ ブログの「C# 4.0 の dynamic: ExpandoObject の概要 (Dynamic in C# 4.0: Introducing the ExpandoObject)」、bit.ly/amRYRw、英語) を参照してください。

クラス ラッパー

独自のライブラリ用にもっと便利な構文を用意したり、既存のライブラリ用にラッパーを作成したりすることができます。これは前の 2 つのシナリオよりも高度なシナリオで、DLR の特質をより深く理解する必要があります。

簡単なケースでは、DynamicObject クラスを使用できます。このクラスでは、メソッドやプロパティの静的宣言と動的ディスパッチを組み合わせて使用できます。したがって、より簡潔な構文を使用したいオブジェクトをクラスのプロパティに格納する一方で、このオブジェクトを利用して動的ディスパッチによりすべての操作を処理できます。

例として、図 1 の DynamicString クラスを見てください。このクラスでは、文字列をラップし、すべてのメソッドの名前を表示してから、リフレクションを利用してそれらのメソッドを実際に呼び出しています。

図 1 DynamicString

public class DynamicString : DynamicObject {
  string str;

  public DynamicString(string str) {
    this.str = str;
  }

  public override bool TryInvokeMember(
    InvokeMemberBinder binder, object[] args, 
    out object result) {

    Console.WriteLine("Calling method: {0}", binder.Name);

    try {
      result = typeof(string).InvokeMember(
        binder.Name,
        BindingFlags.InvokeMethod |
        BindingFlags.Public |
        BindingFlags.Instance,
        null, str, args);
      return true;
    }
    catch {
      result = null;
      return false;
    }
  }
}

このクラスのインスタンスを作成するには、dynamic キーワードを使用します。

dynamic dStr = new DynamicString("Test");
Console.WriteLine(dStr.ToUpper()); 
Console.ReadLine();

もちろん、この例は不自然で、効率的とは言えません。しかし、既にリフレクションを大量に利用している API がある場合は、この例のようにリフレクションによってすべての呼び出しをラップすることで、API のエンド ユーザーから呼び出しを隠すことができます。

その他の例については、MSDN ドキュメント (msdn.microsoft.com/library/system.dynamic.dynamicobject) および C# の FAQ ブログの「C# の dynamic: DynamicObject を使用してラッパーを作成する (Dynamic in C# 4.0: Creating Wrappers with DynamicObject)」(bit.ly/dgS3od、英語) を参照してください。

前述のとおり、DynamicObject クラスは CLR によって提供されます。DynamicObject または ExpandoObject さえあれば、動的オブジェクトを作成できます。ただし、動的オブジェクトの中には、メンバーのアクセスやメソッドの呼び出しに、複雑なバインド ロジックを使用するものがあります。このようなオブジェクトでは、IDynamicMetaObjectProvider インターフェイスを実装して、オブジェクト独自の動的ディスパッチを提供する必要があります。これは高度なシナリオですが、興味のある方は、Bill Wagner による「動的インターフェイスを実装する」(msdn.microsoft.com/vcsharp/ff800651、英語) および Alex Turner と Bill Chiles による「ライブラリの作成者として DLR を使い始める」(dlr.codeplex.com、英語) を参照してください。

スクリプト可能なアプリケーション

スクリプトは、アプリケーションに拡張性を持たせる強力な手段の 1 つです。Visual Basic for Applications (VBA) のおかげでさまざまなマクロ、アドオン、およびプラグインが存在する、 Microsoft Office が好例の 1 つです。その上現在は、複数の言語用のホスト API の共通セットを提供する DLR のおかげでスクリプト可能なアプリケーションを作成できるようになりました。

たとえば、製品本体に新しい機能の追加を要請しなくても、新しい文字や地図をゲームに追加したり、新しいグラフをビジネス アプリケーションに追加したりなど、ユーザー自身が機能を追加できるアプリケーションを作成できます。

現時点では、DLR のスクリプトおよびホスト API はオープン ソース バージョンの DLR でしか提供されていないため、.NET Framework 4 が使用する DLR ではなく、オープン ソースの DLR (dlr.codeplex.com、英語) を使用する必要があります。また、C# ではなく、IronPython や IronRuby などの .NET 動的言語の 1 つを使用してスクリプトを作成することも想定されています。ただし、たとえ DLR を基盤とした言語でなくても、これらの API はどの言語でもサポートできます。

この機能の使用の詳細については、Dino Viehland による PDC09 でのプレゼンテーション「動的言語を使用してスクリプト可能アプリケーションを作成する」(microsoftpdc.com/2009/FT30、英語) を参照してください。

動的オブジェクトを特定する

動的オブジェクトとそれ以外のオブジェクトはどのようにして見分けるのでしょう。簡単な方法の 1 つは、組み込みの IDE 機能を使用することです。オブジェクトにカーソルを合わせて宣言型を参照するか、IntelliSense を利用できるかどうかを確認します (図 2 参照)。

image: Dynamic Object in Visual Studio

図 2 Visual Studio での動的オブジェクトの確認

ただし、実行時にはもっと複雑な操作が必要です。変数が dynamic キーワードを使用して宣言されているかどうかは確認できません。動的オブジェクトの実行時の型は、オブジェクトに格納されている値の型ですが、オブジェクトの静的型宣言は取得できません。これは、変数をオブジェクトとして宣言しているようなものです。つまり、実行時は、変数が保持している値の型しか取得できず、この変数がもともとオブジェクトとして宣言されているかどうかは判断できません。

実行時に特定できるのは、オブジェクトが DLR に由来しているかどうかです。これは、ExpandoObject や DynamicObject のような型のオブジェクトは実行時に動作が変わる可能性があるため (たとえば、プロパティやメソッドを追加する場合と削除する場合があります)、重要になる場合があります。

また、標準のリフレクション手法を使用して、このようなオブジェクトについての情報を取得することもできません。ExpandoObject クラスのインスタンスにプロパティを追加する場合、このプロパティはリフレクションによって取得できません。

dynamic expando = new ExpandoObject();
expando.SampleProperty = "This property was added at run time";
PropertyInfo dynamicProperty = 
  expando.GetType().GetProperty("SampleProperty");
// dynamicProperty is null.

利用できるのは、.NET Framework 4 では、動的にメンバーを追加および削除できるすべてのオブジェクトは、System.Dynamic.IDynamicMetaObjectProvider という特定のインターフェイスを実装する必要がある点です。DynamicObject クラスにも ExpandoObject クラスにも、このインターフェイスは実装されています。ただし、dynamic キーワードを使用して宣言されたオブジェクトには、必ずこのインターフェイスが実装されているということではありません。

dynamic expando = new ExpandoObject();
Console.WriteLine(expando is IDynamicMetaObjectProvider);
// True

dynamic test = "test";
Console.WriteLine(test is IDynamicMetaObjectProvider);
// False

したがって、リフレクションと併せて dynamic を使用する場合は、動的に追加されたプロパティやメソッドにはリフレクションは有効ではないこと、およびリフレクション対象のオブジェクトに IDynamicMetaObjectProvider インターフェイスが実装されているかどうかを確認すると役に立つ可能性があることに留意してください。

dynamic と COM 相互運用

C# チームが C# 4 のリリースで特に取り組んだ COM 相互運用のシナリオは、Word や Excel などの Microsoft Office アプリケーションを対象にしたプログラミングでした。狙いは、従来の Visual Basic でのプログラミングと同じぐらい、簡単で自然に C# でもプログラミングできるようにすることでした。これも、Visual Basic と C# の両方で同等の機能の提供を図り、それぞれの最も優れた、生産性の高いソリューションを流用し合うという Visual Basic と C# の共進化戦略の一環です。

詳細については、Scott Wiltamuth の Visual Studio ブログで「C# と VB の共進化 (C# and VB coevolution)」(bit.ly/bFUpxG、英語) を参照してください。

図 3 は、Excel ワークシートの最初のセルに値を追加し、最初の列に AutoFit メソッドを適用する C# 4 のコードです。各行の下にあるコメントは、その行に相当する 3.0 以前の C# でのコードを示しています。

図 3 C# による Excel スクリプト

// Add this line to the beginning of the file:
// using Excel = Microsoft.Office.Interop.Excel;

var excelApp = new Excel.Application();

excelApp.Workbooks.Add();
// excelApp.Workbooks.Add(Type.Missing);

excelApp.Visible = true;

Excel.Range targetRange = excelApp.Range["A1"];
// Excel.Range targetRange = excelApp.get_Range("A1", Type.Missing);

targetRange.Value = "Name";
// targetRange.set_Value(Type.Missing, "Name");

targetRange.Columns[1].AutoFit();
// ((Excel.Range)targetRange.Columns[1, Type.Missing]).AutoFit();

この例で興味深いことは、dynamic キーワードがコードのどこにも見当たらないことです。実際に、使用されているのは次の 1 行だけです。

targetRange.Columns[1].AutoFit();
// ((Excel.Range)targetRange.Columns[1, Type.Missing]).AutoFit();

C# 3.0 のコードでは、targetRange.Columns[1, Type.Missing] によってオブジェクトが返されるため、Excel.Range へのキャストが必要です。しかし、C# 4 と Visual Studio 2010 では、このような呼び出しは暗黙のうちに動的呼び出しに変換されます。したがって、C# 4 での targetRange.Columns[1] 型は、実際に動的になります。

もう 1 つの重要な点は、C# 4 での COM 相互運用の機能強化は動的関連の機能だけではありません。インデックス付きのプロパティや名前付きパラメーターと省略可能パラメーターなど、その他の新機能によって、コード全体で構文が簡潔になっています。これらの新機能の概要については、Chris Burrows による秀逸な MSDN Magazineの記事「.NET Framework 4 における C# の新機能」(msdn.microsoft.com/magazine/ff796223) を参照してください。

詳細情報

今月のコラムで、C# 4 の dynamic キーワードについての疑問のほとんどにお答えできていればさいわいですが、すべてに回答できていないことは確かです。コメントやご質問、ご提案がある方は、dlr.codeplex.com/discussions (英語) にアクセスし、奮ってご質問ください。既に同じ問題が質問されている場合がありますが、問題が既出でない場合は、新しいディスカッションを作成できます。活発なコミュニティがあります。新しい仲間は大歓迎です。

Alexandra Rusina は、Silverlight チームのプログラム マネージャーです。それ以前は、Visual Studio 2010 リリースの期間中に、Visual Studio 言語チームでプログラミング ライターを務めていました。また、ブログ「C# Frequently Asked Question」(C# についてよく寄せられる質問、blogs.msdn.com/b/csharpfaq/、英語) にも定期的に投稿しています。

この記事のレビューに協力してくれた技術スタッフの Bill Chiles に心より感謝いたします。