Quantcast
Channel: ++C++; // 未確認飛行 C
Viewing all articles
Browse latest Browse all 75

ローカル関数と匿名関数

$
0
0

概要

C# には、関数内に関数を書く方法として、ローカル関数と匿名関数という2つの機能があります。

いずれも共通して、以下のような性質があります。

  • 定義している関数の中でしか使えない
  • 周りの(定義している関数側にある)ローカル変数を取り込める

ローカル関数の方ができることは多いですが、書ける場所は少なくなります。 匿名関数はその逆で、できることに少し制限がある代わりに、どこにでも書けます。

サンプル コード: https://github.com/ufcpp/UfcppSample/tree/master/Chapters/Functional/LocalFunctions

ローカル関数

Ver. 7

C# 7では、関数の中で別の関数を定義して使うことができます。 関数の中でしか使えないため、ローカル関数(local function: その場所でしか使えない関数)と呼びます。

例えば以下のように書けます。

using System;

class Program
{
    static void Main()
    {
        // Main 関数の中で、ローカル関数 f を定義
        int f(int n) => n >= 1 ? n * f(n - 1) : 1;

        Console.WriteLine(f(10));
    }
}

ローカル関数(この例でいう f)は、定義した関数(この例でいう Mainメソッド)の中でしか使えません。

ローカル関数は、通常のメソッドでできることであれば概ね何でもできます。例えば、以下のようなこともできます。

  • 再帰呼び出し
  • イテレーター
  • 非同期メソッド

また、メソッド内に限らず、関数メンバーならどれの中でも定義できます。

class Sample
{
    public Sample()
    {
        int f(int n) => n * n;
    }

    public int Property
    {
        get
        {
            int f(int n) => n * n;
            return f(10);
        }
    }

    public static Sample operator+(Sample x)
    {
        int f(int n) => n * n;
        return null;
    }
}

ローカル関数の使い道

ローカル関数を使いたくなる一番の動機は、定義した関数内からだけ使えるというになるでしょう。

あるメソッドMの中から、そのMでしか使わないメソッドを呼び出したい場面が時々あります。 このとき、ローカル関数を使わないと、Mでしか使わないメソッドにMInternalなど、あまり意味のない名前を付ける羽目になり、不格好です。

static void M()
{
    // 何らかの前準備とか
    MInternal();
}

static void MInternal()
{
    // 実際の処理はこちらで
}

名前が不格好な程度ならそれほど大きな問題ではないんですが、 このMInternalは、M以外のメソッドからも呼べてしまうという問題が発生します。 こういう場合に、ローカル関数を使えば、以下のように書くことができ、呼びたい場所からだけ呼べるようになります。

static void M()
{
    // 何らかの前準備とか

    void m()
    {
        // 実際の処理はこちらで
    }

    m();
}

例1: イテレーターの引数チェック

例えば、イテレーターの引数チェックではこういうコードが必要になりがちです。

例として、標準ライブラリ中の処理を1つ自作してみましょう。Enumerableクラス(System.Linq名前空間)のWhereメソッドをまねてみます。 まず、単純な書き方をしてみましょう。この書き方には、コメントに書いてあるように、少し欠陥があります。

using System;
using System.Collections.Generic;

static class MyEnumerable
{
    public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        // イテレーター中のコードは、最初に列挙した(foreach などに渡す)時に初めて実行される
        // このメソッドを呼んだ時点では、↓この引数チェックが働かない
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));

        foreach (var x in source)
            if (predicate(x))
                yield return x;
    }
}

コメント中に「メソッドを呼んだ時点では引数チェックが働かない」とありますが、使う側のコードも書いてみると問題がよりはっきりするでしょう。 以下のように、期待されるのと異なるタイミングで例外が起きます。

using Iterator1;
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        IEnumerable<string> input = null;

        // input が null なので例外を投げてほしい
        // 多くの人がそれを期待する
        var output = input.Where(x => x.Length < 10);

        Console.WriteLine("ここが表示されるとおかしい"); // でも表示される

        foreach (var x in output) // 実際に例外が出るのはこの行
        {
            Console.WriteLine(x);
        }
    }
}

そこで、よく以下のような書き方をします。

using System;
using System.Collections.Generic;

static class MyEnumerable
{
    public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        // イテレーターではなくなった(イテレーターなのは WhereInternal の方)ので、ちゃんと呼ばれた時点でチェックが走る
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));

        return WhereInternal(source, predicate);
    }

    private static IEnumerable<T> WhereInternal<T>(IEnumerable<T> source, Func<T, bool> predicate)
    {
        foreach (var x in source)
            if (predicate(x))
                yield return x;
    }
}

こういう場面こそ、ローカル関数の出番です。 以下のように書き直すことができます。

using System;
using System.Collections.Generic;

static class MyEnumerable
{
    public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        // イテレーターではなくなった(イテレーターなのは WhereInternal の方)ので、ちゃんと呼ばれた時点でチェックが走る
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));

        IEnumerable<T> f()
        {
            foreach (var x in source)
                if (predicate(x))
                    yield return x;
        }

        return f();
    }
}

例2: イテレーターをToArrayしてから返す

イテレーターを使って書きたいものの、 遅延実行(foreachで列挙されて初めて実行される)ではなく即座に実行するために、ToArrayメソッド(System.Enumerableクラスの拡張メソッド)を掛けてから返したい場合があります。

この場合も、1つのメソッドからしか呼ばれないメソッドが作られがちです。 例えば以下のようなコードになります。

using System.Collections.Generic;
using System.Linq;

static class MyEnumerable
{
    public static U[] SelectToArray<T, U>(this T[] array, Func<T, U> selector)
    {
        return Select(array, selector).ToArray();
    }

    // SelectToArray からしか呼ばれない
    private static IEnumerable<U> Select<T, U>(IEnumerable<T> source, Func<T, U> selector)
    {
        foreach (var x in source)
            yield return selector(x);
    }
}

これも、以下のように書き直せます。

using System.Collections.Generic;
using System.Linq;

static class MyEnumerable
{
    public static U[] SelectToArray<T, U>(this T[] array, Func<T, U> selector)
    {
        IEnumerable<U> inner()
        {
            foreach (var x in array)
                yield return selector(x);
        }

        return inner().ToArray();
    }
}

例3: 非同期メソッドのキャッシュ

最後の例は、非同期メソッドで作ったTaskのキャッシュです。

非同期メソッドを呼び出すと、呼び出すたびにTaskクラス(System.Threading.Tasks名前空間)のインスタンスが作られます。 しかし、これを、1度だけ呼んで、2度目以降はキャッシュして持っているTaskを返したいことがあります。

static async Task MainAsync()
{
    // 何度か呼ぶけども、キャッシュされているので通信は1回きり
    Console.WriteLine(await LoadAsync());
    Console.WriteLine(await LoadAsync());
    Console.WriteLine(await LoadAsync());
}

static Task<string> LoadAsync()
{
    _loadCache = _loadCache ?? LoadAsyncInternal();
    return _loadCache;
}
static Task<string> _loadCache;

static async Task<string> LoadAsyncInternal()
{
    var c = new HttpClient();
    var res = await c.GetAsync("http://ufcpp.net");
    var content = await res.Content.ReadAsStringAsync();

    return Regex.Match(content, @"\<title\>(.*?)\<").Groups[1].Value;
}

これも、以下のように書き直せます。

static Task<string> LoadAsync()
{
    async Task<string> inner()
    {
        var c = new HttpClient();
        var res = await c.GetAsync("http://ufcpp.net");
        var content = await res.Content.ReadAsStringAsync();

        return Regex.Match(content, @"\<title\>(.*?)\<").Groups[1].Value;
    }

    _loadCache = _loadCache ?? inner();
    return _loadCache;
}
static Task<string> _loadCache;

匿名関数

Ver. 2.0
Ver. 3.0

C# 2.0では匿名メソッド式、C# 3.0ではラムダ式という構文が入り、これらを合わせて匿名関数と呼びます。

(ラムダ式はほぼ匿名メソッド式の上位互換です。 C#開発者も、「ラムダ式が最初からあれば、匿名メソッド式の構文はC#には不要だった」と言っています。 そのため、匿名メソッド式はC# 2.0時代の互換性を保つためだけの機能だと考えて差し支えないです。 本節でも、以下の説明はラムダ式でのみ行います。)

匿名関数(ラムダ式)は、以下の例のように、引数リストと関数本体を =>でつないで書きます。

(int x) =>
{
    var sum = 0;
    for (int i = 0; i < x; i++)
        sum += i;
    return sum;
}

この例を見ての通り、関数名がありません。これが「匿名」と呼ばれる理由です。

=> は、矢印のように見えることからアロー演算子(arrow operator)と呼ばれたり、 その矢印を「行先」に見立ててgoes to演算子と呼ばれたりします。 (実際、x => 2 * xを x goes to 2x (xが2xに行く)と読むと、英語的に案外しっくり来るそうです。)

=> の後ろの関数本体の部分は、式が1つだけの場合、{}returnを省略して、以下のように書くことができます。

(int x) => x * x

また、=>の前の引数リストでは、引数の型を推論できる場合には型を省略できます。 このとき、引数が1つだけであれば、()も省略できます。

(x, y) => x * y
x => x * x

例えば、以下のような使い方ができます。

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        var input = new[] { 1, 2, 3, 4, 5 };
        var output = input
            .Where(n => n > 3)
            .Select(n => n * n);

        foreach (var x in output)
        {
            Console.WriteLine(x);
        }
    }
}

強調表示している部分が匿名関数です。 匿名関数の引数(n)の型は、渡す先(WhereSelect)から推論されます。

ローカル関数と匿名関数のそれぞれの利点

前節の例のように、匿名関数は式(この例ではWhereメソッドやSelectメソッドの引数)の中に書くことができます。 ここがローカル関数との最大の違いになります。 ローカル関数の場合は、関数(この場合Mainメソッド)直下にしか書けません。

匿名関数はどこにでも書けるという利点がある一方で、以下のような制限があります。

  • 再帰呼び出しが素直にはできない
  • イテレーターにできない
  • ジェネリックにできない
  • 引数の規定値を持てない
// ローカル関数は素直に再帰を書ける
int f1(int n) => n >= 1 ? n * f1(n - 1) : 1;

// 匿名関数はひと手間必要
Func<int, int> f2 = null;
f2 = n => n >= 1 ? n * f2(n - 1) : 1;
// ローカル関数ならイテレーターにできる
IEnumerable<int> g1(IEnumerable<int> items)
{
    foreach (var x in items)
        yield return 2 * x;
}

// 匿名関数ではコンパイル エラー
Func<IEnumerable<int>, IEnumerable<int>> g2 = items =>
{
    foreach (var x in items)
        yield return 2 * x;
}
// ローカル関数ならジェネリックに使える
bool eq1<T>(T x, T y) where T : IComparable<T> => x.CompareTo(y) == 0;
Console.WriteLine(eq1(1, 2));
Console.WriteLine(eq1("aaa", "aaa"));

// 匿名関数はジェネリックにならない
// Func<T, T, bool> の時点でコンパイル エラー
// where 制約を付ける構文もない
Func<T, T, bool> eq2 = (x, y) => x.CompareTo(y) == 0;
// 当然、呼べない
Console.WriteLine(eq2(1, 2));
Console.WriteLine(eq2("aaa", "aaa"));
// ローカル関数の引数には規定値を与えられる
int f1(int n = 0) => 2 * n;
Console.WriteLine(f1());
Console.WriteLine(f1(5));

// 匿名関数は無理
// この時点でコンパイル エラー
Func<int, int> f2 = (int n = 0) => 2 * n;
// 当然、呼べない
Console.WriteLine(f2());
Console.WriteLine(f2(5));

すなわち、以下のことが言えます。

  • ローカル関数は書ける場所が限られるものの、機能的には通常のメソッドと同程度に何でも書ける
  • 逆に、匿名関数はどこにでも書ける代わりに、いくつか機能的に制限がある

また、詳しくは「[雑記] 匿名関数のコンパイル結果」で説明しますが、 多少、実行性能にも差があります。 呼び出し方次第ではありますが、ローカル関数の方が高速になる場合があります。

余談: 経緯

ちなみに、C# 7でローカル関数が導入されるに至った経緯としては、匿名関数の制限を緩和してほしいという要望から始まっています。 すなわち、前述の、「匿名関数はイテレーター化できない、再帰呼び出しが大変」という問題の解決策がローカル関数です。

書ける場所にも違いがあるので、この要望が完全に満たされたわけではありません。 しかし、「イテレーター化」あるいは「再帰呼び出し」をしたい場面を改めて考えてみたところ、 「別に式中に書きたいわけじゃない」、「ローカル関数で十分」、「ローカル関数の方が実行性能的にお得になる場面もある」となったみたいです。

ローカル変数の捕獲

ローカル関数でも匿名関数でも、周りの(定義している関数内の)ローカル変数や引数を取り込んで使うことができます。 例えば以下のようなコードが書けます。

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        // ユーザーからの入力をローカル変数に記録
        var m = int.Parse(Console.ReadLine());
        var n = int.Parse(Console.ReadLine());

        var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        // ユーザーの入力 m よりも大きいか判定
        bool filter(int x) => x > m;

        var output = input
            .Where(filter)
            .Select(x => n * x); // ユーザーの入力 n を掛ける

        foreach (var x in output)
        {
            Console.WriteLine(x);
        }
    }
}

こういう処理を、ローカル変数の捕獲(capture)と言います(カタカナ言葉で「キャプチャする」ともよく言います)。 また、ローカル変数を捕獲しているローカル関数や匿名関数をクロージャ(closure: 囲い込み)と呼んだりします。

捕獲したローカル変数は書き換えることもできます。

using System;

class Program
{
    static void Main()
    {
        var x = 1;

        // ローカル関数内で変数xを書き換え
        void f(int n) => x = n;

        Console.WriteLine(x); // 1

        f(2);
        Console.WriteLine(x); // 2
    }
}

注意点として、詳しくは「[雑記] 匿名関数のコンパイル結果」で説明しますが、 ローカル変数の取り込みには少々ペナルティがかかります。 実行性能への要求が極めて高い場合には、避けれるなら避けるべきです (ペナルティは小さいので、ボトルネックになっていない場所でまで無理に頑張る必要はありません)。


Viewing all articles
Browse latest Browse all 75

Trending Articles