前回は、プラグインで拡張できるアプリケーション構造を作りました。
今回は、dynamic を使った動的な呼び出しを見ていきます。
dynamic は、コンパイル時ではなく実行時にメンバーを解決するための仕組みです。
リフレクションより簡潔に書ける場面がありますが、型安全性を一部手放すため、使いどころを選ぶ必要があります。
dynamic とは
通常の C# では、メソッドやプロパティの存在はコンパイル時にチェックされます。
string text = "hello";
Console.WriteLine(text.Length);
Length が存在しなければ、コンパイルエラーになります。
一方、dynamic を使うと、メンバーの存在チェックが実行時に遅れます。
dynamic value = "hello";
Console.WriteLine(value.Length);
このコードは実行時に Length を探します。
もし存在しないメンバーを呼び出すと、コンパイルエラーではなく実行時例外になります。
dynamic value = "hello";
// 実行時に失敗する
Console.WriteLine(value.UnknownMember);
動的に宣言した値のメンバーを呼ぶ
dynamic の便利なところは、メンバー呼び出しの見た目が普通の C# とほぼ同じになる点です。
public class Greeter
{
public string SayHello(string name)
{
return $"Hello, {name}";
}
}
dynamic greeter = new Greeter();
string message = greeter.SayHello("Alice");
Console.WriteLine(message);
リフレクションで同じことを書くと、次のようになります。
object greeter = new Greeter();
Type type = greeter.GetType();
MethodInfo? method = type.GetMethod("SayHello");
object? result = method?.Invoke(greeter, new object?[] { "Alice" });
Console.WriteLine(result);
dynamic の方が短く、読みやすい場面があります。
dynamic の適用範囲
dynamic は、変数、引数、戻り値、コレクション要素などに使えます。
static dynamic CreateValue(bool text)
{
return text ? "hello" : 123;
}
dynamic value = CreateValue(true);
Console.WriteLine(value.Length);
メソッド引数にも使えます。
static void PrintName(dynamic value)
{
Console.WriteLine(value.Name);
}
ただし、このメソッドは Name を持たない値が渡されると実行時に失敗します。
PrintName(new { Name = "Alice" });
PrintName(123); // 実行時に失敗
dynamic は便利ですが、呼び出し側と呼び出される側の契約がコード上で見えにくくなります。
dynamic の制限
dynamic は何でも解決してくれる魔法ではありません。
主な制限や注意点は次の通りです。
- メンバーの存在チェックが実行時になる
- リファクタリングで検出しづらい
- IntelliSense が効きにくい
- 実行時例外が増えやすい
- API の契約が読み取りにくくなる
そのため、アプリケーション全体に広げるより、境界部分だけで使うのが安全です。
たとえば、外部サービスから得た柔軟な JSON、COM 連携、スクリプト連携、リフレクション呼び出しの薄いラッパーなどです。
実用的な使いどころ
dynamic が役立つ代表例をいくつか見てみます。
形が変わるデータを一時的に扱う
dynamic item = new
{
Id = 1,
Name = "Sample"
};
Console.WriteLine(item.Name);
ただし、長く使うデータ構造なら、クラスや record を定義した方が安全です。
リフレクション呼び出しを簡潔にする
object instance = Activator.CreateInstance(typeof(Greeter))!;
dynamic dynamicGreeter = instance;
Console.WriteLine(dynamicGreeter.SayHello("Alice"));
メソッド名を文字列で指定するより自然に書けます。
ただし、メソッドが存在しない場合は実行時に失敗します。
相互運用の境界で使う
COM やスクリプト的な API など、コンパイル時に型情報が扱いづらい相手との境界で役立つことがあります。
アプリ内部のドメインロジックでは、通常の静的型付けを使う方が保守しやすいです。
動的な呼び出しを支える仕組み
dynamic の裏側では、動的なメンバー解決を支えるランタイムの仕組みが働きます。
C# コンパイラーは、dynamic な呼び出しを通常の静的呼び出しとして確定しません。
実行時に、対象オブジェクトの型や呼び出し内容をもとに、どのメンバーを呼ぶかを解決します。
この仕組みにより、C# から動的言語的な呼び出しができます。
ただし、実行時解決にはコストがあります。
大量に繰り返される処理の内部で無造作に使うと、性能や可読性に影響する可能性があります。
式木とは
式木は、コードを「実行するもの」ではなく「データ構造」として表現する仕組みです。
using System.Linq.Expressions;
Expression<Func<int, int, int>> expression = (x, y) => x + y;
Console.WriteLine(expression);
この例では、x + y という式が、実行結果ではなく構造として保持されます。
式木は、LINQ プロバイダー、ORM、動的クエリ生成などで使われます。
たとえば Entity Framework Core は、式木を解析して SQL に変換します。
式木を実行する
式木は、コンパイルしてデリゲートとして実行できます。
using System.Linq.Expressions;
Expression<Func<int, int, int>> expression = (x, y) => x + y;
Func<int, int, int> add = expression.Compile();
Console.WriteLine(add(2, 3));
式木は「実行できるコード」でもあり、「解析できるデータ」でもあります。
プラグインやルールエンジンを作る場合、条件式をデータとして組み立てる用途でも使えます。
dynamic で遅延呼び出しを簡潔にする
リフレクションでメソッドを呼ぶと、どうしてもコードが長くなります。
object plugin = Activator.CreateInstance(pluginType)!;
MethodInfo? method = pluginType.GetMethod("Execute");
method?.Invoke(plugin, new object?[] { "input.csv" });
dynamic を使うと、見た目は普通のメソッド呼び出しに近くなります。
dynamic plugin = Activator.CreateInstance(pluginType)!;
plugin.Execute("input.csv");
引数も自然に渡せます。
dynamic plugin = Activator.CreateInstance(pluginType)!;
string result = plugin.Convert("input.csv", "output.json");
Console.WriteLine(result);
ただし、プラグイン設計では、可能なら共通インターフェイスを使う方が安全です。
dynamic は、どうしても静的な契約を作りにくい境界で使う補助的な選択肢と考えるのがよいです。
プラグイン設計での使い分け
拡張可能なアプリケーションでは、次の順で検討すると整理しやすいです。
まず共通インターフェイスを使う
もっとも安全です。
コンパイル時に契約を確認でき、呼び出し側も読みやすくなります。
public interface IPlugin
{
Task ExecuteAsync(CancellationToken token);
}
属性で発見しやすくする
インターフェイスだけでは表示名やカテゴリが不足する場合、属性を追加します。
[Plugin("CSV 取り込み", Category = "Import")]
public sealed class CsvPlugin : IPlugin
{
public Task ExecuteAsync(CancellationToken token)
{
return Task.CompletedTask;
}
}
dynamic は境界を小さくして使う
どうしても実行時まで形が分からない相手には dynamic を使えます。
ただし、呼び出し箇所を少なくし、例外処理を厚めにします。
try
{
dynamic plugin = Activator.CreateInstance(pluginType)!;
plugin.Execute();
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex)
{
Console.WriteLine($"呼び出しに失敗しました: {ex.Message}");
}
まとめ
dynamic は、実行時にメンバーを解決するための仕組みです。
リフレクションより短く書ける場面がありますが、型安全性や保守性を失いやすいため、使いどころを選ぶ必要があります。
式木は、コードをデータ構造として扱う仕組みです。
LINQ プロバイダーや動的なクエリ生成など、実行前に式を解析したい場面で活躍します。
Chapter 17 のテーマ全体を通して見ると、型メタデータ、リフレクション、属性、動的呼び出しは、どれも「実行時にアプリケーションを理解し、拡張する」ための技術です。
これらを設計の道具として使えるようになると、プラグイン、診断ツール、ルールエンジン、フレームワーク的な仕組みを作る力がぐっと伸びます。