【C#】何となく分かった気になれる!リフレクションとは?

この記事は、 KENTEM TechBlog アドベントカレンダー2024 4日目、12月4日の記事です。

C#erの皆さん「リフレクション」って分かりますか?
分かるようで分からない、ちょっと難解で敬遠しがちな技術だと思います。
というか使ったことないや、という人のためにも初心者向けに簡単な形で説明していきます!

リフレクションとは?

Microsoftによると下記のような説明になるようです。

リフレクションは、アセンブリ、モジュール、および型を記述するオブジェクト (型Type ) を提供します。リフレクションを使用すると、型のインスタンスを動的に作成したり、型を既存のオブジェクトにバインドしたり、既存のオブジェクトから型を取得してそのメソッドを呼び出したり、そのフィールドやプロパティにアクセスしたりできます。コードで属性を使用している場合は、リフレクションを使用して属性にアクセスできます。

ちょっと説明が小難しいので、一つずつコードでどんなものなのか示していきます!

アセンブリ、モジュール、型の取得

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        // アセンブリを取得
        Assembly assembly = Assembly.GetExecutingAssembly();
        Console.WriteLine($"Assembly: {assembly.FullName}");

        // モジュールを取得
        Module[] modules = assembly.GetModules();
        foreach (var module in modules)
        {
            Console.WriteLine($"Module: {module.Name}");
        }

        // 型情報を取得
        Type type = typeof(SampleClass);
        Console.WriteLine($"Type: {type.FullName}");
    }
}

class SampleClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}
  • 出力結果
Assembly: ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Module: ConsoleApp1.dll
Type: ConsoleApp1.SampleClass

動的に型のインスタンスを作成、メソッドを呼ぶ

using System;

class Program
{
    static void Main()
    {
        // 型情報を取得
        Type type = typeof(SampleClass);

        // 動的に型のインスタンスを作成
        object? instance = Activator.CreateInstance(type);

        // メソッド情報を取得
        MethodInfo? method = type.GetMethod("SayHello");

        // メソッドを呼び出し(インスタンスとパラメータを渡す)
        method?.Invoke(instance, new object[] { "KENTEM" });
    }
}

class SampleClass
{
    public void SayHello(string name)
        => Console.WriteLine($"Hello, {name}!");
}
  • 出力結果
Hello, KENTEM!

フィールドやプロパティにアクセス

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        SampleClass obj = new SampleClass();

        Type type = typeof(SampleClass);

        // プロパティにアクセス
        PropertyInfo? property = type.GetProperty("PublicProperty");
        property?.SetValue(obj, "よろしく");
        Console.WriteLine($"Public Property: {property?.GetValue(obj)}");

        // privateフィールドにもアクセスできます
        FieldInfo? privateField = type.GetField("_privateField", BindingFlags.NonPublic | BindingFlags.Instance);
        privateField?.SetValue(obj, 4649);
        Console.WriteLine($"Private Field: {privateField?.GetValue(obj)}");

        // staticフィールドにアクセス
        FieldInfo? staticField = type.GetField("_staticField", BindingFlags.NonPublic | BindingFlags.Static);
        staticField?.SetValue(obj, true);
        Console.WriteLine($"Static Field: {staticField?.GetValue(null)}");

        // 条件に合致するものをまとめて取得もできる
        foreach (var info in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
            Console.WriteLine($"Type: {info.PropertyType} Name: {info.Name}");
    }
}

class SampleClass
{
    private int _privateField;
    private static bool _staticField = false;
    public string? PublicProperty { get; set; }
    protected object? ProtectedProperty { get; set; }
    private double PrivateProperty { get; set; }
}
  • 出力結果
Public Property: よろしく
Private Field: 4649
Static Field: True
Type: System.String Name: PublicProperty
Type: System.Object Name: ProtectedProperty
Type: System.Double Name: PrivateProperty

属性にアクセス

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Runtime.CompilerServices;

class Program
{
    static void Main()
    {
        Type type = typeof(SampleClass);

        //属性の型を指定して取得
        var classAttribute = type.GetCustomAttribute<DescriptionAttribute>();
        Console.WriteLine($"Class Attribute: {classAttribute?.Description}");

        // プロパティの属性を取得
        foreach (var propertyInfo in type.GetProperties())
        {
            foreach (var attr in propertyInfo.CustomAttributes)
            {
                Console.WriteLine($"Property: {propertyInfo.Name}\t Attribute: {attr.AttributeType.Name}");
            }
        }

        // メソッドの属性を取得
        foreach (var methodInfo in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                                       .Where(q => !q.IsSpecialName))
        {
            foreach (var attr in methodInfo.CustomAttributes)
            {
                Console.WriteLine($"Method: {methodInfo.Name}\t Attribute: {attr.AttributeType.Name}");
            }
        }
    }
}

[Description("サンプルクラスです")]
class SampleClass
{
    [Required]
    [StringLength(100)]
    public string? Name { get; set; }

    [Range(1, 100)]
    public int Quantity { get; set; }

    [Obsolete("使わないでね")]
    public void OldMethod() { }

    //特に属性を付けていないけど、asyncにするだけで属性が自動的に付与される
    public async Task NewMethod() { await Task.Delay(1); }
}
  • 出力結果
Class Attribute: サンプルクラスです
Property: Name      Attribute: RequiredAttribute
Property: Name      Attribute: StringLengthAttribute
Property: Quantity  Attribute: RangeAttribute
Method: OldMethod        Attribute: ObsoleteAttribute
Method: NewMethod        Attribute: AsyncStateMachineAttribute
Method: NewMethod        Attribute: DebuggerStepThroughAttribute


リフレクションの使い道って?

プラグインや動的モジュールのロード

外部から提供されているモジュールやプラグインを動的にロードして扱うことができます。

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        // 外部アセンブリ (MyPlugin.dll) をロード
        Assembly pluginAssembly = Assembly.LoadFrom("MyPlugin.dll");

        // プラグインのメインクラスを取得
        Type pluginType = pluginAssembly.GetType("MyPlugin.Main")!;

        // インスタンスを生成してメソッドを呼び出す
        object? pluginInstance = Activator.CreateInstance(pluginType);
        MethodInfo? executeMethod = pluginType.GetMethod("Execute");
        executeMethod?.Invoke(pluginInstance, null);
    }
}

デバッグやテストフレームワークの実装

テストフレームワークでよくあるような特定のAttributeのみをテストする、というような使い方もできます。
あと通常ではアクセスできないprivateなメソッドやプロパティにもアクセスできるため、テストだけなら結構無理やりできます!

using System;
using System.Reflection;
using System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Method)]
class TestMethodAttribute : Attribute { }

class TestSuite
{
    [TestMethod]
    public void TestAddition() => HelperMethod();

    [TestMethod]
    public void TestSubtraction() => HelperMethod();

    // TestMethodがついてないのでテスト対象外
    private void HelperMethod([CallerMemberName] string member = "")
        => Console.WriteLine($"CallerMethod: {member}");
}

class Program
{
    static void Main()
    {
        var testSuite = new TestSuite();
        Type type = testSuite.GetType();

        foreach (var method in type.GetMethods())
        {
            // TestMethodだけを実行
            if (method.GetCustomAttribute<TestMethodAttribute>() != null)
            {
                method.Invoke(testSuite, null);
            }
        }

        // 通常では触れないはずのprivateのものもテストできたりもする!
        type.GetMethod("HelperMethod", BindingFlags.NonPublic | BindingFlags.Instance)
            ?.Invoke(testSuite, [MethodBase.GetCurrentMethod()?.Name]);
    }
}
  • 出力結果
CallerMethod: TestAddition
CallerMethod: TestSubtraction
CallerMethod: Main


リフレクションの注意点

上記のような説明だとリフレクションは便利なものだと思いますが、使いどころには注意が必要です。

セキュリティ上のリスク

リフレクションはprivateだろうが何だろうが型情報さえ分かってしまえばいくらでもアクセスが可能です。
privateは触れられたくないから隠しているのであって、何でもできてしまうとなると実装方法によりセキュリティ上の脅威となり得る場合もあります。
このような使い方は基本的にテストやデバッグでの使用に留めるのが良いかと思います。

パフォーマンス上の問題

実行時に動的に型情報を解析して実行しているため、どうしてもパフォーマンスが低下してしまう可能性があります。
ループの中でリフレクションなどを多用していると異常に遅くなってしまったりもしますので注意しましょう。


まとめ

リフレクションが何となく分かった気になったでしょうか?
リフレクションは使いどころを誤らなければ、動的なプログラムの実装や柔軟性がある実装ができるため有用な技術です。
問題点もきちんと理解しながら今後もリフレクションと付き合ってみてください!

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp