C#リフレクション入門:型情報の取得からメソッド実行まで使い方をわかりやすく解説

はじめに

C#リフレクションは、プログラムの実行中にクラス、プロパティ、メソッド、属性などの情報を取得したり、動的に値を読み書きしたり、メソッドを実行したりできる仕組みです。

通常のC#コードでは、呼び出す型やメソッドをコンパイル時に決めておきます。一方、リフレクションを使うと、実行時に型情報を調べて処理を分岐できます。

たとえば、次のような場面でC# reflectionはよく使われます。

  • オブジェクトのプロパティ一覧を自動で取得したい

  • クラス名やメソッド名を文字列で指定して実行したい

  • 属性を使って処理対象を判定したい

  • プラグインのように後から追加されるクラスを読み込みたい

  • テストコードでprivateメンバーの動作を確認したい

  • フレームワークやライブラリの内部処理を理解したい

この記事では、C#リフレクションの基本から、型情報の取得、プロパティやフィールドの操作、メソッド実行、インスタンス生成、属性の扱い、実務での活用例、注意点、パフォーマンス改善までを順番に解説します。

1. C#のリフレクションとは

C#のリフレクションとは、実行時にプログラム自身の構造を調べたり操作したりする機能です。

クラスの名前、名前空間、プロパティ、フィールド、メソッド、コンストラクター、属性などをコード上から取得できます。また、取得した情報を使って、プロパティの値を変更したり、メソッドを呼び出したり、インスタンスを生成したりすることもできます。

リフレクションの中心になるのは、System.TypeクラスやSystem.Reflection名前空間に含まれるPropertyInfoMethodInfoFieldInfoConstructorInfoなどのクラスです。

1-1. リフレクションでできること

C#リフレクションでは、主に次のような操作ができます。

C#
Type type = typeof(string);

Console.WriteLine(type.Name); // String
Console.WriteLine(type.FullName); // System.String
Console.WriteLine(type.Namespace); // System

型情報を取得するだけでなく、次のような処理も可能です。

C#
var properties = typeof(User).GetProperties();

foreach (var property in properties)
{
Console.WriteLine(property.Name);
}

このコードでは、Userクラスに定義されているpublicプロパティの一覧を取得しています。

さらに、メソッド名を文字列で指定して呼び出すこともできます。

C#
User user = new User();
MethodInfo? method = typeof(User).GetMethod("SayHello");

method?.Invoke(user, null);

このように、リフレクションを使うと、コンパイル時に直接呼び出しを書かなくても、実行時に柔軟な処理ができます。

1-2. 通常のコード実行との違い

通常のコードでは、呼び出すメソッドやアクセスするプロパティはコンパイル時に決まります。

C#
User user = new User();
user.Name = "Alice";
user.SayHello();

この場合、NameプロパティやSayHelloメソッドが存在しなければ、コンパイル時点でエラーになります。

一方、リフレクションでは名前を文字列で指定できます。

C#
User user = new User();
PropertyInfo? property = typeof(User).GetProperty("Name");

property?.SetValue(user, "Alice");

この場合、"Name"という文字列が間違っていても、コンパイル時には検出されません。実行時にnullが返ったり、例外が発生したりします。

つまり、通常のコードは安全で高速ですが柔軟性は低めです。リフレクションは柔軟ですが、実行時エラーやパフォーマンス低下に注意が必要です。

1-3. リフレクションが必要になる代表的な場面

リフレクションが特に役立つのは、処理対象の型やメンバーが実行時まで決まらない場面です。

たとえば、JSONのキーとオブジェクトのプロパティを対応させる処理、属性を読み取ってバリデーションする処理、アセンブリ内の特定インターフェース実装クラスを探す処理などです。

C#
foreach (var property in typeof(User).GetProperties())
{
Console.WriteLine($"{property.Name}: {property.PropertyType.Name}");
}

このような処理は、クラスのプロパティ数や名前が変わっても、コードを大きく変更せずに対応できます。

ただし、単純にメソッドを呼び出すだけなら、通常のメソッド呼び出し、interface、genericsなどを使う方が適しています。

1-4. この記事で学べること

この記事では、C# reflectionを初めて使う人でも理解できるように、次の内容を解説します。

  • Typeを使った型情報の取得

  • typeofGetTypeの違い

  • プロパティ、フィールド、メソッド、コンストラクターの取得

  • BindingFlagsを使ったprivateメンバーの取得

  • リフレクションによる値の取得と変更

  • MethodInfo.Invokeによるメソッド実行

  • Activator.CreateInstanceによるインスタンス生成

  • 属性の取得と活用

  • 実務での使用例

  • パフォーマンス改善と注意点

  • generics、interface、dynamic、Expression Tree、Source Generatorとの使い分け

2. C#リフレクションの基本知識

リフレクションを理解するには、まず「型情報」と「メタデータ」という考え方を押さえる必要があります。

C#で書いたクラスやメソッドは、コンパイルされるとアセンブリに含まれます。このアセンブリには、実行コードだけでなく、型名、メソッド名、引数、戻り値、属性などのメタデータも含まれています。

リフレクションは、このメタデータを実行時に読み取るための仕組みです。

2-1. 型情報とは何か

型情報とは、クラスや構造体、インターフェース、列挙型などに関する情報です。

たとえば、次のようなクラスがあるとします。

C#
public class User
{
public string Name { get; set; } = "";
public int Age { get; set; }

public void SayHello()
{
Console.WriteLine($"Hello, {Name}");
}
}

このUser型には、次のような情報があります。

  • 型名はUser

  • publicプロパティとしてNameAgeを持つ

  • SayHelloメソッドを持つ

  • objectを基底クラスに持つ

  • 所属する名前空間やアセンブリがある

これらの情報を実行時に取得するのがリフレクションです。

C#
Type type = typeof(User);

Console.WriteLine(type.Name);
Console.WriteLine(type.BaseType);

2-2. System.Reflection名前空間の役割

リフレクション関連の多くのクラスは、System.Reflection名前空間に含まれています。

C#
using System.Reflection;

よく使うクラスは次のとおりです。

C#
Type type = typeof(User);

PropertyInfo[] properties = type.GetProperties();
MethodInfo[] methods = type.GetMethods();
FieldInfo[] fields = type.GetFields();
ConstructorInfo[] constructors = type.GetConstructors();

それぞれの役割は次のようになります。

  • Type:型そのものの情報を表す

  • PropertyInfo:プロパティ情報を表す

  • FieldInfo:フィールド情報を表す

  • MethodInfo:メソッド情報を表す

  • ConstructorInfo:コンストラクター情報を表す

  • Assembly:アセンブリ情報を表す

  • BindingFlags:取得対象の範囲を指定する

リフレクションを使うときは、まずTypeを取得し、そこからプロパティやメソッドなどを取得する流れが基本です。

2-3. Typeクラスの基本

Typeクラスは、C#リフレクションの中心となるクラスです。

C#
Type type = typeof(User);

Console.WriteLine(type.Name); // User
Console.WriteLine(type.FullName); // MyApp.User など
Console.WriteLine(type.Namespace); // MyApp など
Console.WriteLine(type.IsClass); // True
Console.WriteLine(type.IsInterface); // False
Console.WriteLine(type.IsValueType); // False

Typeを使うと、型の名前だけでなく、クラスかどうか、インターフェースかどうか、値型かどうかなども判定できます。

C#
if (type.IsClass)
{
Console.WriteLine("これはクラスです");
}

型に応じて処理を分岐したい場合に便利です。

2-4. typeofとGetTypeの違い

型情報を取得する代表的な方法には、typeofGetTypeがあります。

typeofは、型名からTypeを取得します。

C#
Type type = typeof(User);

一方、GetTypeは、インスタンスから実際の型を取得します。

C#
User user = new User();
Type type = user.GetType();

大きな違いは、typeofはコンパイル時に型が決まっている場合に使い、GetTypeは実行時のインスタンスから型を調べる場合に使う点です。

C#
object obj = new User();

Console.WriteLine(typeof(object).Name); // Object
Console.WriteLine(obj.GetType().Name); // User

この例では、変数の型はobjectですが、実体はUserです。GetTypeを使うと、実行時の実際の型を取得できます。

2-5. Assemblyから型情報を取得する仕組み

アセンブリとは、コンパイルされた.dll.exeの単位です。リフレクションでは、アセンブリから含まれる型一覧を取得できます。

C#
Assembly assembly = typeof(User).Assembly;

foreach (Type type in assembly.GetTypes())
{
Console.WriteLine(type.FullName);
}

現在実行中のアセンブリを取得する場合は、次のように書けます。

C#
Assembly assembly = Assembly.GetExecutingAssembly();

Type[] types = assembly.GetTypes();

プラグイン機能や自動登録処理では、アセンブリ内の型を走査し、特定のインターフェースを実装しているクラスを探すことがあります。

C#
var pluginTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);

3. 型情報を取得する基本的な使い方

ここからは、C#リフレクションで型情報を取得する基本的な使い方を見ていきます。

サンプルとして、次のクラスを使います。

C#
public interface IEntity
{
int Id { get; set; }
}

public class Person
{
public string Name { get; set; } = "";
}

public class User : Person, IEntity
{
public int Id { get; set; }
public int Age { get; set; }
}

3-1. typeofを使って型情報を取得する

typeofを使うと、指定した型のTypeオブジェクトを取得できます。

C#
Type type = typeof(User);

Console.WriteLine(type.Name);
Console.WriteLine(type.FullName);

typeofは、型がコード上で明確に決まっている場合に使います。

C#
Type stringType = typeof(string);
Type intType = typeof(int);
Type userType = typeof(User);

ジェネリック型でも使えます。

C#
Type listType = typeof(List<string>);

Console.WriteLine(listType.Name); // List`1
Console.WriteLine(listType.FullName);

型そのものを調べたい場合は、まずtypeofから始めると分かりやすいです。

3-2. インスタンスからGetTypeで型情報を取得する

GetTypeは、インスタンスから実行時の型情報を取得します。

C#
User user = new User();

Type type = user.GetType();

Console.WriteLine(type.Name); // User

ポリモーフィズムが関係する場面では、GetTypeが特に重要です。

C#
Person person = new User();

Console.WriteLine(typeof(Person).Name); // Person
Console.WriteLine(person.GetType().Name); // User

変数の宣言型ではなく、実際に生成されたオブジェクトの型を知りたいときはGetTypeを使います。

3-3. クラス名や名前空間を取得する

Typeからクラス名や名前空間を取得できます。

C#
Type type = typeof(User);

Console.WriteLine(type.Name); // User
Console.WriteLine(type.FullName); // 名前空間を含む完全名
Console.WriteLine(type.Namespace); // 名前空間

Nameは型名のみ、FullNameは名前空間を含む名前です。

C#
if (type.FullName == "MyApp.Models.User")
{
Console.WriteLine("User型です");
}

ただし、文字列で型名を比較すると、名前変更時に壊れやすくなります。可能であればtypeof(User)との比較を使う方が安全です。

C#
if (type == typeof(User))
{
Console.WriteLine("User型です");
}

3-4. 基底クラスやインターフェースを取得する

基底クラスはBaseTypeで取得できます。

C#
Type type = typeof(User);

Console.WriteLine(type.BaseType?.Name); // Person

実装しているインターフェースはGetInterfacesで取得できます。

C#
Type[] interfaces = type.GetInterfaces();

foreach (Type interfaceType in interfaces)
{
Console.WriteLine(interfaceType.Name);
}

特定のインターフェースを実装しているかを判定する場合は、IsAssignableFromが便利です。

C#
bool result = typeof(IEntity).IsAssignableFrom(typeof(User));

Console.WriteLine(result); // True

この判定は、DIコンテナの自動登録やプラグイン探索でよく使われます。

3-5. アセンブリ内の型一覧を取得する

アセンブリ内の型一覧は、Assembly.GetTypesで取得できます。

C#
Assembly assembly = Assembly.GetExecutingAssembly();

foreach (Type type in assembly.GetTypes())
{
Console.WriteLine(type.FullName);
}

特定の条件に合う型だけを取得することもできます。

C#
var entityTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => typeof(IEntity).IsAssignableFrom(t))
.Where(t => t.IsClass)
.Where(t => !t.IsAbstract);

foreach (Type type in entityTypes)
{
Console.WriteLine(type.Name);
}

このように、リフレクションを使えば、アプリケーション内に存在する型を動的に検索できます。

4. メンバー情報を取得する方法

型情報を取得したら、次はその型が持つメンバーを取得します。

メンバーとは、プロパティ、フィールド、メソッド、コンストラクターなどを指します。

次のクラスを例にします。

C#
public class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }

private string secretCode = "ABC";

public Product()
{
}

public Product(string name, decimal price)
{
Name = name;
Price = price;
}

public void Display()
{
Console.WriteLine($"{Name}: {Price}");
}

private void InternalProcess()
{
Console.WriteLine("内部処理");
}
}

4-1. プロパティ情報を取得する

プロパティ情報はGetPropertiesで取得します。

C#
Type type = typeof(Product);

PropertyInfo[] properties = type.GetProperties();

foreach (PropertyInfo property in properties)
{
Console.WriteLine($"{property.Name}: {property.PropertyType.Name}");
}

特定のプロパティだけを取得する場合はGetPropertyを使います。

C#
PropertyInfo? property = typeof(Product).GetProperty("Name");

Console.WriteLine(property?.Name);
Console.WriteLine(property?.PropertyType);

GetPropertyは、指定した名前のプロパティが見つからない場合にnullを返します。そのため、nullチェックを行うと安全です。

4-2. フィールド情報を取得する

フィールド情報はGetFieldsで取得します。

C#
FieldInfo[] fields = typeof(Product).GetFields();

foreach (FieldInfo field in fields)
{
Console.WriteLine($"{field.Name}: {field.FieldType.Name}");
}

ただし、引数なしのGetFieldsで取得できるのはpublicフィールドです。privateフィールドを取得するには、後述するBindingFlagsを使います。

C#
FieldInfo? field = typeof(Product).GetField(
"secretCode",
BindingFlags.NonPublic | BindingFlags.Instance
);

Console.WriteLine(field?.Name);

C#では通常、外部から扱う値はフィールドではなくプロパティとして公開することが多いため、実務ではGetPropertiesの方がよく使われます。

4-3. メソッド情報を取得する

メソッド情報はGetMethodsで取得します。

C#
MethodInfo[] methods = typeof(Product).GetMethods();

foreach (MethodInfo method in methods)
{
Console.WriteLine(method.Name);
}

GetMethodsを使うと、ToStringEqualsなど、objectから継承されたメソッドも含まれます。

特定のメソッドを取得したい場合はGetMethodを使います。

C#
MethodInfo? method = typeof(Product).GetMethod("Display");

Console.WriteLine(method?.Name);
Console.WriteLine(method?.ReturnType);

引数の情報も取得できます。

C#
foreach (ParameterInfo parameter in method!.GetParameters())
{
Console.WriteLine($"{parameter.Name}: {parameter.ParameterType.Name}");
}

4-4. コンストラクター情報を取得する

コンストラクター情報はGetConstructorsで取得します。

C#
ConstructorInfo[] constructors = typeof(Product).GetConstructors();

foreach (ConstructorInfo constructor in constructors)
{
var parameters = constructor.GetParameters();

Console.WriteLine($"引数数: {parameters.Length}");
}

特定の引数を持つコンストラクターを取得するには、GetConstructorを使います。

C#
ConstructorInfo? constructor = typeof(Product).GetConstructor(
new[] { typeof(string), typeof(decimal) }
);

Console.WriteLine(constructor != null);

コンストラクター情報は、リフレクションでインスタンスを生成するときに使えます。

4-5. public以外のメンバーを取得する

通常のGetPropertiesGetFieldsGetMethodsでは、publicメンバーが主な取得対象になります。

privateメンバーやprotectedメンバーを取得したい場合は、BindingFlagsを指定します。

C#
FieldInfo? field = typeof(Product).GetField(
"secretCode",
BindingFlags.NonPublic | BindingFlags.Instance
);

privateメソッドを取得する例です。

C#
MethodInfo? method = typeof(Product).GetMethod(
"InternalProcess",
BindingFlags.NonPublic | BindingFlags.Instance
);

privateメンバーへアクセスできることは便利ですが、クラスの内部実装に強く依存します。実務では、テストやフレームワーク内部など、必要性が明確な場面に限定するのが望ましいです。

4-6. BindingFlagsの使い方

BindingFlagsは、リフレクションで取得するメンバーの範囲を指定するための列挙型です。

よく使う指定は次のとおりです。

C#
BindingFlags.Public
BindingFlags.NonPublic
BindingFlags.Instance
BindingFlags.Static
BindingFlags.DeclaredOnly

たとえば、publicなインスタンスプロパティだけを取得する場合は、次のように書きます。

C#
var properties = typeof(Product).GetProperties(
BindingFlags.Public | BindingFlags.Instance
);

privateなインスタンスフィールドを取得する場合は、次のように書きます。

C#
var fields = typeof(Product).GetFields(
BindingFlags.NonPublic | BindingFlags.Instance
);

staticメソッドを取得する場合は、BindingFlags.Staticを使います。

C#
var methods = typeof(Math).GetMethods(
BindingFlags.Public | BindingFlags.Static
);

BindingFlagsは組み合わせて使うのが基本です。PublicまたはNonPublicInstanceまたはStaticを適切に指定しないと、期待したメンバーが取得できないことがあります。

5. リフレクションで値を取得・変更する方法

リフレクションでは、取得したPropertyInfoFieldInfoを使って、オブジェクトの値を読み書きできます。

次のクラスを例にします。

C#
public class User
{
public string Name { get; set; } = "";
public int Age { get; set; }

private string password = "secret";
}

5-1. プロパティの値を取得する

プロパティの値を取得するには、PropertyInfo.GetValueを使います。

C#
User user = new User
{
Name = "Alice",
Age = 30
};

PropertyInfo? property = typeof(User).GetProperty("Name");

object? value = property?.GetValue(user);

Console.WriteLine(value); // Alice

戻り値はobject?です。必要に応じてキャストします。

C#
string? name = property?.GetValue(user) as string;

Console.WriteLine(name);

複数のプロパティをまとめて表示することもできます。

C#
foreach (PropertyInfo prop in typeof(User).GetProperties())
{
object? value = prop.GetValue(user);
Console.WriteLine($"{prop.Name}: {value}");
}

5-2. プロパティの値を変更する

プロパティの値を変更するには、PropertyInfo.SetValueを使います。

C#
User user = new User();

PropertyInfo? property = typeof(User).GetProperty("Name");

property?.SetValue(user, "Bob");

Console.WriteLine(user.Name); // Bob

int型のプロパティに値を設定する例です。

C#
PropertyInfo? ageProperty = typeof(User).GetProperty("Age");

ageProperty?.SetValue(user, 25);

Console.WriteLine(user.Age); // 25

ただし、型が一致していない場合は例外が発生します。

C#
// ageProperty?.SetValue(user, "25"); // 型が一致しないため例外

文字列から数値に変換して設定したい場合は、Convert.ChangeTypeなどを使います。

C#
object convertedValue = Convert.ChangeType("25", typeof(int));

ageProperty?.SetValue(user, convertedValue);

5-3. フィールドの値を取得する

フィールドの値を取得するには、FieldInfo.GetValueを使います。

publicフィールドの場合は次のように取得できます。

C#
public class Sample
{
public int Count = 10;
}

Sample sample = new Sample();

FieldInfo? field = typeof(Sample).GetField("Count");

object? value = field?.GetValue(sample);

Console.WriteLine(value); // 10

privateフィールドを取得する場合は、BindingFlagsを指定します。

C#
User user = new User();

FieldInfo? field = typeof(User).GetField(
"password",
BindingFlags.NonPublic | BindingFlags.Instance
);

object? value = field?.GetValue(user);

Console.WriteLine(value); // secret

privateフィールドの取得は強力ですが、内部実装に依存します。フィールド名を変更しただけで動かなくなるため、通常の業務ロジックでは多用しない方が安全です。

5-4. フィールドの値を変更する

フィールドの値を変更するには、FieldInfo.SetValueを使います。

C#
Sample sample = new Sample();

FieldInfo? field = typeof(Sample).GetField("Count");

field?.SetValue(sample, 99);

Console.WriteLine(sample.Count); // 99

privateフィールドも同様に変更できます。

C#
User user = new User();

FieldInfo? field = typeof(User).GetField(
"password",
BindingFlags.NonPublic | BindingFlags.Instance
);

field?.SetValue(user, "new-secret");

Console.WriteLine(field?.GetValue(user)); // new-secret

ただし、privateフィールドの変更はクラスの不変条件を壊す可能性があります。実務で使う場合は、テストや移行処理など、目的を限定するべきです。

5-5. nullや型変換で注意すべきポイント

リフレクションで値を扱うときは、nullと型変換に注意が必要です。

GetPropertyGetFieldは、対象が見つからない場合にnullを返します。

C#
PropertyInfo? property = typeof(User).GetProperty("Unknown");

if (property == null)
{
Console.WriteLine("プロパティが見つかりません");
}

また、SetValueで設定する値の型が一致しないと例外になります。

C#
PropertyInfo? property = typeof(User).GetProperty("Age");

if (property != null)
{
object value = Convert.ChangeType("30", property.PropertyType);
property.SetValue(user, value);
}

Nullable型の場合は、変換先の型に注意が必要です。

C#
Type targetType = Nullable.GetUnderlyingType(property.PropertyType)
?? property.PropertyType;

object? convertedValue = value == null
? null
: Convert.ChangeType(value, targetType);

Nullable<int>の実体はint?ですが、変換時には中身のint型を取り出す必要があります。

6. リフレクションでメソッドを実行する方法

C#リフレクションでは、MethodInfoを取得してInvokeを呼び出すことで、メソッドを実行できます。

次のクラスを例にします。

C#
public class Calculator
{
public int Add(int x, int y)
{
return x + y;
}

public void Print(string message)
{
Console.WriteLine(message);
}

private string CreateMessage(string name)
{
return $"Hello, {name}";
}

public static int Square(int x)
{
return x * x;
}
}

6-1. MethodInfoを取得する

メソッド情報はGetMethodで取得します。

C#
Type type = typeof(Calculator);

MethodInfo? method = type.GetMethod("Add");

Console.WriteLine(method?.Name);

メソッドが存在しない場合はnullが返ります。

C#
if (method == null)
{
Console.WriteLine("メソッドが見つかりません");
}

オーバーロードされたメソッドがある場合は、引数の型を指定して取得する方が安全です。

C#
MethodInfo? method = typeof(Calculator).GetMethod(
"Add",
new[] { typeof(int), typeof(int) }
);

6-2. Invokeでメソッドを実行する

MethodInfo.Invokeを使うと、取得したメソッドを実行できます。

C#
Calculator calculator = new Calculator();

MethodInfo? method = typeof(Calculator).GetMethod("Print");

method?.Invoke(calculator, new object[] { "Hello Reflection" });

第1引数には対象インスタンスを指定します。第2引数にはメソッドに渡す引数を配列で指定します。

引数がないメソッドの場合は、nullまたは空配列を渡せます。

C#
method?.Invoke(calculator, null);

ただし、対象メソッドが引数を必要とする場合にnullを渡すと例外になります。

6-3. 引数ありメソッドを実行する

引数ありメソッドを実行する場合は、引数をobject[]で渡します。

C#
Calculator calculator = new Calculator();

MethodInfo? method = typeof(Calculator).GetMethod("Add");

object? result = method?.Invoke(calculator, new object[] { 10, 20 });

Console.WriteLine(result); // 30

引数の順番や型がメソッド定義と一致している必要があります。

C#
// method?.Invoke(calculator, new object[] { "10", "20" }); // 型不一致で例外

文字列から数値に変換して渡したい場合は、あらかじめ変換しておきます。

C#
object[] args =
{
Convert.ToInt32("10"),
Convert.ToInt32("20")
};

object? result = method?.Invoke(calculator, args);

6-4. 戻り値を取得する

Invokeの戻り値はobject?です。

C#
MethodInfo? method = typeof(Calculator).GetMethod("Add");

object? result = method?.Invoke(calculator, new object[] { 3, 5 });

int sum = (int)result!;

Console.WriteLine(sum); // 8

戻り値がないvoidメソッドの場合、Invokeの戻り値はnullになります。

C#
MethodInfo? method = typeof(Calculator).GetMethod("Print");

object? result = method?.Invoke(calculator, new object[] { "test" });

Console.WriteLine(result == null); // True

戻り値の型を確認したい場合は、ReturnTypeを使います。

C#
Console.WriteLine(method?.ReturnType.Name);

6-5. privateメソッドを実行する

privateメソッドを取得して実行するには、BindingFlags.NonPublicBindingFlags.Instanceを指定します。

C#
Calculator calculator = new Calculator();

MethodInfo? method = typeof(Calculator).GetMethod(
"CreateMessage",
BindingFlags.NonPublic | BindingFlags.Instance
);

object? result = method?.Invoke(calculator, new object[] { "Alice" });

Console.WriteLine(result); // Hello, Alice

privateメソッドの実行は可能ですが、通常のアプリケーションコードでは慎重に扱う必要があります。

privateメソッドは本来、クラス内部だけで使う前提の実装詳細です。リフレクションで外部から呼び出すと、設計意図が崩れたり、将来の変更に弱くなったりします。

6-6. 静的メソッドを実行する

staticメソッドを実行する場合、Invokeの第1引数にはnullを指定します。

C#
MethodInfo? method = typeof(Calculator).GetMethod(
"Square",
BindingFlags.Public | BindingFlags.Static
);

object? result = method?.Invoke(null, new object[] { 4 });

Console.WriteLine(result); // 16

staticメソッドはインスタンスに紐づかないため、対象オブジェクトを渡す必要がありません。

7. インスタンス生成とコンストラクターの実行

リフレクションでは、型情報からインスタンスを生成できます。

代表的な方法はActivator.CreateInstanceを使う方法です。また、ConstructorInfoを取得してコンストラクターを直接実行することもできます。

次のクラスを例にします。

C#
public class User
{
public string Name { get; }
public int Age { get; }

public User()
{
Name = "Unknown";
Age = 0;
}

public User(string name, int age)
{
Name = name;
Age = age;
}
}

7-1. Activator.CreateInstanceの基本

Activator.CreateInstanceを使うと、Typeからインスタンスを生成できます。

C#
Type type = typeof(User);

object? instance = Activator.CreateInstance(type);

Console.WriteLine(instance);

戻り値はobject?なので、必要に応じてキャストします。

C#
User user = (User)Activator.CreateInstance(typeof(User))!;

Console.WriteLine(user.Name);

型が実行時にしか分からない場合に便利です。

C#
Type type = Type.GetType("MyApp.User")!;

object? instance = Activator.CreateInstance(type);

7-2. 引数なしコンストラクターで生成する

引数なしコンストラクターがある場合は、次のように簡単に生成できます。

C#
User user = (User)Activator.CreateInstance(typeof(User))!;

Console.WriteLine(user.Name); // Unknown
Console.WriteLine(user.Age); // 0

ジェネリックを使う場合は、次のようにも書けます。

C#
T Create<T>() where T : new()
{
return new T();
}

ただし、実行時に型を指定したい場合は、Activator.CreateInstanceの方が柔軟です。

7-3. 引数ありコンストラクターで生成する

引数ありコンストラクターを呼び出す場合は、引数を渡します。

C#
object? instance = Activator.CreateInstance(
typeof(User),
"Alice",
30
);

User user = (User)instance!;

Console.WriteLine(user.Name); // Alice
Console.WriteLine(user.Age); // 30

引数の順番や型がコンストラクター定義と一致している必要があります。

C#
// Activator.CreateInstance(typeof(User), 30, "Alice"); // 順番が違うため失敗

文字列から数値に変換する必要がある場合は、事前に変換しておきます。

C#
object? instance = Activator.CreateInstance(
typeof(User),
"Alice",
Convert.ToInt32("30")
);

7-4. ConstructorInfoを使って生成する

ConstructorInfoを使って、特定のコンストラクターを取得して実行できます。

C#
ConstructorInfo? constructor = typeof(User).GetConstructor(
new[] { typeof(string), typeof(int) }
);

object? instance = constructor?.Invoke(new object[] { "Bob", 25 });

User user = (User)instance!;

Console.WriteLine(user.Name); // Bob
Console.WriteLine(user.Age); // 25

privateコンストラクターを取得したい場合は、BindingFlagsを使います。

C#
ConstructorInfo? constructor = typeof(User).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
binder: null,
types: Type.EmptyTypes,
modifiers: null
);

コンストラクターを明示的に選びたい場合は、ConstructorInfoを使うと分かりやすくなります。

7-5. インスタンス生成時の例外対策

リフレクションでインスタンスを生成するときは、次のような原因で例外が発生することがあります。

  • 型が見つからない

  • 引数なしコンストラクターがない

  • 引数の型が一致しない

  • 抽象クラスやインターフェースを生成しようとしている

  • コンストラクター内部で例外が発生している

安全に扱うには、事前チェックを行います。

C#
Type type = typeof(User);

if (type.IsAbstract || type.IsInterface)
{
throw new InvalidOperationException("インスタンス化できない型です");
}

object? instance = Activator.CreateInstance(type);

コンストラクターが存在するか確認することも重要です。

C#
ConstructorInfo? constructor = type.GetConstructor(Type.EmptyTypes);

if (constructor == null)
{
Console.WriteLine("引数なしコンストラクターがありません");
}

例外を握りつぶすのではなく、原因が分かるようにログを残すことも大切です。

8. 属性を扱うリフレクション

C#の属性は、クラス、プロパティ、メソッドなどに追加情報を付与する仕組みです。

リフレクションを使うと、実行時に属性を読み取り、その内容に応じて処理を変えることができます。

8-1. Attributeとは何か

属性は、コードにメタデータを付けるための仕組みです。

たとえば、標準属性には次のようなものがあります。

C#
[Obsolete("このメソッドは古いです")]
public void OldMethod()
{
}

Obsolete属性を付けると、そのメソッドが非推奨であることを表現できます。

属性は、フレームワークやライブラリでも多用されています。ASP.NET Coreのルーティング、データ注釈、テストフレームワークのテストメソッド指定なども属性を活用しています。

8-2. カスタム属性を定義する

独自の属性を作るには、Attributeクラスを継承します。

C#
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class DisplayNameAttribute : Attribute
{
public string Name { get; }

public DisplayNameAttribute(string name)
{
Name = name;
}
}

この属性をクラスやプロパティに付けます。

C#
[DisplayName("ユーザー")]
public class User
{
[DisplayName("名前")]
public string Name { get; set; } = "";

[DisplayName("年齢")]
public int Age { get; set; }
}

AttributeUsageを使うと、属性を付けられる対象を制限できます。

8-3. クラスやプロパティの属性を取得する

クラスに付いた属性を取得するには、GetCustomAttributeを使います。

C#
Type type = typeof(User);

DisplayNameAttribute? attribute =
type.GetCustomAttribute<DisplayNameAttribute>();

Console.WriteLine(attribute?.Name); // ユーザー

プロパティに付いた属性を取得する例です。

C#
foreach (PropertyInfo property in typeof(User).GetProperties())
{
DisplayNameAttribute? attr =
property.GetCustomAttribute<DisplayNameAttribute>();

Console.WriteLine($"{property.Name}: {attr?.Name}");
}

複数の属性を取得したい場合は、GetCustomAttributesを使います。

C#
var attributes = typeof(User).GetCustomAttributes();

foreach (var attr in attributes)
{
Console.WriteLine(attr.GetType().Name);
}

8-4. 属性を使った実装例

属性を使うと、表示名を自動で切り替えるような処理を実装できます。

C#
public static void PrintDisplayNames<T>()
{
foreach (PropertyInfo property in typeof(T).GetProperties())
{
DisplayNameAttribute? attr =
property.GetCustomAttribute<DisplayNameAttribute>();

string displayName = attr?.Name ?? property.Name;

Console.WriteLine(displayName);
}
}

PrintDisplayNames<User>();

属性が設定されていれば属性の値を使い、設定されていなければプロパティ名を使っています。

この考え方は、CSV出力、画面表示、バリデーション、権限制御などに応用できます。

8-5. 属性を使うと便利なケース

属性は、処理そのものではなく、処理に必要な追加情報をコードに付けたい場合に便利です。

たとえば、次のような用途があります。

  • 画面表示名を指定する

  • CSVやExcelの列名を指定する

  • バリデーションルールを指定する

  • APIのルーティング情報を指定する

  • 処理対象から除外するプロパティを指定する

  • テスト対象メソッドを指定する

属性とリフレクションを組み合わせると、クラス側に宣言的な設定を書き、共通処理側でその設定を読み取る設計ができます。

9. 実務でよく使うリフレクションの活用例

C#リフレクションは、業務アプリケーションでもフレームワークでも広く使われています。

ただし、何でもリフレクションで解決すればよいわけではありません。ここでは、実務でよくある活用例を紹介します。

9-1. オブジェクトのプロパティ一覧を表示する

デバッグやログ出力では、オブジェクトのプロパティ一覧を自動で表示したいことがあります。

C#
public static void DumpObject(object obj)
{
Type type = obj.GetType();

foreach (PropertyInfo property in type.GetProperties())
{
object? value = property.GetValue(obj);
Console.WriteLine($"{property.Name}: {value}");
}
}

使い方は次のとおりです。

C#
User user = new User
{
Name = "Alice",
Age = 30
};

DumpObject(user);

プロパティが増えても、DumpObject側を修正する必要がありません。

9-2. DTOやモデルの自動マッピング

同じ名前のプロパティを持つクラス同士で値をコピーする処理にもリフレクションを使えます。

C#
public static void Map<TSource, TDestination>(TSource source, TDestination destination)
{
var sourceProperties = typeof(TSource).GetProperties();
var destinationProperties = typeof(TDestination).GetProperties();

foreach (var sourceProperty in sourceProperties)
{
var destinationProperty = destinationProperties
.FirstOrDefault(p => p.Name == sourceProperty.Name &&
p.PropertyType == sourceProperty.PropertyType &&
p.CanWrite);

if (destinationProperty == null)
{
continue;
}

object? value = sourceProperty.GetValue(source);
destinationProperty.SetValue(destination, value);
}
}

ただし、実務ではAutoMapperのようなライブラリやSource Generatorを使う選択肢もあります。頻繁に実行される処理では、パフォーマンスも考慮する必要があります。

9-3. 設定ファイルやJSONとの連携

設定ファイルやJSONのキーとプロパティ名を対応させる処理でも、リフレクションは役立ちます。

C#
public static void SetPropertyValue(object target, string propertyName, object? value)
{
PropertyInfo? property = target.GetType().GetProperty(propertyName);

if (property == null || !property.CanWrite)
{
return;
}

Type targetType = Nullable.GetUnderlyingType(property.PropertyType)
?? property.PropertyType;

object? convertedValue = value == null
? null
: Convert.ChangeType(value, targetType);

property.SetValue(target, convertedValue);
}

このような仕組みにより、文字列で渡されたキー名をオブジェクトのプロパティに対応させられます。

ただし、JSON処理ではSystem.Text.JsonNewtonsoft.Jsonなどのライブラリを使うのが一般的です。自作する場合は、型変換、null、配列、ネストしたオブジェクトなどに注意が必要です。

9-4. プラグイン機能の実装

プラグイン機能では、アセンブリ内から特定のインターフェースを実装しているクラスを探し、動的にインスタンス化することがあります。

C#
public interface IPlugin
{
void Execute();
}

実装クラスです。

C#
public class SamplePlugin : IPlugin
{
public void Execute()
{
Console.WriteLine("プラグインを実行しました");
}
}

リフレクションで検索して実行します。

C#
var pluginTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t))
.Where(t => t.IsClass && !t.IsAbstract);

foreach (Type pluginType in pluginTypes)
{
IPlugin? plugin = Activator.CreateInstance(pluginType) as IPlugin;
plugin?.Execute();
}

この方法を使うと、新しいプラグインクラスを追加するだけで処理対象に含めることができます。

9-5. テストコードでの利用

テストコードでは、privateメソッドやprivateフィールドを確認したい場面があります。

C#
MethodInfo? method = typeof(Calculator).GetMethod(
"CreateMessage",
BindingFlags.NonPublic | BindingFlags.Instance
);

Calculator calculator = new Calculator();

object? result = method?.Invoke(calculator, new object[] { "Alice" });

Console.WriteLine(result);

ただし、privateメソッドを直接テストすることには賛否があります。多くの場合、publicメソッドの振る舞いを通してテストする方が設計として自然です。

privateメンバーへのリフレクションアクセスは、どうしても必要な場合に限定するのがよいでしょう。

9-6. フレームワーク内部での利用例

C#の多くのフレームワークは、内部でリフレクションを利用しています。

たとえば、次のような処理です。

  • DIコンテナがコンストラクターを調べて依存関係を注入する

  • ORMがクラスのプロパティとデータベース列を対応させる

  • JSONシリアライザーがプロパティを読み書きする

  • ASP.NET Coreがコントローラーや属性を読み取る

  • テストフレームワークがテストメソッドを探す

フレームワークを使う側ではリフレクションを直接意識しないことも多いですが、内部の仕組みを理解しておくと、エラー調査や拡張機能の実装に役立ちます。

10. リフレクション使用時の注意点

リフレクションは便利ですが、使い方を誤るとパフォーマンス、保守性、安全性に問題が出やすい機能です。

ここでは、C# reflectionを使う前に知っておきたい注意点を整理します。

10-1. パフォーマンスが低下しやすい理由

リフレクションは、通常のメソッド呼び出しやプロパティアクセスよりも遅くなりやすいです。

理由は、実行時にメタデータを検索したり、型チェックを行ったり、objectを介して値を扱ったりするためです。

C#
PropertyInfo? property = typeof(User).GetProperty("Name");
object? value = property?.GetValue(user);

この処理は、次の通常アクセスよりもコストが高くなります。

C#
string name = user.Name;

一度だけ実行する処理であれば問題になりにくいですが、大量データのループ内で何度も呼び出す場合は注意が必要です。

10-2. コンパイル時にエラーを検出しにくい

リフレクションでは、プロパティ名やメソッド名を文字列で指定することが多くなります。

C#
PropertyInfo? property = typeof(User).GetProperty("Name");

この"Name"という文字列は、コンパイラが安全性を保証してくれません。プロパティ名をFullNameに変更しても、文字列側は自動で更新されません。

その結果、実行時に初めてエラーが分かることがあります。

可能であれば、nameofを使うと安全性が少し上がります。

C#
PropertyInfo? property = typeof(User).GetProperty(nameof(User.Name));

nameofを使えば、プロパティ名の変更に追従しやすくなります。

10-3. privateメンバーへのアクセスは慎重に扱う

リフレクションを使うと、privateメンバーにもアクセスできます。

C#
FieldInfo? field = typeof(User).GetField(
"password",
BindingFlags.NonPublic | BindingFlags.Instance
);

しかし、privateメンバーはクラスの内部実装です。外部から直接操作すると、クラスが本来守るべき整合性を壊す可能性があります。

また、privateメンバー名は将来変更される可能性が高く、保守性も低くなります。

業務コードでは、privateメンバーに頼る設計ではなく、必要な操作をpublic APIとして用意する方が望ましいです。

10-4. セキュリティ上の注意点

外部入力をそのままリフレクションに渡す設計は危険です。

たとえば、ユーザーが入力したメソッド名をそのまま実行するような処理は避けるべきです。

C#
string methodName = inputFromUser;

MethodInfo? method = typeof(AdminService).GetMethod(methodName);
method?.Invoke(service, null);

このような実装では、意図しないメソッドが実行される可能性があります。

安全にするには、実行可能なメソッドをホワイトリストで制限します。

C#
var allowedMethods = new HashSet<string>
{
"Start",
"Stop"
};

if (!allowedMethods.Contains(methodName))
{
throw new InvalidOperationException("許可されていないメソッドです");
}

リフレクションは強力な機能なので、外部入力と組み合わせる場合は特に慎重に扱う必要があります。

10-5. 保守性を下げないための考え方

リフレクションは、コードの依存関係を見えにくくします。

通常のコードであれば、メソッド呼び出しを検索すれば利用箇所を追跡できます。しかし、リフレクションで文字列指定している場合、IDEの参照検索に引っかかりにくいことがあります。

保守性を下げないためには、次のような工夫が有効です。

C#
private static readonly string TargetPropertyName = nameof(User.Name);

また、リフレクション処理をアプリケーション全体に散らばらせず、共通クラスに閉じ込めることも重要です。

C#
public static class ReflectionHelper
{
public static object? GetPropertyValue(object target, string propertyName)
{
return target.GetType()
.GetProperty(propertyName)
?.GetValue(target);
}
}

必要な場所だけで使い、通常のビジネスロジックにはできるだけ持ち込まない設計が望ましいです。

11. リフレクションのパフォーマンス改善方法

リフレクションは通常のコードより遅くなりやすいため、頻繁に呼び出す処理では改善が必要です。

特に、大量のオブジェクトを変換するマッピング処理や、JSON・CSV処理などではパフォーマンス差が出やすくなります。

11-1. TypeやMethodInfoをキャッシュする

最も基本的な改善は、TypePropertyInfoMethodInfoなどを毎回取得せずにキャッシュすることです。

悪い例です。

C#
foreach (var user in users)
{
PropertyInfo? property = typeof(User).GetProperty("Name");
object? value = property?.GetValue(user);
}

毎回GetPropertyを呼ぶのは無駄です。

改善例です。

C#
PropertyInfo? property = typeof(User).GetProperty("Name");

foreach (var user in users)
{
object? value = property?.GetValue(user);
}

複数の型に対応する場合は、辞書にキャッシュできます。

C#
private static readonly Dictionary<Type, PropertyInfo[]> PropertyCache = new();

public static PropertyInfo[] GetCachedProperties(Type type)
{
if (!PropertyCache.TryGetValue(type, out var properties))
{
properties = type.GetProperties();
PropertyCache[type] = properties;
}

return properties;
}

マルチスレッド環境では、ConcurrentDictionaryを使うと安全です。

C#
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> PropertyCache = new();

public static PropertyInfo[] GetCachedProperties(Type type)
{
return PropertyCache.GetOrAdd(type, t => t.GetProperties());
}

11-2. 繰り返しInvokeを避ける

MethodInfo.Invokeは便利ですが、繰り返し実行すると負荷が大きくなります。

C#
MethodInfo? method = typeof(Calculator).GetMethod("Add");

for (int i = 0; i < 10000; i++)
{
method?.Invoke(calculator, new object[] { i, i });
}

このような処理は、通常のメソッド呼び出しに比べて遅くなります。

可能であれば、処理の初期化時だけリフレクションを使い、実行時は通常の呼び出しに近い形に変換します。

11-3. Delegate化して高速化する

MethodInfoからデリゲートを作成すると、Invokeより高速に呼び出せる場合があります。

C#
Calculator calculator = new Calculator();

MethodInfo? method = typeof(Calculator).GetMethod("Add");

var del = method?.CreateDelegate<Func<int, int, int>>(calculator);

int result = del!(10, 20);

Console.WriteLine(result); // 30

staticメソッドの場合は、インスタンスを渡さずにデリゲート化できます。

C#
MethodInfo? method = typeof(Calculator).GetMethod("Square");

var del = method?.CreateDelegate<Func<int, int>>();

int result = del!(5);

Console.WriteLine(result); // 25

頻繁に呼び出すメソッドは、MethodInfo.Invokeを毎回使うより、初回だけデリゲート化してキャッシュする方が効率的です。

11-4. Expression Treeとの使い分け

Expression Treeを使うと、プロパティアクセスやメソッド呼び出しを式として組み立て、コンパイルして実行できます。

C#
ParameterExpression parameter = Expression.Parameter(typeof(User), "x");
MemberExpression property = Expression.Property(parameter, "Name");

var lambda = Expression.Lambda<Func<User, string>>(property, parameter);
Func<User, string> getter = lambda.Compile();

string name = getter(new User { Name = "Alice" });

Expression Treeは、初回の式構築とコンパイルにコストがかかりますが、コンパイル後の実行は比較的高速です。

一方、単発の処理であれば、リフレクションの方が簡単です。

  • 単発処理:リフレクションで十分

  • 大量ループ:キャッシュやDelegate化を検討

  • 動的な式を高速実行したい:Expression Treeを検討

このように使い分けるとよいでしょう。

11-5. 必要な場面だけで使う設計にする

最も重要なのは、リフレクションを必要な場面だけで使うことです。

通常のメソッド呼び出しで済む処理に、あえてリフレクションを使う必要はありません。

C#
// 通常はこちらで十分
user.Name = "Alice";

リフレクションが向いているのは、型やメンバーが実行時まで分からない場合、または共通処理として多くの型を扱う必要がある場合です。

設計段階で、次のように考えると判断しやすくなります。

  • コンパイル時に型が分かるなら通常コードを使う

  • 共通化のために型情報が必要ならリフレクションを使う

  • 高速性が必要ならキャッシュやDelegate化を行う

  • さらに高速化したいならSource Generatorも検討する

12. リフレクションでよくあるエラーと対処法

C#リフレクションでは、通常のコードよりも実行時エラーが発生しやすくなります。

ここでは、よくあるエラーと対処法を紹介します。

12-1. Typeが取得できない

Type.GetTypeで型を取得しようとして、nullになることがあります。

C#
Type? type = Type.GetType("User");

Console.WriteLine(type == null); // Trueになることがある

Type.GetTypeでは、名前空間を含む完全修飾名が必要になることがあります。

C#
Type? type = Type.GetType("MyApp.Models.User");

別アセンブリの型を取得する場合は、アセンブリ修飾名が必要になる場合もあります。

C#
Type? type = Type.GetType("MyApp.Models.User, MyApp");

現在のアセンブリ内から探すなら、Assembly.GetExecutingAssembly().GetTypeを使う方法もあります。

C#
Type? type = Assembly.GetExecutingAssembly()
.GetType("MyApp.Models.User");

12-2. メソッドが見つからない

GetMethodnullを返す原因には、次のようなものがあります。

  • メソッド名が間違っている

  • privateメソッドなのにBindingFlags.NonPublicを指定していない

  • staticメソッドなのにBindingFlags.Staticを指定していない

  • オーバーロードがあり、引数型の指定が必要

  • 継承元のメソッドと混同している

privateメソッドの場合は、次のように指定します。

C#
MethodInfo? method = typeof(Calculator).GetMethod(
"CreateMessage",
BindingFlags.NonPublic | BindingFlags.Instance
);

オーバーロードがある場合は、引数型を指定します。

C#
MethodInfo? method = typeof(Calculator).GetMethod(
"Add",
new[] { typeof(int), typeof(int) }
);

メソッド名にはnameofを使うと、タイプミスを減らせます。

C#
MethodInfo? method = typeof(Calculator).GetMethod(nameof(Calculator.Add));

12-3. TargetInvocationExceptionが発生する

MethodInfo.Invokeで呼び出したメソッドの内部で例外が発生すると、TargetInvocationExceptionでラップされることがあります。

C#
try
{
method?.Invoke(target, args);
}
catch (TargetInvocationException ex)
{
Console.WriteLine(ex.InnerException?.Message);
}

重要なのは、実際の原因はInnerExceptionに入っていることが多い点です。

C#
catch (TargetInvocationException ex) when (ex.InnerException != null)
{
throw ex.InnerException;
}

ログを出すときも、TargetInvocationExceptionそのものだけでなく、InnerExceptionを確認しましょう。

12-4. 引数の型が一致しない

InvokeSetValueでは、渡す値の型が一致していないと例外が発生します。

C#
MethodInfo? method = typeof(Calculator).GetMethod("Add");

// method?.Invoke(calculator, new object[] { "1", "2" }); // 型不一致

メソッドの引数情報はGetParametersで取得できます。

C#
ParameterInfo[] parameters = method!.GetParameters();

foreach (ParameterInfo parameter in parameters)
{
Console.WriteLine($"{parameter.Name}: {parameter.ParameterType}");
}

必要に応じて型変換します。

C#
object converted = Convert.ChangeType("123", typeof(int));

ただし、Convert.ChangeTypeで変換できない型もあります。enum、Guid、DateTime、Nullableなどは個別対応が必要になることがあります。

12-5. アクセス権限で失敗する

privateメンバーを取得しようとして失敗する場合、BindingFlagsの指定不足が原因であることが多いです。

C#
FieldInfo? field = typeof(User).GetField(
"password",
BindingFlags.NonPublic | BindingFlags.Instance
);

staticメンバーならBindingFlags.Staticが必要です。

C#
MethodInfo? method = typeof(MyClass).GetMethod(
"StaticMethod",
BindingFlags.NonPublic | BindingFlags.Static
);

また、実行環境や制限によっては、非publicメンバーへのアクセスが想定どおりに動かない場合もあります。特にライブラリ、AOT、トリミング、セキュリティ制限のある環境では注意が必要です。

12-6. Nullableやジェネリック型でつまずく

Nullable型を扱う場合、PropertyTypeNullable<T>になります。

C#
public int? Age { get; set; }

中身の型を取り出すには、Nullable.GetUnderlyingTypeを使います。

C#
Type propertyType = typeof(User).GetProperty("Age")!.PropertyType;

Type actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;

Console.WriteLine(actualType); // System.Int32

ジェネリック型かどうかはIsGenericTypeで判定できます。

C#
Type type = typeof(List<string>);

Console.WriteLine(type.IsGenericType); // True

型引数はGetGenericArgumentsで取得します。

C#
Type[] arguments = type.GetGenericArguments();

foreach (Type arg in arguments)
{
Console.WriteLine(arg.Name); // String
}

Nullableやジェネリック型を扱う共通処理では、単純な型比較だけでは対応できないことがあります。

13. リフレクションと代替手段の使い分け

リフレクションは便利ですが、常に最適とは限りません。

C#には、generics、interface、dynamic、Expression Tree、Source Generatorなど、リフレクションの代わりに使える仕組みがあります。

13-1. genericsで解決できるケース

型がコンパイル時に決まっている、または型パラメーターとして扱える場合は、genericsを使う方が安全です。

C#
public T Create<T>() where T : new()
{
return new T();
}

この場合、リフレクションを使ってActivator.CreateInstanceを呼ぶよりも、型安全で分かりやすくなります。

C#
User user = Create<User>();

一方、型が文字列や外部ファイルから決まる場合は、genericsだけでは対応しにくいため、リフレクションが候補になります。

13-2. interfaceで解決できるケース

複数のクラスに共通処理を行いたい場合は、まずinterfaceを検討します。

C#
public interface IExecutable
{
void Execute();
}

実装クラスです。

C#
public class Job : IExecutable
{
public void Execute()
{
Console.WriteLine("実行しました");
}
}

呼び出し側です。

C#
void Run(IExecutable executable)
{
executable.Execute();
}

このようにinterfaceで解決できるなら、リフレクションよりも安全で高速です。

リフレクションは、interfaceの実装クラスをアセンブリから自動検出するような場面で使うと効果的です。

13-3. dynamicとの違い

dynamicを使うと、コンパイル時ではなく実行時にメンバー解決が行われます。

C#
dynamic user = new User();
user.Name = "Alice";
Console.WriteLine(user.Name);

dynamicはコードが短く見えますが、存在しないメンバーにアクセスしてもコンパイル時にはエラーになりません。

リフレクションとの違いは、リフレクションは型情報を明示的に取得して操作するのに対し、dynamicは通常のメンバーアクセスに近い書き方で実行時解決する点です。

C#
PropertyInfo? property = typeof(User).GetProperty("Name");
property?.SetValue(user, "Alice");

メンバー一覧を取得したい、属性を読みたい、privateメンバーを扱いたいといった用途では、リフレクションの方が適しています。

13-4. Expression Treeとの違い

Expression Treeは、コードを式ツリーとして表現し、動的に組み立てたりコンパイルしたりできる仕組みです。

リフレクションは、型情報やメンバー情報を調べるために使います。一方、Expression Treeは、動的な処理を効率よく実行するために使われることが多いです。

たとえば、プロパティのgetterを動的に作る場合です。

C#
ParameterExpression parameter = Expression.Parameter(typeof(User), "u");
MemberExpression property = Expression.Property(parameter, "Name");

var lambda = Expression.Lambda<Func<User, string>>(property, parameter);

Func<User, string> getter = lambda.Compile();

Expression Treeは強力ですが、コードが複雑になりやすいです。単純に型情報を取得するだけなら、リフレクションで十分です。

13-5. Source Generatorとの違い

Source Generatorは、コンパイル時にコードを自動生成する仕組みです。

リフレクションは実行時に型情報を調べますが、Source Generatorはコンパイル時に必要なコードを生成できます。

たとえば、プロパティのマッピング処理をリフレクションで毎回実行する代わりに、Source Generatorで専用のマッピングコードを生成すれば、実行時のコストを抑えられます。

リフレクションは柔軟ですが、実行時コストがあります。Source Generatorは準備が必要ですが、実行時には通常のC#コードとして動作するため高速です。

大量データを扱う処理や、AOT・トリミングを意識する環境では、Source Generatorが有力な選択肢になります。

13-6. リフレクションを使うべきか判断する基準

リフレクションを使うかどうかは、次の基準で判断するとよいでしょう。

型やメンバーがコンパイル時に分かっているなら、通常のコード、generics、interfaceを優先します。

C#
user.Name = "Alice";
user.SayHello();

実行時に型やメンバーを動的に扱う必要があるなら、リフレクションを検討します。

C#
PropertyInfo? property = user.GetType().GetProperty(propertyName);
property?.SetValue(user, value);

大量に繰り返す処理なら、キャッシュ、Delegate化、Expression Tree、Source Generatorを検討します。

つまり、リフレクションは「通常のコードでは表現しにくい動的な処理」に限定して使うのが基本です。

まとめ

C#リフレクションは、実行時に型情報を取得し、プロパティ、フィールド、メソッド、コンストラクター、属性などを動的に扱える強力な機能です。

基本的な流れは、まずtypeofGetTypeTypeを取得し、そこからGetPropertiesGetFieldsGetMethodsGetConstructorsなどでメンバー情報を取得します。

プロパティやフィールドの値は、GetValueSetValueで読み書きできます。メソッドはMethodInfo.Invokeで実行できます。インスタンス生成にはActivator.CreateInstanceConstructorInfo.Invokeを使います。

また、属性と組み合わせることで、表示名、バリデーション、ルーティング、マッピング、プラグイン探索など、柔軟な設計が可能になります。

一方で、リフレクションには注意点もあります。通常のコードより遅くなりやすく、文字列指定が多いためコンパイル時にエラーを検出しにくくなります。privateメンバーへのアクセスは保守性や安全性を下げる可能性があるため、慎重に扱う必要があります。

実務では、まず通常のコード、generics、interfaceで解決できないかを考え、それでも実行時の動的な処理が必要な場合にC# reflectionを使うのがよい判断です。

リフレクションを適切に使えるようになると、フレームワークの仕組みを理解しやすくなり、共通処理、動的マッピング、属性ベースの設計、プラグイン機能などをより柔軟に実装できるようになります。