Menu

C#のリフレクションと属性:typeof、GetType、カスタム属性

C#のリフレクションの仕組みを解説します。typeofとGetTypeによるTypeオブジェクト、プロパティの読み取りと設定、名前によるメソッドの呼び出し、インスタンスの作成、そして属性(Obsoleteなどの組み込みのもの、独自の属性の宣言、実行時の読み取り)を扱います。リフレクションのコストも説明します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

リフレクションを使うと、プログラムは実行中に型を調べられます。クラスがどんなプロパティを持ち、その値は何か、どんなメソッドがあり、どんな属性が付いているかです。属性はもう一方の半分で、[Obsolete] や [JsonPropertyName("id")] のような角かっこの中の宣言的なタグです。コンパイラや何らかのコードがリフレクションで読むまで、それ自体は何の意味も持ちません。シリアライザー、ORM、検証ライブラリ、テストフレームワーク、ASP.NETのルーティングは、すべてこの仕組みで動いています。

ほとんどのアプリケーションのコードがリフレクションを直接必要とすることはまれですが、その仕組みを知っていると、フレームワークの多くの振る舞いが理解できます。

Typeオブジェクト:typeofとGetType

すべては System.Type から始まります。取得する方法は2つあります。

出力:

Employee
Manager
Employee
False
True
True
System.Int32
False
Name

覚えておくべき違い:typeof(X) はコードを書くときにわかっている型を指し、obj.GetType() はオブジェクトに本当は何なのかを尋ねます。GetType() == typeof(Employee) の比較は完全な一致で、派生クラスでは失敗し、通常それは望ましくありません。is と IsAssignableFrom は継承を考慮します。nameof は似て見えますが、まったくリフレクションではありません。コンパイラが文字列の定数に置き換えます。

null 参照に対する GetType() は、尋ねる対象のオブジェクトがないので NullReferenceException を投げます。ボックス化されたnull許容値型に対しては、基になる型を返します:((int?)5).GetType() は System.Int32 です。

プロパティの読み取りと設定

GetProperties() は型の公開プロパティを PropertyInfo オブジェクトとして一覧にし、それぞれが特定のインスタンスの値を読み書きできます。

出力:

Name   String   = Mug
Price  Decimal  = 8.50
Stock  Int32    = 12
7.90
True
SUP-77

ここからわかる3つのこと:

  • GetValue は object を返すので、値型はボックス化されて返り、使うにはキャストします。
  • 存在しない名前での GetProperty は null を返し、それに対する次の呼び出しで NullReferenceException が投げられます。使う前に確認します。
  • BindingFlags.NonPublic | BindingFlags.Instance はprivateなメンバーに届きます。ツールやテストでは正当な使い方ですが、カプセル化を迂回し、クラスをリファクタリングすると何も言わずに壊れます。

このループは、本質的にはCSVのエクスポーターやオブジェクトからJSONへのシリアライザーの仕組みそのものです。プロパティをたどり、各値を読み、書式設定します。

名前によるメソッドの呼び出しとオブジェクトの作成

GetMethod はメソッドを見つけ、Invoke は引数の配列でそれを呼び出します。Activator.CreateInstance は Type からオブジェクトを作ります。これがプラグインのシステムやDIコンテナーが、実行時に選んだ型を組み立てる方法です。

出力:

60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True

DeclaredOnly は一覧を、クラス自身で宣言されたメンバーに限定します。これがないと、GetMethods は object の ToString、Equals、GetHashCode、GetType も返します。Type.GetType("Name") には名前空間で修飾した名前が必要で、他のアセンブリの型ならアセンブリ名も必要です("MyApp.Plugins.Csv, MyApp.Plugins")。

呼び出したメソッドが例外を投げると、Invoke はその例外を TargetInvocationException で包みます。元の例外はその InnerException にあります。

属性:コンパイラとフレームワークが読むタグ

属性は、説明する対象の前に角かっこで書きます。フレームワークは多くの属性を定義していて、コンパイラの動作を変えるものもいくつかあります。

public class OrderService
{
    [Obsolete("Use PlaceOrderAsync instead.")]
    public void PlaceOrder(Order order) { }
    // Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
    // [Obsolete("...", true)] makes it error CS0619 instead.

    [Conditional("DEBUG")]
    public void Trace(string message) => Console.WriteLine(message);
    // Calls to Trace are removed entirely from builds without the DEBUG symbol.
}

[Flags] enum Channels { None = 0, Email = 1, Sms = 2 }   // changes how ToString formats combinations
[Serializable] class Snapshot { }                     // marks a type for legacy binary serialization

他のものは、ライブラリが実行時に読みます。[JsonPropertyName] と [JsonIgnore] は System.Text.Json が、[Required] と [MaxLength] はASP.NET Coreのモデルの検証とEntity Frameworkが、[HttpGet("orders/{id}")] はASP.NETのルーティングが、[Fact] と [Test] はテストランナーが読みます。属性そのものは何もせず、何かをするのはそれを探すコードです。

ObsoleteAttribute という名前は、適用するときに [Obsolete] に短縮されます。慣例として属性のクラス名はすべて Attribute で終わり、C#ではその接尾辞を省けます。

カスタム属性の宣言と読み取り

カスタム属性は Attribute から派生したクラスです。[AttributeUsage] で、何に適用できるかを示します。コンストラクターの引数は位置指定の引数に、公開の設定可能なプロパティは名前付きの引数になります。

出力:

Username must be at most 20 characters
Email is required
Keep the city code short
0

これは、ASP.NET Coreのモデルの検証が System.ComponentModel.DataAnnotations で行っていることの小型版です。属性の引数はアセンブリのメタデータに格納されるので、コンパイル時定数(数値、文字列、typeof(...)、enumの値、またはそれらの配列)でなければなりません。GetCustomAttribute<T>() は System.Reflection にある拡張メソッドで、属性があるかどうかだけを知りたいときには IsDefined(typeof(T)) もあります。

リフレクションのコスト

リフレクションは、速度と安全性を柔軟性と引き換えにします。

  • 速度。 名前でメンバーを探して Invoke や GetValue で呼び出すのは、直接の呼び出しよりはるかに遅く、値型をボックス化します。繰り返し使うなら、PropertyInfo や MethodInfo を一度だけ探して保持するか、Delegate.CreateDelegate や MethodInfo.CreateDelegate でデリゲートに変えて、それを呼びます。
  • 安全性。 名前の綴りの間違いやシグネチャの変更はそのままコンパイルでき、実行時に失敗します。名前の変更を検出できるように、できる限り文字列の "Price" より nameof(Product.Price) を優先します。
  • トリミングとAOT。 トリミングされたアプリやNative AOTのアプリは、何も使っていないように見えるメンバーを取り除き、リフレクションはその分析から使用を隠してしまいます。現代のライブラリ(System.Text.Json、GeneratedRegex、ロギング)は、同じ処理をコンパイル時に行うソースジェネレーターに移行しつつあります。

リフレクションは、プログラムの中で本当に事前に型がわからない部分、つまりプラグイン、汎用のツール、シリアライザー、テストのヘルパーに使います。型がわかっているなら、普通のコード、ジェネリック、インターフェースのほうが速く、コンパイラの確認も受けられます。

よくある間違い

  • 基底クラスの判定に GetType() == typeof(Base) を使う。 派生型では失敗します。is か IsAssignableFrom を使います。
  • null を確認しない。 GetProperty、GetMethod、Type.GetType は、何も一致しないと null を返します。
  • キャッシュせずに性能が重要なループでリフレクションを使う。 MemberInfo をキャッシュするか、デリゲートにコンパイルします。
  • Invoke から間違った例外をキャッチする。 本当の例外は TargetInvocationException の InnerException です。
  • メンバー名に魔法の文字列を使う。 nameof を使います。

よくある質問

C#のリフレクションとは何ですか?

リフレクションは、プログラムが実行時に型を調べる能力です。クラスのプロパティやメソッドを一覧にし、名前で値を読み書きし、メソッドを呼び出し、インスタンスを作り、属性を読みます。System.Reflection にあり、Type オブジェクトから始まります。シリアライザー、ORM、依存性の注入のコンテナー、テストフレームワークはこの上に作られています。

C#のtypeofとGetTypeの違いは何ですか?

typeof(Customer) はコンパイル時に型名から解決され、オブジェクトを必要としません。obj.GetType() は実行時にインスタンスに対して呼ばれ、オブジェクトの実際の型を返し、それは変数の宣言された型より派生した型かもしれません。Animal a = new Dog(); では、a.GetType() は Dog です。null参照に対する GetType() は NullReferenceException を投げます。

C#で名前からプロパティの値を取得するには?

obj.GetType().GetProperty("Price") は PropertyInfo を返し(そのような公開プロパティがなければ null)、.GetValue(obj) がそれを object として読みます。.SetValue(obj, value) は書き込みます。ループの中でこれを行うなら、検索が高コストな部分なので PropertyInfo をキャッシュします。

C#でカスタム属性を作るには?

System.Attribute から派生したクラスを宣言し、Attribute の接尾辞を付けた名前にし、[AttributeUsage] で使える場所を示します:[AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }。[MaxLength(50)] として適用し、property.GetCustomAttribute<MaxLengthAttribute>() で読みます。

C#のObsolete属性は何をしますか?

メンバーに付けた [Obsolete("Use PlaceOrderAsync instead")] は、すべての呼び出し箇所で、コンパイラにメッセージ付きの警告CS0618を出させます。[Obsolete("...", true)] は警告をエラーCS0619に変えます。ライブラリが、呼び出し側を一夜で壊すことなくAPIを廃止する方法です。

C#のリフレクションは遅いですか?

直接の呼び出しと比べれば遅いです。名前でメンバーを探し、MethodInfo.Invoke や PropertyInfo.GetValue で呼び出すと、通常は数十倍から数百倍遅くなり、値型はボックス化されます。起動時、設定、ときどきの使用なら問題ありません。性能が重要な経路では、MemberInfo をキャッシュするか、一度だけデリゲートを作るか、代わりにジェネリックやソースジェネレーターを使います。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める