Update your C # in Unity ~ Local Functions ~

4 minute read

There was a time when only old C # could be used in Unity. But that’s a thing of the past. The latest LTS at the time of this writing, Unity 2019.4, supports C # 7.3. In addition, C # 8.0 will be supported in Unity 2020.2, the latest Beta version at the time of this writing.

Since I could only use old C # in Unity for a long time, I think there are many people who say “Is there such a function in C #? I didn’t know!”. In this “Update your C # in Unity” series, we will introduce “You can use relatively new features of C # in Unity like this!”.


Language feature name: local function
Additional versions: local functions in C # 7.0, static local functions in C # 8.0
Description: Functions can be nested and defined within other functions


You can define functions nested within a function by using a local function.

Private functions can be called from members of that type. Local functions, on the other hand, are narrower scoped functions that can only be called within that function. If you want to be very narrow in scope and want to name the process, consider local functions.


An example of using a local function is an implementation of a LINQ-like function.

The following code is a bad example of implementing a LINQ-like Map method. If the formal arguments source and predicate are null, I would like to expect that ʻArgumentNullException will be thrown at the timing of the function call. However, with this implementation, ʻArgumentNullException is not thrown until the timing of first enumerating the return value elements. This can lead to unexpected run-time glitches.

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);
            }
        }
    }
}

In order for ʻArgumentNullException` to be thrown at the timing of function call, it is necessary to divide it into two functions as follows.

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);
    }
}

This will throw ʻArgumentNullException` at the timing of the function call as expected. However, I have a Map_ that can only be called from Map.
This can be improved as follows by using local functions.

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);
        }
    }
}

You can also capture the variables of the outer function from the local function as follows:

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 ();
    
    //Can capture the arguments and variables of the outer function
    IEnumerable<TResult> Impl ()
    {
        foreach (TSource element in source) {
            yield return selector (element);
        }
    }
}

Sometimes it is necessary or convenient to capture the variables of the outer function from the local function.
However, you may inadvertently capture something unintended and cause problems.

To solve this, static local functions have been added since C # 8.0.
For static local functions, capturing variables in the outer function will result in a compilation error.
This will prevent unintended capture of variables in the outer function.
Adding the static modifier to a local function makes it a static local function.

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.Can be used from 0
    //Static local function with static
    //Do not allow capture of outer function arguments or variables
    static IEnumerable<TResult> Impl(IEnumerable<TSource> source_, Func<TSource,TResult> selector_)
    {
        foreach (TSource element in source_) {
            yield return selector_ (element);
        }
    }
}

Here is an example of using it in Unity.

Suppose you have defined a LaunchImpl function that returns an IEumerator and a Launch function that calls it internally and returns a Coroutine, as follows: The LaunchImpl function that returns an IEnumerator is only used by the Launch function that returns a Coroutine. Due to the StartCoroutine specification, it is necessary to separate the two functions in this way.

using System.Collections;
using UnityEngine;

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

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

It can also be defined using a local function as follows:

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

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

You can use local functions to limit the scope and visibility of a function to the scope within the function.
It can be defined with a narrower scope than private functions.
If you want to be very narrow in scope and want to name the process, consider local functions.