LoginSignup
3

More than 1 year has passed since last update.

posted at

Update your C# in Unity ~ ローカル関数 ~

Unityにおいて古いC#しか使えない時代もありました。しかし、それは過去のことです。本稿執筆時の最新LTSであるUnity 2019.4ではC# 7.3がサポートされています。また、本稿執筆時の最新Beta版であるUnity 2020.2ではC# 8.0がサポート予定です。

長らくUnityで古いC#しか使えなかったことで、「C#にこんな機能あるのか?知らなかった!」となることがある方も多いのではないでしょうか?この「Update your C# in Unity」シリーズでは、「C#の比較的新しい機能をUnityでこんな風に使えるよ!」という紹介を行います。


言語機能名: ローカル関数
追加バージョン: C# 7.0でローカル関数、C# 8.0で静的ローカル関数
説明: 関数を他の関数の中に入れ子にし、定義できる


ローカル関数を使うことで、関数の中に入れ子で関数を定義することができます。

private関数はその型のメンバーからは呼び出すことができます。一方でローカル関数は、その関数の中でしか呼び出せない、より狭いスコープの関数です。スコープを非常に狭めたく、かつ処理に名前をつけたい場合、ローカル関数を検討してください。


ローカル関数の利用例はLINQライクな関数の実装です。

次のコードはLINQライクなMapメソッドの実装の、よくない例です。仮引数であるsourceやpredicateがnullの場合、関数呼び出しのタイミングでArgumentNullExceptionが投げられることを期待したいところです。しかし、この実装だと返り値の要素を、最初に列挙するタイミングまでArgumentNullException投げられません。これにより、実行時の思わぬ不具合の原因になってしまうことがあります。

public static class MyEnumerable
{
        public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
        {
            if (source == null)
                throw new ArgumentNullException ("source");
            if (selector == null)
                throw new ArgumentNullException ("predicate");

            foreach (TSource element in source) {
                yield return selector (element);
            }
        }
    }
}

関数呼び出しのタイミングでArgumentNullExceptionが投げられることするためには、次のように二つの関数に分割する必要があります。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return source.Map_ (selector);
}

private static IEnumerable<TResult> Map_<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    foreach (TSource element in source) {
        yield return selector (element);
    }
}

これで期待通り関数呼び出しのタイミングでArgumentNullExceptionが投げられます。しかし、Mapからしか呼ばないMap_ができてしまいました。
これはローカル関数を利用することで、次にように改善することができます。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return Impl (source, selector);

    IEnumerable<TResult> Impl(IEnumerable<TSource> source_, Func<TSource,TResult> selector_)
    {
        foreach (TSource element in source_) {
            yield return selector_ (element);
        }
    }
}

次のように、ローカル関数から外側の関数の変数をキャプチャすることもできます。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return Impl ();

    // 外側の関数の引数や変数をキャプチャできる
    IEnumerable<TResult> Impl ()
    {
        foreach (TSource element in source) {
            yield return selector (element);
        }
    }
}

ローカル関数から外側の関数の変数をキャプチャすることが必要な場合や便利な場合もあります。
しかし、うっかり意図と違うキャプチャをしてしまい不具合の原因になることがあります。

これを解決するために、C# 8.0から静的ローカル関数が加わりました。
静的なローカル関数では、外側の関数の変数をキャプチャするとコンパイルエラーになります。
これにより、意図しない外側の関数の変数のキャプチャを防ぐことができます。
static修飾子をローカル関数につけることで静的なローカル関数になります。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return Impl (source, selector);

    // C# 8.0から使える
    // staticがついていると静的ローカル関数
    // 外側の関数の引数や変数のキャプチャを許さない
    static IEnumerable<TResult> Impl(IEnumerable<TSource> source_, Func<TSource,TResult> selector_)
    {
        foreach (TSource element in source_) {
            yield return selector_ (element);
        }
    }
}

Unityでの利用例をあげます。

次のように、IEumeratorを返すLaunchImpl関数と、それを内部で呼び出しCoroutineを返すLaunch関数を定義していたとします。IEnumeratorを返すLaunchImpl関数は、Coroutineを返すLaunch関数からしか利用していません。StartCoroutineの仕様上、このように二つの関数を分離する必要があります。

using System.Collections;
using UnityEngine;

public class Launcher : MonoBehaviour
{
    public Coroutine Launch()
    {
        return StartCoroutine(LaunchImpl());
    }

    private IEnumerator LaunchImpl()
    {
        // 略
        yield break;
    }
}

これはローカル関数を使い次のように定義することもできます。

public class Launcher : MonoBehaviour
{
    public Coroutine Launch()
    {
        return StartCoroutine(LaunchImpl());

        static IEnumerator LaunchImpl()
        {
            // 略
            yield break;
        }
    }
}

ローカル関数を使うことで、関数のスコープと可視性を関数内のスコープだけに制限することができます。
private関数より狭いスコープで定義することができます。
スコープを非常に狭めたく、かつ処理に名前をつかたい場合、ローカル関数を検討してください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3