1. 導入
C# は強い型付けの言語です。つまり、コンパイル時に変数は特定の型を持っていなければならず、クラスのメンバーにアクセスする時はその型に従って行います。これは安全で便利で、IDE やコンパイラにとっても扱いやすいです。
しかし、時にはコンパイル時に構造がわからないオブジェクトにアクセスする必要があります。例えば:
- .NET 上の別の言語で書かれたコードを実行する場合(例: IronPython や IronRuby)――これらは動的型付けが主です。
- COM オブジェクト(例: Interop 経由での Microsoft Office)を扱うとき。
- 動的なデータ構造とやりとりする場合: 例えば冗長に形式化されていない JSON オブジェクトなど。
- 大量のボイラープレートを書くのを避けて、オブジェクトを信じてその場でメソッドを名前で呼んでみたいとき。
C# 4.0 でキーワード dynamic と DLR が導入され、C# で「Pythonっぽい」コードを書くことが可能になりました: 検査は実行時に行われ、コンパイル時に厳密にチェックされません。この魔法のようなキーワードでコンパイラに「もう少し緩くして、実行時に確認するよ」と指示できます。
DLR とは何か?
Dynamic Language Runtime (DLR) は CLR (Common Language Runtime) の拡張で、動的言語(および静的言語での動的挙動)をサポートするためのインフラです。DLR はコンパイラが事前に検査できない操作(例: 実行時に型が決まるメソッド呼び出し)を処理します。DLR により C# は「動的モード」を持てるようになります。
2. キーワード dynamic: 動作の仕組み
object との違い
初心者の中には dynamic を単にオシャレな object の書き方だと考える人がいますが、それは違います。
- object 型の変数は、メソッド呼び出しのために常に具体的な型への明示的キャストが必要で(コンパイラがチェックします)、
- dynamic 型の変数はコンパイラに「任せるから実行時に決めてくれ」と指示します。メソッドが存在しなければ実行時に例外になります。
例:
// object の例
object obj = "こんにちは、C#!";
// obj.ToUpper(); // コンパイルエラー!コンパイラは obj が文字列だと知らない
string s = ((string)obj).ToUpper(); // こうするしかない
// dynamic の例
dynamic dyn = "こんにちは、C#!";
string upper = dyn.ToUpper(); // コンパイラは黙る、チェックはランタイムで
dynamic を使えば存在しないプロパティやメソッドにアクセスできます。存在しなければ RuntimeBinderException が発生します。
アプリケーション例での使用法
例えば、ミニアプリに JSON データを扱う部分があり、その構造が実行時に変わることがあるとします。dynamic を使えば、まるでプロパティが常に存在するかのようにアクセスできます:
dynamic user = new System.Dynamic.ExpandoObject();
user.Name = "ヴァーシャ";
user.Age = 35;
Console.WriteLine($"名前: {user.Name}, 年齢: {user.Age}");
3. DLR の動作: 裏側で何が起きるか
変数が dynamic として宣言されると、C# コンパイラはそのオブジェクトに対するメソッド、プロパティ、インデクサ、演算子などのアクセスを事前に検査できないことを記録します。
代わりに、任意の動的呼び出し位置に「システムのディスパッチ要求」への呼び出しが挿入されます。DLR はチェックします: 本当にそのメソッドやプロパティが存在するのか?存在すれば呼び出し、なければ例外を投げます。
視覚的な図
+------------------------+
| dynamic を含むコード |
+----------+-------------+
|
v
+----------+-------------+
| C# コンパイラが挿入する |
| 「DLR への要求」 |
+----------+-------------+
|
v
+----------+-------------+
| 実行時に DLR が探す |
| メソッド/プロパティ |
+----------+-------------+
|
/---------\
| |
発見 未発見
| |
v v
呼び出し 例外発生
実行 (RuntimeBinderException)
DLR はオブジェクト内のメソッドやプロパティ検索を担当し、頻繁に使われる動的呼び出しをキャッシュすることさえあります。
少し歴史的背景
DLR は元々動的言語(IronPython, IronRuby)をサポートするために作られ、C# における動的オブジェクト統合を容易にしました。今日では ExpandoObject や DynamicObject、COM、動的にシリアライズされたデータを扱う際に活用されています。
4. 比較: static と dynamic
プロパティやメソッド呼び出しが静的と動的でどう違うか見てみましょう:
| 方法 | 宣言 | コンパイル時チェック | エラー時の挙動 |
|---|---|---|---|
|
|
必要な型へのキャストを要求 | コンパイルエラーや InvalidCastException |
|
|
いいえ、すべてランタイムで | RuntimeBinderException(メソッドがなければ) |
| 静的型 | |
コンパイラがチェック | コンパイルエラー |
分かりやすい例:
// 静的型付け
MyUser u = new MyUser();
u.PrintInfo(); // コンパイラがチェック
// dynamic
dynamic du = new MyUser();
du.PrintInfo(); // コンパイラはチェックしない、ランタイムで見つける
// object
object ou = new MyUser();
// ou.PrintInfo(); // コンパイルエラー
((MyUser)ou).PrintInfo(); // 型を明示的にキャストする必要がある
静的メソッドと dynamic
dynamic d = "Hello";
Console.WriteLine(d.Length); // 5 — 問題なし
Console.WriteLine(d.FakeMethod()); // 実行時に RuntimeBinderException
注意が必要です。タイポや存在しないメソッド名は実行時まで発覚しないので、いわゆる「本番でのバグ」になり得ます。
5. 実用例: 実際のタスクでの動的利用
ExpandoObject を使った操作
using System.Dynamic;
dynamic person = new ExpandoObject();
person.Name = "セルゲイ";
person.Greet = (Action)(() => Console.WriteLine($"こんにちは、{person.Name}!"));
person.Greet(); // 出力: こんにちは、セルゲイ!
ここでは実行時にプロパティ Name を追加し、デリゲート Greet も追加しました。JavaScript のオブジェクト操作にとても似ています。
JSON 構造へのバインディング
using System.Dynamic;
using Newtonsoft.Json;
string json = @"{""name"": ""アンドレイ"", ""age"": 30}";
dynamic obj = JsonConvert.DeserializeObject
(json); Console.WriteLine(obj.name); // アンドレイ Console.WriteLine(obj.age); // 30
注意: obj.surname のように存在しないプロパティにアクセスすると実行時エラーになります。
COM-Interop(Office 自動化)
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true; // コンパイラはその型を知らないけどプロパティは「見える」
6. 便利な注意点
DLR、dynamic と他の .NET 言語での利用
- IronPython/IronRuby: これらの言語のオブジェクトをインスタンス化して、C# から dynamic 経由でメソッドを呼べます。
- COM オブジェクト: dynamic によるメソッド・プロパティの自動ラップ。
- カスタム動的オブジェクト: IDynamicMetaObjectProvider を実装すれば、動的呼び出しを持つ独自クラスを作れます。
IronPython との統合例
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
ScriptEngine engine = Python.CreateEngine();
dynamic py = engine.Execute("class Test: pass\nTest()");
py.x = 42;
Console.WriteLine(py.x); // 42
独自の動的型を実装する方法
普通は DynamicObject を継承するのが簡単です。
using System.Dynamic;
class MyDynamic : DynamicObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "Ping")
{
result = "Pong!";
return true;
}
result = null;
return false;
}
}
dynamic d = new MyDynamic();
Console.WriteLine(d.Ping); // Pong!
Console.WriteLine(d.Miss); // null(例外は投げない)
dynamic が害になる場合
- パフォーマンスが低下する(動的呼び出しは静的より遅い)。
- デバッグやメンテナンスが難しくなる。
- IDE の機能(オートコンプリート、リファクタリング、参照検索など)をほとんど使えなくなる。
- ユーザーやサーバーでしか発見されないエラーにつながる可能性がある。
いつ dynamic を本当に使うべきか
- ラッパーを書くのが不可能または非現実的な外部ライブラリと統合する場合(COM、Python、JavaScript)。
- 型があいまいな構造(JSON/XML/ExpandoObject)を操る場合。
- フレームワーク作り、メタプログラミング、意図的に動的であるべき柔軟な API を書く場合。
それ以外のケースでは静的型を使ってコンパイラの助けを享受するのが良いでしょう :-)
7. dynamic と DLR を使う際の典型的なミス
ミス #1: 存在しないメンバーへのアクセス。
dynamic に対して存在しないメソッドやプロパティを呼ぶと、ランタイムで RuntimeBinderException が発生し、開発時に捕まえにくいです。
ミス #2: 型チェックを無視する。
呼び出し前に型チェックを行わない(例えば if (obj is IDictionary<string, object>) のような)と、ランタイムで予期しないエラーが出ます。
ミス #3: public API での dynamic の乱用。
動的型はコードの保守性を下げます。IDE や解析ツールが正しいメンバーを教えてくれないため可読性が落ちます。
ミス #4: パフォーマンスが重要な場所での dynamic の濫用。
動的呼び出しは静的呼び出しより遅いので、必要な場合のみ dynamic を使うべきです。
GO TO FULL VERSION