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