プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

リフレクション Q&A

Dynamic

「Hokuriku.NET C# メタプログラミング ~リフレクション~」に参加してきた。

Hokuriku.NET C# メタプログラミング ~リフレクション~
日時 2013年6月29日
会場 海みらい図書館 (石川県金沢市)
関連記事

その中で話題になったことから、何点かQ&A形式でご紹介したい。

■ Q. ジェネリックの型情報って取れるの?

A. 取れる。

例えば System.Collections.Generic.Dictionary の型情報を取ってみよう。

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Dictionary<,> の型情報を取得
        Type typeOfDictionary = typeof(Dictionary<,>);
        Console.WriteLine(typeOfDictionary);
    }
}

実行結果は、次のようになる。

System.Collections.Generic.Dictionary`2[TKey,TValue]

この型情報から、型引数を指定した場合の型情報を得るには、次のように MakeGenericType を使う。

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Dictionary<,> の型情報を取得
        Type typeOfDictionary               = typeof(Dictionary<,>);

        // typeOfDictionary に型引数を指定して型情報を取得
        Type typeOfDictionaryOfStringAndInt = typeOfDictionary.MakeGenericType(new Type { typeof(string), typeof(int) });
        Console.WriteLine(typeOfDictionaryOfStringAndInt);
    }
}

実行結果:

System.Collections.Generic.Dictionary`2[System.String,System.Int32]

勿論、始めから型引数を指定した場合の型情報と同じになる。

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Dictionary<string, int> の型情報を取得
        Type typeOfDictionaryOfStringAndInt = typeof(Dictionary<string, int>);
        Console.WriteLine(typeOfDictionaryOfStringAndInt);
    }
}

実行結果:

System.Collections.Generic.Dictionary`2[System.String,System.Int32]

■ Q. プロパティって内部的にはメソッドなの?

A. その通り。プロパティでもある。

リフレクションを使って、プロパティを持ったクラスのメンバーを調べてみよう。

using System;
using System.Reflection;

// プロパティ Name を持つクラス Person (Object クラスから派生)
class Person // : Object
{
    public string Name { get; set; }
}

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

        // Person の型情報から、Person の全メンバーの情報を取得
        MemberInfo memberInfos = type.GetMembers();

        // 各メンバー情報の名前と種類を表示
        foreach (MemberInfo member in memberInfos)
            Console.WriteLine("Name: {0}, MemberType: {1}", member.Name, member.MemberType);
    }
}

Type.GetMembers メソッドを使い、全メンバーの情報を取得し、各メンバー情報の名前と種類を表示させてみる。

実行してみると、次のように Person とそのベースクラスである Object の、public なメンバーがリストアップされる:

Name: get_Name, MemberType: Method
Name: set_Name, MemberType: Method
Name: ToString, MemberType: Method
Name: Equals, MemberType: Method
Name: GetHashCode, MemberType: Method
Name: GetType, MemberType: Method
Name: .ctor, MemberType: Constructor
Name: Name, MemberType: Property

プロパティとして Name、そのアクセサー メソッドとして get_Name と set_Name があるのが判る。

次のように、プロパティ Name から、アクセサー メソッドを取り出してみることもできる。

using System;
using System.Reflection;

// プロパティ Name を持つクラス Person
class Person
{
    public string Name { get; set; }
}

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

        // Person の型情報から、プロパティ Name の情報を取得
        PropertyInfo propertyInfo = type.GetProperty("Name");
        Console.WriteLine(propertyInfo);

        // プロパティ Name の情報から、アクセサー メソッド (getter と setter) の情報を取得
        MethodInfo methodInfos  = propertyInfo.GetAccessors();

        // アクセサー メソッド (getter と setter) の情報を表示
        foreach (MethodInfo methodInfo in methodInfos)
            Console.WriteLine(methodInfo);
    }
}

実行結果:

System.String Name
System.String get_Name()
Void set_Name(System.String)

つまり、プロパティ Name があるときには、内部的にアクセサーとして get_Name と set_Name メソッドを持つことになる。

これは、単に get_Name と set_Name メソッドを持つのとは異なる。Name というプロパティも持つことになる。

class Person
{
    public string Name { get; set; } // プロパティ Name (アクセサーとして get_Name と set_Name メソッドも持つことになる)
}

と異なり、

class Person
{
    public string get_Name(           ) { return null; } 
    public void   set_Name(string name) {              }
}

では、get_Name と set_Name メソッドを持つことは同じだが、Name プロパティを持たない。

■ Q. Name プロパティがあるときに内部的にアクセサーとして get_Name と set_Name メソッドを持つのだとすると、Name プロパティがあるときは、get_Name という名前や set_Name という名前のメソッドは作れない?

A. そう、作れない。コンパイル エラーになる。

次のコードをコンパイルしてみると、コンパイル エラーになる。

class Person
{
    public string Name { get; set; } // プロパティ Name

    public string get_Name(           ) { return null; } 
    public void   set_Name(string name) {              }
}

コンパイル結果:

  • エラー 1 型 'Person' は、'get_Name' と呼ばれるメンバーを同じパラメーターの型で既に予約しています。
  • エラー 2 型 'Person' は、'set_Name' と呼ばれるメンバーを同じパラメーターの型で既に予約しています。

■ Q. プロパティが内部的にメソッドだとすると、リフレクションでその内部的なメソッドを実行してもプロパティの値の設定/取得ができる?

A. できる。

先ずは、リフレクションを使って、プロパティに値を設定/取得してみる。

using System;
using System.Reflection;

// プロパティ Name を持つクラス Person
class Person
{
    public string Name { get; set; }
}

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

        var          person       = new Person();
        PropertyInfo propertyInfo = type.GetProperty("Name");

        // リフレクションを使って、person の Name の値を設定
        propertyInfo.SetValue(person, "明智光秀", null); // .NET 4.5 未満の場合
        // .NET 4.5 以上の場合は propertyInfo.SetValue(person, "明智光秀"); でも良い

        // リフレクションを使って、person の Name の値を取得
        string       name         = (string)propertyInfo.GetValue(person, null); // .NET 4.5 未満の場合
        // .NET 4.5 以上の場合は string name = (string)propertyInfo.GetValue(person); でも良い

        Console.WriteLine(name);
    }
}

実行結果:

次に、リフレクションを使い、内部的な set_Name メソッドで値を設定し、Name プロパティで値を取得してみる。

using System;
using System.Reflection;

// プロパティ Name を持つクラス Person
class Person
{
    public string Name { get; set; }
}

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

        var          person       = new Person();

        // リフレクションを使って、person の set_Name を呼び出す
        MethodInfo   methodInfo   = type.GetMethod("set_Name");
        methodInfo.Invoke(person, new object { "織田信長" });

        // リフレクションを使って、person の Name の値を取得
        PropertyInfo propertyInfo = type.GetProperty("Name");
        string       name         = (string)propertyInfo.GetValue(person, null); // .NET 4.5 未満の場合
        // .NET 4.5 以上の場合は string name = (string)propertyInfo.GetValue(person); でも良い

        Console.WriteLine(name);
    }
}

実行結果:

織田信長

このように、プロパティの内部的なメソッドをリフレクションを使って実行することで、プロパティの値の設定/取得ができる。

勿論、リフレクションを使わずに、直接 set_Name や get_Name を呼び出すことはできない。

using System;
using System.Reflection;

// プロパティ Name を持つクラス Person
class Person
{
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        var person = new Person();
        person.set_Name("石田光成"); // コンパイル エラーになる
    }
}

コンパイル結果:

  • エラー 1 'Person.Name.set': 演算子またはアクセサーを明示的に呼び出すことはできません。

■ Q. リフレクションで static なメンバーや private なメンバーにもアクセスできる?

A. できる。

次の static メンバーや private メンバーを持つクラス Singleton で、試してみよう。

using System;
using System.Reflection;

// static メンバーや private メンバーを持つクラス
class Singleton
{
    private static Singleton instance = new Singleton();

    public static Singleton GetInstance()
    { return instance; }

    private Singleton()
    {}
}

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

        // BindingFlags.NonPublic | BindingFlags.Instance を指定することで、public でも static でもないコンストラクターの情報を取得
        ConstructorInfo constructorInfo = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null);

        // リフレクションを使うことで、private なコンストラクターが実行できる
        Singleton       singleton       = (Singleton)constructorInfo.Invoke(null);

        // BindingFlags.NonPublic | BindingFlags.Static を指定することで、public でなく static なフィールドの情報を取得
        FieldInfo       fieldInfo       = type.GetField("instance", BindingFlags.NonPublic | BindingFlags.Static);

        // リフレクションを使うことで、private で static なフィールドにアクセスできる
        fieldInfo.SetValue(null, singleton);
    }
}

■ Q. const なフィールドって static なフィールドとしてリフレクションでアクセスできる? まさかと思うけど値を書き換えられる?

A. const なフィールドは static なフィールドとしてリフレクションで値を取得できるが、書き換えられる訳ではない。

試してみよう:

using System;
using System.Reflection;

class ConstantSample
{
    public const double PI = 3.14159265358979 ; // const なフィールド
}

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

        // Constant の型情報から、const なフィールド PI の情報を取得
        FieldInfo piFieldInfo = type.GetField("PI");

        // インスタンスを指定せずに static フィールドとして値を取得してみる
        var       pi          = (double)piFieldInfo.GetValue(null);
        Console.WriteLine(pi);

        // インスタンスを指定せずに static フィールドとして値を設定してみる
        piFieldInfo.SetValue(null, 3.0);
    }
}

実行してみると、次のように、PI の値を取得することはできるが、設定ではエラーになる:

3.14159265358979

ハンドルされていない例外: System.FieldAccessException: 定数フィールドを設定できません。

■ Q. じゃあ readonly なフィールドは?

A. readonly なフィールドの場合は、const なフィールドの場合と異なり、static としてなければインスタンス フィールドになる。また、値を取得するだけでなく設定することもできる。

readonly なフィールドの場合について試してみよう。

using System;
using System.Reflection;

class ReadonlySample
{
    public readonly int DayOfYear = DateTime.Now.DayOfYear; // readonly なフィールド
}

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

        // Readonly の型情報から、readonly なフィールド DayOfYear の情報を取得
        FieldInfo dayOfYearFieldInfo = type.GetField("DayOfYear");

        // インスタンスを指定せずに static フィールドとして値を取得してみると、これは実行時エラーとなり取得できない
        //var     dayOfYear          = (int)dayOfYearFieldInfo.GetValue(null); // 実行時エラー

        // Readonly のインスタンスを生成
        var       readonlySample     = new ReadonlySample();
        // インスタンスを指定してインスタンス フィールドとして値を取得してみる
        var       dayOfYear          = (int)dayOfYearFieldInfo.GetValue(readonlySample);
        Console.WriteLine(dayOfYear);

        // インスタンスを指定してインスタンス フィールドとして値を設定してみる
        dayOfYearFieldInfo.SetValue(readonlySample, 0);
        // 設定されたか確認
        Console.WriteLine(readonlySample.DayOfYear); 
    }
}

実行結果は次の通り:

182
0

取得だけでなく、設定も可能だ。