渋谷ほととぎす通信

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

【Unity】C#のActionとローカル関数どっちを使うべき?

【Unity】C#のActionとローカル関数どっちを使うべき?

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

お悩みさん
お悩みさん
  • C#のActionとローカル関数の違いって何?
  • ローカル関数は使うべき?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    Unityでゲーム開発する際によく使う型「Action」。UIを実装するときもしょっちゅう登場します。

    Actionと同じような機能を実装できる「ローカル関数」をご存知でしょうか?Actionと同様な機能を実装できるけど、どっちを使ったら良いのか迷いますよね。

    そこで本記事ではActionとローカル関数のパフォーマンスの違い、どちらを使うべきかを解説していきます。

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

    ローカル関数とは?

    ローカル関数はその名の通り関数内で使用できる関数です。関数内でしか参照できない関数ということです。

    ローカル関数はC#7から登場しました。ちなみにC#7はUnity2018.3から使用可能です。

    ちなみにUnity6からC#9が使用可能になりました。

    ローカル関数とActionのパフォーマンスの違い

    ローカル関数はActionでも同じような実装が可能です。しかし内部的にどちらの負荷が高いのかSharpLabで確認してみましょう。

    SharpLabとは自分の書いたC#コードがどのようなコードにコンパイルされるか確認できる便利サイトです。

    ローカル関数とActionがどのようなC#にコンパイルされるか調査してみましょう。

    以下が検証コードです。

    using System;  
    
    public class C  
    {
        public void M()  
        {
            Action a =()=>{ Console.WriteLine("Hoge"); };  
            a();  
        }
    
        public void N()  
        {
            void b(){ Console.WriteLine("Foo"); };  
            b();  
        }
    }
    
    • M関数・・・Actionを使用
    • N関数・・・ローカル関数を使用

    この2つの関数がどのようなコードにコンパイルされるでしょうか。

    コンパイルされたC#

    IL変換前のC#は以下のようにコンパイルされます。

    public class C  
    {
        [Serializable]  
        [CompilerGenerated]  
        private sealed class <>c  
        {
            public static readonly <>c <>9 = new <>c();  
    
            public static Action <>9__0_0;  
    
            internal void <M>b__0_0()  
            {
                Console.WriteLine("Hoge");  
            }
        }
    
        public void M()  
        {
            (<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Action(<>c.<>9.<M>b__0_0)))();  
        }
    
        public void N()  
        {
            <N>g__b|1_0();  
        }
    
        [CompilerGenerated]  
        internal static void <N>g__b|1_0()  
        {
            Console.WriteLine("Foo");  
        }
    }
    

    中身を見ていきます。

    SharpLab全ソースはこちら

    ローカル関数の処理

    1. ローカル関数bをprivate関数として関数Nの外に出す
    2. N関数がb関数を呼ぶ

    というシンプルな処理になっています。

    Actionの処理

    1. 事前にActionで定義された処理を含むprivate classを定義
    2. private classのstaticなインスタンスを生成しておく
    3. M関数実行初回にActionオブジェクトを生成(2度目以降はprivate classのstatic変数のキャッシュを利用)
    4. private classに定義したActionの処理を実行

    ローカル関数と比べると複雑な処理になっていることが分かります。

    ローカル関数の方が軽いけども...

    メモリアロケーション、CPU負荷の観点から考えればActionよりローカル関数の方がパフォーマンスが高くなりそうです。

    とはいえローカル関数にもデメリットがあるとオオバは考えています。

    それは 「ソースコードの見通しの悪さ」 です。

    ローカル関数を使用するとメソッドの終了地点が分かりづらくなります。例えば次のソースコードです。

    public void Initialize()  
    {
        //~~~~~~ メソッド処理 ~~~~~~  
    
        void _Hoge()  
        {
            // ローカル関数の処理  
        }
    
        void _Foo()  
        {
            // ローカル関数の処理  
        }
    }
    

    ローカル関数が入ることでことでメソッド処理がどこまで続いているのかが分かりづらくなります。それによってコードレビューがしづらくなった時がありました。

    インスタンス関数とローカル関数の違いが分かりづらいため、関数名の頭に「_」をつけました。

    ここは好き嫌いがあると思うのでチーム内でルールを決めてもらえればと思います。

    まとめ

    本記事ではC#のActionとローカル関数について解説してきました。

    Actionもローカルカンスも同様の機能を実装することができますが、パフォーマンスに違いが出てきます。

    パフォーマンス的にはローカル関数の方が軽いですが、ソースコードの見通しは悪くなります。

    Actionとローカル関数の使い分けはチーム内でルール化しておいた方がトラブルがなくなるかなと思います。

    オススメ記事
    参考サイト