こんにちは、エンジニアの清水です。

現在の業務では、開発言語として主にC#を使用しています。
少々込み入ったことをする際に、クラスのメタデータにアクセスできる機能「リフレクション」を使うと便利なケースがあります。

今回はC#でのリフレクションの使用例、使いどころについてご紹介したいと思います。
※本稿では、リフレクションについての詳細な解説は割愛させていただきます。

使用例1

Simple FactoryパターンやAbstract Factoryパターンにて、
条件に応じて生成するクラスを振り分ける場合、通常は生成対象のクラスが増えるごとに
Factoryクラスの条件分岐も増やす必要があります。

例えば、Simple Factoryパターンのサンプルコードは以下のようになります。

using System;

// 動物インターフェイス
public interface IAnimal
{
    // 鳴きます。
    string Cry();
}

// 犬クラス
public class Dog : IAnimal
{
    public string Cry() { return "わんわん!"; }
}

// 猫クラス
public class Cat : IAnimal
{
    public string Cry() { return "にゃ〜ん"; }
}

// 動物インスタンスを生成するFactoryクラス
public class AnimalFactory
{
    public IAnimal CreateAnimal(string className)
    {
        // 引数に応じて生成するクラスを判断。対象クラスが増えるとif文も増える!
        if(className == "Dog")
        {
            return new Dog();
        }
        else if (className == "Cat")
        {
            return new Cat();
        }
        else
        {
            throw new ArgumentException();
        }
    }
}

class MainClass
{
    public static void Main()
    {
        var _animalFactory = new AnimalFactory();
        Console.WriteLine(_animalFactory.CreateAnimal("Cat").Cry());
    }
}

上記例のように渡される文字列とクラス名が同一、
もしくは規則性がある場合には、例えばFactoryクラスを以下のように書き換えることで
クラスの種類が増えた場合に条件分岐を増やす必要がなくなります。

// 動物インスタンスを生成するFactoryクラス
public class AnimalFactoryMod
{
    public IAnimal CreateAnimal(string className)
    {
        // 引数文字列からクラスを生成
        Type type = Type.GetType(className);
        if (type == null)
        {
            throw new ArgumentException();
        }
        return (IAnimal)Activator.CreateInstance(type);
    }
}

当然ながら、出力結果は

にゃ〜ん

となります。

使用例2

実行中のクラス名やメソッド名を、ログに出力したい場合

public class MyClass1
{
    public void MyMethod1() 
    {
        Console.WriteLine("クラス名:MyClass1, メソッド名:MyMethod1");

        // 何か処理...
    }
}

public class MyClass2
{
    public void MyMethod2()
    {
        Console.WriteLine("クラス名:MyClass2, メソッド名:MyMethod2");

        // 何か処理...
    }
}

class MainClass
{
    public static void Main()
    {
        var _myClass1 = new MyClass1();
        _myClass1.MyMethod1();
        var _myClass2 = new MyClass2();
        _myClass2.MyMethod2();
    }
}

上記のように出力値をオンコードで書くと、メソッド名等を逐一書き換える必要が出てきてしまいます。
クラス名やメソッド名を動的に取得することで、ログ出力処理を共通のコードとすることができます。

public class MyClass1
{
    public void MyMethod1() 
    {
        Console.WriteLine("クラス名:" + GetType().FullName + ", メソッド名:" + MethodBase.GetCurrentMethod().Name);

        // 何か処理...
    }
}

public class MyClass2
{
    public void MyMethod2()
    {
        Console.WriteLine("クラス名:" + GetType().FullName + ", メソッド名:" + MethodBase.GetCurrentMethod().Name);

        // 何か処理...
    }
}

class MainClass
{
    public static void Main()
    {
        var _myClass1 = new MyClass1();
        _myClass1.MyMethod1();
        var _myClass2 = new MyClass2();
        _myClass2.MyMethod2();
    }
}

出力結果:

クラス名:MyClass1, メソッド名:MyMethod1
クラス名:MyClass2, メソッド名:MyMethod2

使用例3

たとえば、以下のようなクラスがあったとします。
 ※この時点で既にイマイチな設計だな…という話もあるかとは思いますが、
  致し方ないケースもあるのです…。さておき

class Document
{
    public string Name1 { get; set; }
    public string Name2 { get; set; }
    public string Name3 { get; set; }
    public string Name4 { get; set; }
    public string Name5 { get; set; }

    public int Age1 { get; set; }
    public int Age2 { get; set; }
    public int Age3 { get; set; }
    public int Age4 { get; set; }
    public int Age5 { get; set; }
}

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Documentクラスに、こんなデータを突っ込みたい場合、

List<Person> people = new List<Person>
{
    new Person { Name = "Taro", Age = 31 },
    new Person { Name = "Hanako", Age = 33 },
    new Person { Name = "Hitoshi", Age = 17 },
    new Person { Name = "Yoshie", Age = 28 },
    new Person { Name = "Kenta", Age = 21 }
};

愚直にやると、こんな実装をするハメになってしまいます。

Document doc1 = new Document
{
    // 地道に1つずつセット
    Name1 = people[0].Name,
    Age1 = people[0].Age,
    Name2 = people[1].Name,
    Age2 = people[1].Age
    Name3 = people[2].Name,
    Age3 = people[2].Age
    Name4 = people[3].Name,
    Age4 = people[3].Age
    Name5 = people[4].Name,
    Age5 = people[4].Age
};

上記の例のように、5個程度かつ単純な処理ならまだ良いですが、
項目が多くなると大変ですし、DRY原則に反していますね。

そこでリフレクションを使ってみましょう。

Document doc1 = new Document();
int index = 1;
foreach (var item in people)
{
    // Name[1-5]プロパティに動的にアクセスし、値を設定
    var nameProperty = typeof(Document).GetProperty("Name" + index.ToString());
    nameProperty.SetValue(doc1, item.Name);

    // Age[1-5]プロパティに動的にアクセスし、値を設定
    var ageProperty = typeof(Document).GetProperty("Age" + index.ToString());
    ageProperty.SetValue(doc1, item.Age);

    index++;
}

文字列を使ってプロパティにアクセスすることで、
foreachによって繰り返し処理をまとめて書くことができました。

使用例4

以下の2つのインスタンスを比較し、
差異のあるプロパティ名(この場合、”Name2″, “Age2″)を特定したいとします。

Document doc2 = new Document
{
    Name1 = "Akira",
    Age1 = 13,
    Name2 = "Takashi",
    Age2 = 43,
    Name3 = "Kiyoko",
    Age3 = 29,
};

Document doc3 = new Document
{
    Name1 = "Akira",
    Age1 = 13,
    Name2 = "Masaru",
    Age2 = 65,
    Name3 = "Kiyoko",
    Age3 = 29,
};

この場合は Type.GetProperties() メソッドを使用することで
オブジェクトが持つ全てのプロパティを取得し、foreachを回すことができます。

Type type = doc2.GetType();
PropertyInfo[] properties = type.GetProperties();

// GetProperties()にて取得したプロパティに対して繰り返し処理
foreach (PropertyInfo p in properties)
{
    PropertyInfo prop = type.GetProperty(p.Name);
    if (prop.GetValue(doc2) != prop.GetValue(doc3))
    {
        // 比較結果が異なっていたらプロパティ名を出力
        Console.WriteLine(p.Name);
    }
}

ところが…

Name2
Age1
Age2
Age3
Age4
Age5

意図に反してAge1〜Age5が出力されてしまいました。

どうやらPropertyInfo.GetValue()の戻り値がobject型であるため、
元の型がintの場合には異なるクラスと判断されてしまうようです。

元の型から判断してキャストする処理を書いても良いのですが、
せっかくなら型も動的に判断したいし、どうしたものかと考えた結果

動的な型…dynamic!

と思いついて、dynamic型にキャストしてみたところ、

Type type = doc2.GetType();
PropertyInfo[] properties = type.GetProperties();

// GetProperties()にて取得したプロパティに対して繰り返し処理
foreach (PropertyInfo p in properties)
{
    PropertyInfo prop = type.GetProperty(p.Name);

    // dynamic型にキャストして比較
    if ((dynamic)prop.GetValue(doc2) != (dynamic)prop.GetValue(doc3))
    {
        // 比較結果が異なっていたらプロパティ名を出力
        Console.WriteLine(p.Name);
    }
}
Name2
Age2

想定どおりの出力結果となりました。

おわりに

今回はリフレクションを使うと便利なケースをご紹介しましたが、
そもそもリフレクションはパフォーマンスが悪いという問題があり、
また、普通はアクセスできないものに、無理やりアクセスが可能であるため危険性も伴います。

リフレクションを使いたくなっても、抽象クラスや部分クラスを使う等、

別の手段で解決可能なケースあるかと思います。
既存のコードに手を入れられない事情がある、パフォーマンス低下がそれほど問題にならないケース等、
条件が揃った場合にリフレクションの使用を検討すべきでしょう。

最後までお読みいただき、ありがとうございました。

清水誠
エンジニア 清水誠
金融系業務システムの開発に従事。重くて遅い音楽を偏愛しています。