xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

ローカル関数の使いどころ - LINQ 拡張 -

C# 7 で搭載予定のローカル関数。その詳細は以下を参照いただくとして、どういうケースで使えそうかと考えていたら「あ、そうか LINQ だ!」と思ったので紹介します。

yield (遅延評価) の罠

と書くだけで察しの良い方はお気付きかと思いますが、yield を含むメソッドはその機能ゆえに若干クセがあります。以下をご覧いただければ分かるように、例外が飛ぶタイミングがメソッド呼び出し時ではありません

public static void Main()
{
    var xs = CreateEnumerable();  //--- 遅延評価されるのでここでは例外が飛ばない
    var x  = xs.First();  //--- ここで初めて最初の yield までが評価されて例外が飛ぶ
}

public static IEnumerable<int> CreateEnumerable()
{
    throw new Exception("m9(^Д^)");
    yield return 123;
}

これまでのオレオレ LINQ 拡張での書き方

通常、メソッドの実装をする際には引数チェックを行います。LINQ のオレオレ拡張を作っている際も例外ではないので、何も考えなければ以下のようなコードを書くでしょう。

public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    if (source == null)   throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));

    foreach (var x in source)
        yield return selector(x);
}

しかしこの書き方は先の遅延評価の罠に引っかかり、実際の評価時まで引数チェックが働きません。引数チェックだけはメソッド呼び出し時に実行されてほしいので、以下のようにメソッドを分離して実装するのが定石になっていました。

public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    //--- 引数チェックだけするメソッド
    if (source == null)   throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));
    return source.MySelectCore(selector);
}

private static IEnumerable<TResult> MySelectCore<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    //--- yield だけするメソッド
    foreach (var x in source)
        yield return selector(x);
}

オレオレ LINQ 拡張へのローカル関数の適用

C# 7 で導入されるローカル関数は yield を使うことができるため、以下のようにメソッドを分離する必要がなくなります。非常にスッキリ!

public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    //--- 引数チェック
    if (source == null)   throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));

    //--- ローカル関数の定義
    //--- パフォーマンス改善がなされたクロージャで引数を渡しつつ...
    IEnumerable<TResult> core()
    {
        foreach (var x in source)
            yield return selector(x);
    }

    //--- 実行!!
    return core();
}

ref 戻り値 / ref ローカル変数

今回は ref 戻り値 (Ref Returns)ref ローカル変数 (Ref Locals) について見ていきます。それぞれの機能は端的に言うと以下のようなものです。

名称 機能
Ref Returns メソッドの戻り値を参照として返す
Ref Locals 値を参照として受ける

C++ に嗜みがある方であれば & キーワード による参照渡しと言うと分かりやすいかもしれません。むしろ C++er からすれば圧倒的なイマサラ感すら沸き上がるかもしれません...。この機能は GitHub 上の以下の issues で提案/議論されています。

パフォーマンスの向上

基本的に値型でですが、引数に渡したり戻り値として値を返す場合はメモリ領域が再確保され、そこにコピーが生成されます。以下のような超シンプルなコードでも、実は値が 3 回コピーされています。(確かそのはず...

static void Main()
{
    var a = 123;  //--- 1. メモリ領域確保
    var d = PassThrough(a);  //--- 4. 戻されたのを受けとるときにもう一回コピー
}

static int PassThrough(int b)  //--- 2. 引数として渡すのに値をコピー
{
    int c = b;  //--- 3. 別の変数として受ける場合もコピー
    return c;
}

つまり、上記で言うと変数 a / b / c / d はすべてメモリ上の別領域に値を持っています。int 型のような軽量なものであれば特に気にならないのかもしれませんが、サイズのデカい値型となるとコピー (とその領域のガベージコレクションも?) による影響が無視できなくなります。究極的にパフォーマンスを高く持つためには、値のコピー (とメモリ領域の解放も?) の頻度/回数を減らす必要があります。

これをサポートする機能としてこれまでも ref 引数 (参照渡し)が搭載されていましたが、ローカル変数として参照の形で値を受けたり、参照のまま戻り値として返す (= 参照返し) は提供されていませんでした。今回追加される ref 戻り値 / ref ローカル変数で、そのような足りなかったパーツが補われます。

安全性

実は、これまでの C# 6.0 まででもアンセーフコードという機能を使うことで同様のことを実現できました。通常 C# ではポインタは使えないのですが、プロジェクトのプロパティで [アンセーフコードの許可] を設定することでポインタが利用できるようになります。しかし、C# においてはポインタを直で触ることは推奨されません。詳しくは以下に書かれているので参考にしてください。

ref 戻り値 / ref ローカル変数ではアンセーフコードを有効化することなく、安全に参照渡しを実現することができます。

使い方検証

以下のサンプルは「配列の要素を書き換える」というのをヒジョーに回りくどくやってみたものです。アチコチに ref が入ってだいぶ気持ち悪いですが、こんな風に書くんだというところだけ掴んでいただければと思います。

static void Main()
{
    var a = new int[] { 0, 1, 2, 3, 4 };
    Console.WriteLine(string.Join(",", a));
    // 0, 1, 2, 3, 4

    ref var d = ref GetValue(a);  //--- 参照返しを変数 d で受ける
    d = 5;                        //--- 書き換え
    Console.WriteLine(string.Join(",", a));
    // 0, 1, 5, 3, 4
}

static ref int GetValue(int[] b)
{
    ref var c = ref b[2];  //--- b の 3 番目の要素を参照する変数 c を作る
    return ref c;          //--- 変数 c の参照先を返す
}

ローカル変数の参照返しはダメ

ローカル変数として定義された値を参照返ししようとするとコンパイルエラーになります。スコープを越えてガベージコレクションの対象となったメモリ領域を参照していることは危険しかないので、シッカリ防いでくれます。

static ref int GetValue()
{
    var a = 123;
    return ref a;  //--- コンパイルエラー
}

逆に言うと、フィールド / プロパティ / 呼び出し元のローカル変数など関数内で定義されたものを対象しないのであれば、ref 戻り値や ref ローカル変数を利用することができます。

ref 引数を返す

ref 引数は参照渡しなので、その値をそのまま戻すことは可能です。

static ref int GetValue(ref int a, ref int b)
{
    if (a > b)
        return ref a;
    return ref b;
}

条件演算子 (三項演算子) が使えない

仕様なのかバグなのかは定かではありませんが、上記のサンプルに対して条件演算子を使ってみるとコンパイルエラーになります (謎ぃ...

static ref int GetValue(ref int a, ref int b)
    => (a > b) ? ref a : ref b;  //--- コンパイルエラー

プロパティで参照返し

参照返しはメソッドだけでなくプロパティにも適用できます。ただし自動実装プロパティに対しては適用できません。

class Person
{
    //--- OK! setter はないけど直接書き換え可能
    public ref string Name => ref this.name;
    private string name = "Anders";

    //--- NG! 自動実装プロパティの参照渡しはできない
    //public ref string Name { get; }
}

static void Main()
{
    var p = new Person();
    Console.WriteLine(p.Name);  //--- Anders
    p.Name = "xin9le";
    Console.WriteLine(p.Name);  //--- xin9le
}

インデクサで参照返し

参照返しのインデクサを作ることで、あたかも配列のように扱うこともできます。

class ReadOnlyIntArray
{
    private int[] Source { get; }
    public ref int this[int index] => ref this.Source[index]; 
    public ReadOnlyIntArray(int[] source)
    {
        this.Source = source;
    }
}

static void Main()
{
    var a = new int[] { 0, 1, 2, 3, 4 };
    var b = new ReadOnlyIntArray(a);
    Console.WriteLine(b[2]);  //--- 2
    b[2] = 5;                 //--- ref がない場合はコンパイルエラー
    Console.WriteLine(b[2]);  //--- 5
}

逆コンパイル

どのように実現されているかを知るために逆コンパイルをしてみましょう!今回は先の例でも利用した以下のサンプルを使います。

using System;

namespace RefReturnsRefLocals
{
    class Program
    {
        static void Main()
        {
            var a = new int[] { 0, 1, 2, 3, 4 };
            Console.WriteLine(string.Join(",", a));

            ref var d = ref GetValue(a);
            d = 5;
            Console.WriteLine(string.Join(",", a));
        }

        static ref int GetValue(int[] b)
        {
            ref var c = ref b[2];
            return ref c;
        }
    }
}

逆コンパイル結果が以下です。unsafe コードとして展開されていますがどうやらこれは ILSpy の誤判定っぽく、IL を直接のぞくと int& (= 参照渡し) として展開されていることが分かります。IL レベルとしてはずっと以前から参照返しをサポートしていましたが、C# 側での制限として実装されていなかったようですね。

using System;

namespace RefReturnsRefLocals
{
    internal class Program
    {
        private unsafe static void Main()
        {
            int[] a = new int[]{ 0, 1, 2, 3, 4 };
            Console.WriteLine(string.Join<int>(",", a));
            int* d = Program.GetValue(a);
            *d = 5;
            Console.WriteLine(string.Join<int>(",", a));
        }

        //--- unsafe コードとして展開されているように見えるけど...
        private unsafe static int* GetValue(int[] b)
            => ref b[2];
    }
}
.method private hidebysig static 
    int32& GetValue (  //--- IL では & キーワードによる参照渡しで出力されている
        int32[] b
    ) cil managed 
{
    .maxstack 2
    .locals init (
        [0] int32& c,
        [1] int32&
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.2
    IL_0003: ldelema [mscorlib]System.Int32
    IL_0008: stloc.0
    IL_0009: ldloc.0
    IL_000a: stloc.1
    IL_000b: br.s IL_000d

    IL_000d: ldloc.1
    IL_000e: ret
}

ローカル関数

コードを書いていると、稀に関数の中に「そこでしか使わない関数」を作りたくなる場合があります。例えば再帰処理などはその最たるものかと思います。C# 6.0 まではデリゲートを駆使して以下のように書いていました。

static void Main()
{
    //--- 再帰処理したい場合は一旦変数を切る
    //--- 変数をキャプチャするためのテクニック
    Func<int, int> fibonacci = null;
    fibonacci = x =>
    {
        if (x <= 1)
            return x;
        return fibonacci(x - 1) + fibonacci(x - 2);
    }
    var result = fibonacci(7);
}

書けますし問題なく動作しますが、いちいちデリゲートのインスタンスを作らなければなりません。呼び出し以外のコスト (= インスタンス生成コスト) がかかっていることが分かります。C# 7 からはこれらデリゲートで実現することで起こる諸所の問題を解決し、用途に特化した「ローカル関数」という機能が追加される見込みで、以下のような感じで書けるようになります。

static void Main()
{
    //--- ローカル関数を利用した再帰呼び出し
    int fibonacci(int x)
    {
        if (x <= 1)
            return x;
        return fibonacci(x - 1) + fibonacci(x - 2);
    }
    var r1 = fibonacci(7);

    //--- もちろん Expression-Bodied な書き方も OK
    string asExpressionBodied(int x) => (x * x).ToString();
    var r2 = asExpressionBodied(456);
}

検証内容

ローカル関数は基本的にローカル変数と同様のルールに則った挙動をします。以降、できること/できないことをアレコレ検証してみたので紹介していきます。

アクセシビリティ

関数内でしか利用できないものなので (そもそも) アクセシビリティの設定はできません。

void Foo()
{
    //--- コンパイルエラー!
    //--- public とか private みたいな設定はできない
    public int bar() => 123;
}

静的関数

static キーワードを付けたローカル関数は作れません。なので以下はコンパイルエラーになります。

void Foo()
{
    //--- これもできない
    static int bar() => 123;
}

非同期メソッド (async/await)

async/await の構文は問題なく利用できます。

void Foo()
{
    async Task<int> bar()
    {
        await Task.Delay(1000);
        return 123;
    }
    var result = bar().Result;
}

ジェネリック

Func<T>Action<T> を使用していた場合、ジェネリック関数にするためには親となる関数自体もジェネリックにして型引数を利用する必要がありました。例えば以下のような感じです。

//--- こうやって書いても結局使い物にならない
void Foo<T>()
    where T : struct
{
    Func<T, string> bar = x => x.ToString();

  //var result = bar(123);   //--- int    から T に変換できないのでコンパイルエラー
  //var result = bar(12.3);  //--- double から T に変換できないのでコンパイルエラー
    var result = bar(default(T));  //--- これなら OK
}

関数 Foo をジェネリックにしたいわけではなくてもそうせざるを得ませんでした。しかし、ローカル関数を利用すれば親の関数をジェネリック化することなくジェネリック関数を作ることができるようになります。これは嬉しい!

//--- 親の関数をジェネリック化する必要がない
void Foo()
{
    //--- 型制約も当然使える
    string bar<T>(T x)
        where T : struct
        => x.ToString();

    var r1 = bar(123);   //--- T を int    として解釈させる
    var r2 = bar(12.3);  //--- T を double として解釈させる
}

イテレータ (yield)

ラムダ式などの匿名関数内では yield が使えない制約があり、yield を使いたい場合はクラス/構造体のメンバーとして private メソッドを作成する必要がありました。しかし、ローカル関数ではそんな制約もなくなります

void Foo()
{
    IEnumerable<string> getFruits()
    {
        yield return "Apple";
        yield return "Orange";
        yield return "Banana";
    }

    foreach (var x in getFruits())
        Console.WriteLine(x);
}

スコープ

冒頭でも説明しましたが、ローカル関数はローカル変数と同じようなルールが適用されます。なので、呼び出しのスコープも以下のようになります。

static void Main()
{
    string foo(object x) => x.ToString();

    {
        string foo(object x) => x.ToString();  //--- 親階層に同名のローカル関数があるからダメ
        string bar(object x) => x.ToString();
        var r1 = foo(123);  //--- OK
        var r2 = bar(123);  //--- OK
    }

    var r3 = foo(123);  //--- OK
    var r4 = bar(123);  //--- スコープ範囲外によりコンパイルエラー!
}

ネスト

匿名関数の中で匿名関数を作れるように、ローカル関数もネストすることができます

static void Main()
{
    //--- ローカル関数
    string foo()
    {
        //--- 入れ子になったローカル関数
        int bar(int x) => x * x;
        return bar(3).ToString();
    }
    var result = foo();
}

変数キャプチャ (= クロージャ)

ローカル関数にはクロージャの機能があり、関数定義よりも先にある変数をキャプチャすることができます。

static void Main()
{
    var age = 31;
    void foo(string name)
    {
        Console.WriteLine(name);  //--- xin9le
        Console.WriteLine(age);   //--- 31
    }
    foo("xin9le");
}

デリゲートとして扱う

ローカル関数もやはり関数なのでデリゲートとして扱うことができます。

static void Main()
{
    var foo = GetFoo();
    foo("xin9le");  //--- ローカル関数呼び出し
}

static Action<string> GetFoo()
{
    var age = 31;
    void foo(string name)
    {
        Console.WriteLine(name);
        Console.WriteLine(age);
    }
    return foo;  //--- デリゲートとして返す
}

定義箇所

先の例では、ローカル関数を定義している箇所は呼び出し箇所よりも前にしていました。ローカル関数が実装された当初は「呼び出し元よりも前」でなければならないという制約があったのですが、2016/08/26 に入ったコミットでその制約が緩和されました。

これにより以下のようにも書くことができます。

static void Main()
{
    Local();
    void Local(){}  //--- 下に書いてもいいよ
}

逆コンパイル

では最後に、ローカル関数がどのように実現されているのかコンパイル結果を覗いてみましょう!利用するコードは上記 (変数キャプチャ) の節でも利用した以下のコード。

using System;

namespace LocalFunctions
{
    class Program
    {
        static void Main()
        {
            var age = 31;
            void foo(string name)
            {
                Console.WriteLine(name);  //--- xin9le
                Console.WriteLine(age);   //--- 31
            }
            var dummy = 123;
            foo("xin9le");
            Console.WriteLine(dummy);
        }
    }
}

コンパイル結果を ILSpy.NET Reflector で覗いてみると以下のように展開されていることが分かります。

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace LocalFunctions
{
    internal class Program
    {
        [CompilerGenerated]
        [StructLayout(LayoutKind.Auto)]
        private static struct <>c__DisplayClass0_0
        {
            public int age;
        }

        [CompilerGenerated]
        internal static void <Main>g__foo0_0(string name, ref Program.<>c__DisplayClass0_0 ptr)
        {
            Console.WriteLine(name);
            Console.WriteLine(ptr.age);
        }

        private static void Main()
        {
            Program.<>c__DisplayClass0_0 <>c__DisplayClass0_ = default(Program.<>c__DisplayClass0_0);
            <>c__DisplayClass0_.age = 31;
            int dummy = 123;
            Program.<Main>g__foo0_0("xin9le", ref <>c__DisplayClass0_);
            Console.WriteLine(dummy);
        }
    }
}

非常に読みにくい上にコンパイルすら通らないので、同じような意味になるように書き換えてみました。

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace LocalFunctions
{
    internal class Program
    {
        [CompilerGenerated]
        [StructLayout(LayoutKind.Auto)]
        private struct ClosureHelper
        {
            public int age;
        }

        [CompilerGenerated]
        private static void foo(string name, ref ClosureHelper obj)
        {
            Console.WriteLine(name);
            Console.WriteLine(obj.age);
        }

        private static void Main()
        {
            var obj = default(ClosureHelper);
            obj.age = 31;
            int dummy = 123;
            foo("xin9le", ref obj);
            Console.WriteLine(dummy);
        }
    }
}

ここからローカル関数は以下のように実現されていることが読み取れます。コンパイラさん、だいぶ頑張ってるw

  • ローカル関数はメンバー関数に昇格する (= 普通の関数になる)
  • ローカル関数名は他とは重複しない形で生成される
  • メンバー関数として生成されるのでデリゲートは生成されない
  • ローカル関数にキャプチャされているローカル変数を持つ構造体が生成される
  • 構造体のインスタンスは ref 引数の形で渡される (= 参照渡し)
  • ref 引数はローカル関数の引数の末尾に自動で追加される
  • ローカル関数にキャプチャされていない変数は構造体に含まれない

これまでの匿名関数の形では、キャプチャされたローカル変数がクラスとして展開されていたためヒープ領域でのメモリ確保が必ず行われていました。ローカル関数の場合、ローカル変数が構造体の参照渡しになってヒープ領域へのメモリアロケーションがなくなる (= スタック領域しか使わない) ため、パフォーマンスの向上が見込めます

名前の通り、ローカル変数としての規約と関数としての動きをそのままミックスした感じですね :)

おまけ : ローカル変数をキャプチャしているローカル関数のデリゲート

ローカル関数ではローカル変数をキャプチャすると 構造体 + 自動的にメソッド末尾に追加された ref 引数になるのでした。しかし、コンパイラの都合で自動的に末尾に引数を追加されてはデリゲートを作ることができなくなってしまいます。

using System;

namespace LocalFunctions
{
    class Program
    {
        static void Main()
        {
            var foo = GetFoo();
            foo("xin9le");
        }

        static Action<string> GetFoo()
        {
            var age = 31;
            void foo(string name)
            {
                Console.WriteLine(name);
                Console.WriteLine(age);
            }
            return foo;
        }
    }
}

なのでそういう問題が起こらないよう、ローカル変数をキャプチャしているローカル関数をデリゲート化する場合は構造体 + ref 引数ではなく、クラスとして展開されるようになります。よくできていますね!

using System;
using System.Runtime.CompilerServices;

namespace LocalFunctions
{
    internal class Program
    {
        //--- クラスが自動生成される
        [CompilerGenerated]
        private sealed class ClosureHelper
        {
            public int age;
            internal void foo(string name)  //--- ref 引数なしのメンバー関数になる
            {
                Console.WriteLine(name);
                Console.WriteLine(this.age);
            }
        }

        private static void Main()
        {
            var foo = GetFoo();
            foo("xin9le");
        }

        private static Action<string> GetFoo()
        {
            var helper = new ClosureHelper();
            helper.age = 31;
            return helper.foo;
        }
    }
}

数値セパレーター

C# 6.0 までの数値リテラルでは、桁数がどれだけになっても区切り文字を入れることができませんでした。今回 C# 7 で数値セパレーター (= Digit Separators) という機能が追加され、数値リテラルを自由に桁区切りしてグルーピングできるようになりました。

区切り文字として利用できるものは _ のみ。特に前回紹介した 2 進リテラルと一緒に使うと便利そうです。

var bin  = 0b1100_1010;  //--- 4 ビットずつ
var dec  = 12_345_678;   //--- 通貨などでよくある 3 桁区切り
var hex  = 0x33_ff_cc;   //--- RGB で区切ってみたり

詰まるところ _ は表記上の完全なオマケで、解釈としては _ がないものと同一になります。なので、例えば以下のふたつは同じです。

double real = 1_000.123_4e-3_0;  //--- だいぶ読みづらいけど
double real = 1000.1234e-30;     //--- これと同じ

いくつ入れても OK

ザッと試してみたところ、以下のようにセパレーターをいくつ並べても大丈夫でした。やり過ぎる意味はないですが、だいぶ自由にできそうです。

var bin = 0b1_0__1___0____0_____1;

先頭と末尾に入れるのはダメ

セパレーターの個数には自由度がありますが、入れる場所には制限がありました。数値の先頭と末尾には入れられません。ちゃんと「区切り」なさいということですね。

//--- 先頭はダメ
var bin = 0b___1010;
var dec = ___12_345;
var hex = 0x___1f2d;

//--- 末尾もダメ
var bin = 0b1010_0011___;
var dec = 123_456___m;  //--- decimal などの接尾辞を入れてもダメ
var hex = 0x11_ff___;

2 進リテラル

すでに当たり前のように使っている機能ですが、C# 6.0 までは整数リテラル (整数値を表記する方法) として 10 進数と 16 進数がサポートされています。10 進数はそのまま数字を並べるだけ、16 進数は数値の前に 0x / 0X の接頭辞を付けます。

var dec = 123;   //--- 10 進数
var hex = 0x1F;  //--- 16 進数

C# 7 からはこれらに加えて 2 進数表記 (= Binary literals) も追加されます。接頭辞は 0b / 0B で、Binary の意味ですね。以下のように記述できるようになります。

var bin = 0b1011;  //--- 2 進数

利用できる数値は 0 / 1 のみです。仕様は以下に載っています。

整数リテラルは記法の問題だけなので、コンパイル時にはただの整数値として IL に埋め込まれます。2 進数表記をしたから遅くなるとかいうことは一切ありません。

こんなときに便利

よくある例がビット演算をする場合です。Flags 属性を付けた enum を作ることがあるかと思いますが、これまでは愚直に 10 進数で値を並べるかビットシフトを使って以下のように書いていました。

//--- そのまま 10 進数で値を並べる
[Flags]
enum Fruits
{
    None   = 0,
    Dog    = 1,
    Cat    = 2,
    Bird   = 4,
    Rabbit = 8,
    Other  = 16,
}


//--- シフト演算で何ビット目に 1 を立てているかを明示
[Flags]
enum Fruits
{
    None   = 0,
    Dog    = 1 << 0,  //--- 0 ビット左にシフトしたところに 1 を立てる
    Cat    = 1 << 1,
    Bird   = 1 << 2,
    Rabbit = 1 << 3,
    Other  = 1 << 4,
}

これらをより明確に記述できるようになります。

[Flags]
enum Fruits
{
    None   = 0b00000,
    Dog    = 0b00001,
    Cat    = 0b00010,
    Bird   = 0b00100,
    Rabbit = 0b01000,
    Other  = 0b10000,
}

他にも、ビットマスクをするときなどに重宝しそうです。