渋谷ほととぎす通信

エンジニア社長によるUnityとAIのブログ & エンジニアの生存戦略

【Unity】開発の幅が広がるC#のリフレクション入門

【Unity】開発の幅が広がるC#のリフレクション入門

こんにちは、Unityエンジニアのオオバです。

お悩みさん
お悩みさん
  • internalクラスにアクセスしたい
  • Unity内に隠された機能を使ってみたい
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    ゲーム開発中またはUnityエディタを拡張しているときUnityのinternalクラスにアクセスしたくなるときがあります。

    internalとはpublicに提供していないけどUnityが内部で使用している機能のこと。 「めちゃくちゃ便利そうな機能っぽいけど使えない。。。」 って思ったことありませんか?

    そこで本記事ではC#のリフレクションという機能を使って内部クラスにアクセスする方法を紹介していきます。本番アプリへの採用は危険ですがエディタ拡張に使用する分にはリスク低めです。

    • Unity内に隠されている機能を使ってみたい
    • より深くUnityの中に潜ってみたい

    このような方はぜひ最後まで読んでみてください。

    👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

    UnityエディタのC#コードの確認方法

    リフレクションを始める前に、Unityエディタがどのようなコードで動作しているのか確認する方法を紹介します。

    お悩みさん
    お悩みさん
    Unityエディタのコードって見れるの?
    オオバ
    オオバ
    一部ではありますが確認可能です

    例えばMonoBehaviourクラス内に定義された関数はどんなものがあるでしょうか。

    実はUnity公式のGitHubで公開されています。以下のページからUnityエディタ内のC#コードを確認できます。

    例えばMonoBehaviourは↓↓こちらです。

    または皆さんが普段使用しているソースコードエディタ「Visual Studio」や「Rider」を使っていると何も設定することなく中のソースは確認可能です。

    【Unity】開発の幅が広がるC#のリフレクション入門_0
    上図はRiderの画面です。ソースコード上の「MonoBehaviour」を選択した状態で Go To -> Go to Declaration or Usages つまり宣言場所にジャンプでコードを確認できます。

    リフレクションって何?

    ここから実際にリレフレクションを使って内部クラスにアクセスしていきますが、そもそもリフレクションとは何か解説していきます。

    リフレクション(Reflection)とはプログラム実行中にオブジェクトの型情報を取得し、動的にメソッドやプロパティを操作できる仕組みのことです。

    クラス内のメソッドリストを取得したり、文字列から関数を実行したりすることが可能です。

    例えば次のようなprivate関数を外側から呼び出す手段は本来ありません。

    public class Sample  
    {
        // privateメソッドは外から呼び出せない  
        private void PrivateHoge()  
        {
            Debug.Log("Execute PrivateHoge!");  
        }
    }
    

    しかしリフレクションを使うと次のようなコードでprivate関数を外側から実行可能になります。

    var sample = new Sample();  
    var type = sample.GetType();  
    // 文字列で関数名を指定して取得  
    var method = type.GetMethod("PrivateHoge",  
        BindingFlags.NonPublic | BindingFlags.Instance);  
    if (method != null)  
    {
        // Private関数を実行  
        method.Invoke(sample, null); //出力:Execute PrivateHoge!  
    }
    

    このようにリフレクションを使うとクラスの外からprivate関数を簡単に呼び出せます。面白くないですか?オオバは初めてリフレクションを知ったとき 「ヤバい!何でも操作できる!」 と興奮したのを覚えています。

    リフレクションによって通常アクセスできない機能が使える

    リフレクションを使えるようになって何がおいしいのかと言うと 「本来アクセスできない機能にアクセスできる」 ことです。

    例えばUnityにはAnimationウィンドウというアニメーション編集パネルがあります。Animationウィンドウ内の レコードボタン はソースコードから切り替えるAPIは提供されていません。しかしリフレクションを使えば可能です。

    【Unity】開発の幅が広がるC#のリフレクション入門_1

    上の動画のようにスクリプトからレコードボタンを操作できるようになりました。リフレクションを使うことで今まで正攻法ではできなかった処理を作ることができるため 開発の幅が一気に広がる のです。

    リフレクションの注意点

    一見便利なリフレクションですが、デメリットもあります。それは「負荷」と「APIの更新」です。

    1点目はリフレクションが 高負荷 であることです。特性上リフレクションはCPU負荷が高くなりがちです。パフォーマンスを求めるゲーム開発では使い所を見定める必要があります。

    2点目はAPIの更新によってリフレクションで作成した処理が動作しなくなる可能性があることです。例えばUnityのバージョンアップによって内部処理が変わると リフレクションで作った処理は動かなくなります。

    このようなデメリットを踏まえた上でリフレクションと付き合っていきましょう。

    具体的なリフレクションの使い方

    ここから具体的なリフレクションの使い方について解説していきます。リフレクションは様々な使い方があるため、よく使うものをピックアップして解説していこうと思います。

    サンプルとして以下リンク先のコードをリフレクションで参照、アクセスして解説していきます。

    【Unity】開発の幅が広がるC#のリフレクション入門_2
    リフレクションサンプルクラス

    これからリフレクションを使って上記のクラスのデータを取得していきますが、前提として以下の情報しか所持していないものとし、あくまで文字列で取得していきます。

    • ネームスペース(info.shibuya24)
    • クラス名(ReflectionSample)

    早速リフレクションを使ってクラスを解析していきましょう。

    文字列からクラスを取得し生成するリフレクション

    最初のリフレクションとして「文字列からクラス情報を取得」と「インスタンス化」をしてみます。インスタンス化とはnewキーワードを使ってましたよね。

    ReflectionSample instance = new ReflectionSample();  
    

    上記のようなインスタンス化をリフレクションで実現してみます。

    文字列からクラスの取得

    まずは文字列からクラスを取得してみましょう。必要な情報は クラスのフルネーム です。フルネームとはネームスペースを含んだクラス名です。

    namespace info.Shibuya24  
    {
        public class ReflectionSample(){}  
    }
    

    例えば上記のクラスであれば、 info.Shibuya24.ReflectionSample がクラスのフルネームになります。ネームスペースとクラス名をドットでつなげた形です。

    具体的には次のコードでクラス情報を取得します。

    Type type = Type.GetType("info.shibuya24.ReflectionSample");  
    

    Type.GetType を使うとクラス情報を取得できます。クラス情報は 「Type型」 で扱います。

    このType型のインスタンス「type」を使って以降のリフレクションを取り扱っていきます。

    リフレクションで取得したクラスのインスタンス化

    次にリフレクションで取得したクラスをインスタンス化していきます。前章で取得したType型のインスタンス「type」を使います。

    次のコードを見てみましょう。

    object instance = Activator.CreateInstance(type);  
    Debug.Log(instance);  
    

    具体的には Activator.CreateInstance の引数にtypeを渡すことでインスタンス化します。

    戻り値はobject型であることに注意してください。このobject型のインスタンスは以降の メソッドの実行変数の取得 で使用していきます。

    別アセンブリだった場合のアクセスの場合

    上記のコードでクラス情報が取得できない場合があります。

    考えられるケースとして別のアセンブリからのアクセスです。例えばUnity開発では以下のようなケースが発生します。

    • リフレクション対象がランタイムコード
    • リフレクション元がエディタ拡張コード

    ランタイムコードとエディタ拡張コードはアセンブリが異なるため、先のコードではリフレクションで値を取得できません。

    【Unity】開発の幅が広がるC#のリフレクション入門_3

    Unityのランタイムコードはデフォルト状態で 「Assembly-CSharp」 に、エディターコードは 「Assembly-Editor」 にまとめられます。

    異なるアセンブリ間で処理する場合は、GetType関数の引数にアセンブリ情報を追加してみましょう。

    Type type = Type.GetType("info.shibuya24.ReflectionSample, Assembly-CSharp");  
    

    上記のように「info.shibuya24.ReflectionSample, Assembly-CSharp」と、 アセンブリ名を追加 することで別アセンブリの要素にもアクセスできるようになるのです。

    もしリフレクションに失敗してしまう場合はアセンブリを疑ってみてください。

    publicメソッドをリストアップするリフレクション

    クラス情報は取得できましたが、このままではどんなメソッドが実装されているかわかりません。そこでクラス内に宣言されているメソッドをリストアップしてみましょう。解析の第一歩ですね。

    次のコードを実行してみてください。

    Type type = Type.GetType("info.shibuya24.ReflectionSample");  
    object instance = Activator.CreateInstance(type);  
    
    // typeからメソッド情報を取得  
    MethodInfo[] methods = type.GetMethods(  
        BindingFlags.Instance |  
        BindingFlags.DeclaredOnly |  
        BindingFlags.Public);  
    
    foreach(var method in methods)  
    {
        Debug.Log(method.Name);  
    }
    

    すると次の3つのログが出力されます。

    get_MemberPublicProp  
    set_MemberPublicProp  
    MemberPublicMethod  
    
    ログ内容
    get_MemberPublicProppublicプロパティ(ゲッター)
    set_MemberPublicProppublicプロパティ(セッター)
    MemberPublicMethodpublicメソッド

    クラスに定義されたpublic要素が取得できたことが分かります。プロパティだけ特殊で、頭に「get」「set」が自動的に付与されることに注意しましょう。

    このように 「GetMethods」 関数を使うことで簡単にクラス内に宣言されたメソッド一覧を取得できます。

    お悩みさん
    お悩みさん
    親クラスの関数は取得できないのかな?

    本章ではあくまで対象のクラスに定義されたメソッドリストの取得でした。次に取得対象を親クラスを含めてみましょう。

    親クラスを含めたpublicメソッドをリストアップするリフレクション

    前章でクラス内に定義されたpublicメソッドをリストアップしましたが、親クラスのpublicメソッドもリストアップしてみたいですよね。実はとても簡単です。

    Type type = Type.GetType("info.shibuya24.ReflectionSample");  
    object instance = Activator.CreateInstance(type);  
    
    // typeからメソッド情報を取得  
    MethodInfo[] methods = type.GetMethods(  
        BindingFlags.Instance |  
        BindingFlags.Public);  
    
    foreach(var method in methods)  
    {
        Debug.Log(method.Name);  
    }
    

    上記のコードを実行すると次のように親クラスを含めたpublic関数、プロパティのログが出力されます。

    get_MemberPublicProp  
    set_MemberPublicProp  
    MemberPublicMethod  
    get_ParentMemberPublicProp  
    set_ParentMemberPublicProp  
    ParentMemberPublicMethod  
    Equals  
    GetHashCode  
    GetType  
    ToString  
    

    ポイントは親クラスの更に親クラスである「objectクラス」のpublicメソッド(EqualsToStringなど)も取得できていることです。

    ソースコードの変更点はたったの1箇所。

    type.GetMethods(  
        BindingFlags.Instance |  
        BindingFlags.DeclaredOnly |  
        BindingFlags.Public);  
    

    type.GetMethods(  
        BindingFlags.Instance |  
        BindingFlags.Public);  
    

    「BindingFlags.DeclaredOnly」 を消したことです。

    BindingFlags.DeclaredOnlyが存在すれば親クラスは対象外、BindingFlags.DeclaredOnlyを除けば親クラスを対象とします。

    ぜひ覚えておきましょう。

    BindingFlagsがリフレクションの肝

    前章で登場したGetMethods関数に使用した 「BindingFlags」がリフレクションの肝 。BindingFlagsの組み合わせによって自分の思い通りの情報を取得できるようになります。

    ここまでで登場したBindingFlagsをまとめてみました。

    BindingFlagsリフレクション対象備考
    Instanceインスタンス要素-
    Publicpublic要素-
    DeclaredOnly宣言元のみ外すと親クラスも対象

    以降の章ではリフレクションを使ってデータを取得する中で、BindingFlagsの使い方を解説していきます。みなさんもここからは BindingFlags に注目して読んでみてください。

    public以外のメソッドをリストアップするリフレクション

    ここまでpublicメソッドの取得をリフレクションで実現してきました。

    お悩みさん
    お悩みさん
    privateメソッドも取得できる?
    オオバ
    オオバ
    もちろん可能です!

    では次にprivateやprotectedメソッドを取得してみましょう。

    前章コード内の GetMethods の引数を次のように変えて実行してみます。

    MethodInfo[] methods = type.GetMethods(  
        BindingFlags.Instance |  
        BindingFlags.NonPublic);  
    

    すると次のようにログが出力されます。

    get_MemberInternalProp  
    set_MemberInternalProp  
    get_MemberPrivateProp  
    set_MemberPrivateProp  
    get_MemberProtectedProp  
    set_MemberProtectedProp  
    MemberPrivateMethod  
    MemberPrivateMethodArg  
    MemberProtectedMethod  
    MemberInternalMethod  
    get_ParentMemberInternalProp  
    set_ParentMemberInternalProp  
    get_ParentMemberProtectedProp  
    set_ParentMemberProtectedProp  
    ParentMemberProtectedMethod  
    ParentMemberInternalMethod  
    Finalize  
    MemberwiseClone  
    

    「internal」「private」「protected」のメソッドとプロパティが取得できました。しかし、 親クラスのprivateメソッドとプロパティは取得できていない ことにお気づきでしょうか。

    つまりGetMethods関数のリフレクション対象内のみprivate情報を取得できるということです。では親クラスのprivate情報はどうやって取得するのでしょうか。

    結論、リフレクション対象を親クラスにすればよいのです。つまり GetMethodsの対象を親クラスにする ということです。

    親クラスを取得するリフレクション

    では親クラスをリフレクションで取得し見ましょう。リフレクションで親クラスへのアクセスはとても簡単で次の2行で実装できます。

    Type type = Type.GetType("info.shibuya24.ReflectionSample");  
    Type baseType = type.BaseType;  
    

    取得したTypeに対して 「BaseType」 プロパティで親クラスを取得できます。そして親クラスに対してGetMehods関数を使うことでprivate情報を取得することができるのです。

    Type type = Type.GetType("info.shibuya24.ReflectionSample");  
    Type baseType = type.BaseType;  
    MethodInfo[] methods = baseType.GetMethods(  
        BindingFlags.Instance |  
        BindingFlags.NonPublic);  
    

    上記のコードで 親クラスのprivateメソッド を取得できるようになりました。

    リフレクションで取得したメソッドを実行する方法

    お悩みさん
    お悩みさん
    メソッドの実行はどうやるの?

    次にリフレクションで取得したメソッドを実行してみましょう。

    手順は簡単です。

    1. 型情報の取得
    2. インスタンス化
    3. GetMethodでメソッド取得
    4. Invokeで実行

    ソースコードに落とし込むと次のようになります。

    // 1.型情報取得  
    Type type = Type.GetType("info.shibuya24.ReflectionSample");  
    // 2.インスタンス化  
    object instance = Activator.CreateInstance(type);  
    // 3.GetMethodでメソッド取得  
    MethodInfo method = type.GetMethod("MemberPrivateMethod",  
        BindingFlags.Instance |  
        BindingFlags.NonPublic);  
    // 4.Invokeで関数実行  
    method.Invoke(instance, null);  
    

    ポイントは「GetMethod」と「Invoke」です。

    GetMethodで呼びたいメソッド名(文字列)とBindingFlagsで MethodInfo型 のインスタンスを取得します。

    MethodInfoの Invoke関数 を実行することでメソッドを呼び出すことができるのです。

    引数付きのメソッドを文字列から実行するリフレクション

    次に引数ありのメソッドを実行する方法です。先の例では引数がありませんでした。引数ありメソッド「MemberPrivateMethodArg」をリフレクションで取得して実行してみます。

    次のコードを見てみましょう。

    MethodInfo method = type.GetMethod("MemberPrivateMethodArg",  
        BindingFlags.Instance |  
        BindingFlags.NonPublic);  
    method.Invoke(instance, new object[] { 80, "shibuya24" });  
    

    MethodInfoの第2引数に呼びたいメソッドの引数を設定します。型はobject型の配列です。

    今回のサンプルは、第1引数がint型、第2引数が文字列型だったため、以下の配列が引数になります。

    new object[] { 80, "shibuya24" }  
    

    このようにリフレクションで取得した引数ありのメソッドも問題なく実行できます。

    メソッドの引数を調べるリフレクション

    メソッドは取得できたけど引数がわからないということもあると思います。リフレクションを使えば引数情報も取得可能です。

    private void MemberPrivateMethodArg(int id, string name)  
    {
        Debug.Log($"{nameof(MemberPrivateMethodArg)} id : {id}, name : {name}");  
    }
    

    上記の引数ありメソッドをリフレクションで実行してみましょう。次のソースコードを見てみてください。

    // GetParametersで引数情報を取得  
    ParameterInfo[] parameters = methodInfo.GetParameters();  
    foreach (var p in parameters)  
    {
        Debug.Log(p.ParameterType.Name);  
    }
    

    上記のコードを実行すると次のように型名が出力されます。

    Int32  
    String  
    

    第1引数がInt32(int型)、第2引数がString型(文字列型)ということが分かります。

    MethodInfoの GetParameters関数 を呼ぶことでParameterInfo型の配列を取得できます。ParameterInfo型に引数情報が詰まっています。

    ParameterInfoの「ParameterType」から引数の型を取得しているのです。

    もし取得したメソッドの引数がわからないときは GetParametersParameterType を活用してみましょう。

    メソッド戻り値の型を調べるリフレクション

    メソッド名、引数情報ともにリフレクションで取得できるようになりました。最後に戻り値の型情報をリフレクションで取得してみます。

    methodInfo.GetParameters();  
    // ReturnTypeで戻り値の型情報を取得  
    Debug.Log(methodInfo.ReturnType);  
    

    上記コードの通り、戻り値の型の取得はMethodInfoの ReturnType を使います。

    以上で一通りのメソッド情報を取得できるようになったかと思います。

    クラスメソッドへアクセスするリフレクション

    前章まででインスタンスメソッドへのアクセスはできるようになりました。次にクラスメソッドにアクセスしてみます。

    ポイントはBindingFlagsです。

    MethodInfo[] methodInfos = type.GetMethods(  
        BindingFlags.Public  
        | BindingFlags.Static  
        | BindingFlags.NonPublic  
        | BindingFlags.FlattenHierarchy  
    );  
    foreach (var m in methodInfos) Debug.Log(m.Name);  
    

    ポイントは 「BindingFlags.Static」 です。BindingFlags.Staticを指定するとstatic要素を取得できます。

    もう1つ新しい要素として「BindingFlags.FlattenHierarchy」です。static要素を取得する場合「BindingFlags.FlattenHierarchy」を指定しないと親クラスのstatic要素は取得できません。

    親クラスのstatid要素を取得したい場合は 「BindingFlags.FlattenHierarchy」 を指定しましょう。

    BindingFlags.FlattenHierarchyの注意点

    BindingFlags.FlattenHierarchyには注意点があります。それは 親クラスのprivate要素は取得できない ことです。

    「BindingFlags.FlattenHierarchy」と「BindingFlags.NonPublic」を指定しても親クラスのprivate要素は取得できません。

    親クラスのprivate static要素にアクセスするためには、type.baseTypeでリフレクションの対象を親クラスに変更します。

    MethodInfo[] methodInfos = type.baseType.GetMethods(  
        BindingFlags.Static  
        | BindingFlags.NonPublic  
    );  
    

    上記のように「baseType」で親クラスを指定することでprivate statid要素にアクセスできるようになるのです。

    リフレクション変数編

    ここまでメソッドを取得するリフレクションを解説してきました。ここからはリフレクションを使って変数を解析、処理をしていきます。

    • 変数リストの取得
    • 変数の値の取得
    • 変数の値の更新

    変数については上記の3種類の処理をリフレクションで解説していきます。

    1.変数リストを取得するリフレクション

    最初に変数リストを取得してみましょう。基本的にメソッドのときと同様でBindingFlagsを調整して取得する変数をフィルタリングします。

    // public以外のインスタンス変数リストを取得  
    FieldInfo[] fieldInfos = type.GetFields(  
        BindingFlags.NonPublic  
        | BindingFlags.Instance  
        | BindingFlags.DeclaredOnly  
    );  
    

    クラス内の変数リストはGetFields関数を使います。上記のコードを実行するとクラス内に宣言された変数情報リストを取得できます。

    2.変数の値を取得するリフレクション

    次に変数に格納された値を取得します。早速次のソースコードを見ていきましょう。

    // 変数情報の取得  
    FieldInfo fieldInfo = type.GetField("MemberPrivateField",  
        BindingFlags.NonPublic  
        | BindingFlags.Instance  
    );  
    // GetValueで値を取得  
    var value = fieldInfo.GetValue(instance);  
    Debug.Log(value);  
    

    FieldInfoのGetValue関数を使って変数の値を取得できます。GetValueの引数には生成したインスタンスを渡してください。

    3.変数の値を更新するリフレクション

    変数リフレクション最後は値の更新です。早速ソースコードから見ていきましょう。

    // 変数情報の取得  
    FieldInfo fieldInfo = type.GetField("MemberPrivateField",  
        BindingFlags.NonPublic  
        | BindingFlags.Instance  
    );  
    // SetValuで値の更新  
    fieldInfo.SetValue(instance, 120);  
    

    リフレクションで値の更新は SetValue を使います。第1引数にはインスタンス、第2引数には更新したい値を代入してください。

    変数の型を調べるリフレクション

    そもそも変数の型が分からないこともありますよね。リフレクションを使って変数の方を調べてみましょう。

    // 変数情報の取得  
    FieldInfo fieldInfo = type.GetField("MemberPrivateField",  
        BindingFlags.NonPublic  
        | BindingFlags.Instance  
    );  
    // 変数の型を取得  
    Type fieldType = fieldInfo.FieldType;  
    Debug.Log(fieldType.Name);  
    

    上記コードのようにFieldInfoの 「FieldType」 から変数の型を取得できます。もし変数の型が分からないときに活用してみてください。

    リフレクションまとめ

    本記事では、Unityで使うC#のリフレクションについて解説してきました。簡単に内容をまとめます。

    まずはリフレクションでよく使う関数について。

    関数内容
    GetMethodメソッド取得
    GetMethodsメソッドリスト取得
    Invokeメソッド実行
    GetField変数取得
    GetFields変数リスト取得
    GetValue変数の値取得
    SetValue変数の値を更新

    リフレクションの肝となるBindingFlagはこちら。

    BindingFlags取得対象備考
    Publicpublic要素-
    NonPublicpublic以外の要素親クラスのprivateは取得不可
    Instanceインスタンス要素-
    StaticStatic要素-
    DeclaredOnly宣言クラスのみインスタンス要素のみ
    FlattenHierarchy親クラスを含むStatic要素のみ

    リフレクションを使えるようになると 開発の自由度 が上がります。なぜなら通常アクセスできない Unity内部の機能を使える ためです。

    内部クラスにアクセスすることで今まで作れなかったツールや機能を開発できるようになり開発の幅が一気に広がります。

    しかし便利な一方 リフレクションは負荷が高い です。使い所を見定める必要があります。オオバは基本的に ツールやエディタ拡張でのみ使用 することにしています。基本的にランタイムコードでは使いません。

    また内部クラスへのアクセスは十分注意してください。基本的に 自己責任 です。Unityのバージョンアップによって動かなくなることもあるため注意しましょう。

    以上の注意点を把握した上でリフレクションと付き合ってみてください。開発がもっと面白くなりますよ。

    Unityオブジェクトの描画順の制御って難しいですよね。
    この度、Unityの描画順を体系的に学べる「Unity描画順の教科書」を執筆しました。

    Unityの描画順を基礎から学びたい方はぜひ確認してみてください!
    Unity描画順の教科書

    最後まで読んでいただきありがとうございました!
    すばらしいリフレクションライフをお過ごしください。

    オススメ記事