xin9le.net

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

タプル構文 - 多値戻り値のサポート

C# 7 では言語機能としてタプル (複数の値をまとめる) 構文 がサポートされそうです。これまでも複数の値を簡易的にパッケージングする機能として System.Tuple<T1, T2, ...> が提供されていましたが、より可読性が高く、パフォーマンスが良くなる形になる見込みです。例えば、単純なものだと以下のようになります。

(string, int, 性別) GetUserInfo()
{
    var tuple = ("xin9le", 31, 性別.男);
    return tuple;
}

タプル構文については以下で議論がまとめられています。

f:id:xin9le:20160511025349p:plain

System.ValueTuple 型の提供

これまで複数の値/型をまとめる汎用型としては System.Tuple 型が提供されていました。これは参照型としての表現なのでインスタンスを作るたびにヒープ領域を使用します。C# 7 ではより高い性能を出すことにも注力しているようで、タプル型も値型としての表現が追加される予定です。現時点ですでに CoreFx リポジトリの master ブランチに System.ValueTuple 型が追加されています。

この ValueTuple 型によって、特に後述の「多値戻り値」として利用する場合のパフォーマンス向上が見込まれています。また C# 7 関連のパフォーマンス改善については以下が大変勉強になります。オススメ!

タプル構文は ValueTuple 型の糖衣構文

C# 7 で追加予定のタプル構文は、先に紹介した ValueTuple 型の糖衣構文として提供されそうです。なので以下は完全に同一のコードとして展開されます

//--- こう書いたものは
var t1 = ("xin9le", 31, 性別.男);
var t2 = new ValueTuple<string, int, 性別>("xin9le", 31, 性別.男);

//--- 逆コンパイルするとこうなる
var t1 = new ValueTuple<string, int, 性別>("xin9le", 31, 性別.男);
var t2 = new ValueTuple<string, int, 性別>("xin9le", 31, 性別.男);

また、戻り値も同様です。

//--- これは
(string, int, 性別) GetUserInfo()
    => ("xin9le", 31, 性別.男);

//--- こう展開される
ValueTuple<string, int, 性別> GetUserInfo()
    => new ValueTuple<string, int, 性別>("xin9le", 31, 性別.男);

つまり、タプル構文は ValueTuple 型の表記を省略するショートカット記法ということです。

多値戻り値としての表現

みなさんご存じの通り、プログラミング言語には関数という処理を行う機能単位があります。何か入力 (引数) を与えたら何か結果 (戻り値) が返ってくる、そんなブラックボックスなイメージのシステムです。以下に C# における引数と戻り値の関係をまとめてみます。

入力 出力 書き方
0 0 void Func();
1 0 void Func(T x);
N 0 void Func(T1 x1, T2 x2, ...);
0 1 TResult Func();
1 1 TResult Func(T x);
N 1 TResult Func(T1 x1, T2 x2, ...);
0 N 未提供
1 N 未提供
N N 未提供

Go 言語など複数の戻り値をサポートしているものもありますが、C# では戻り値はひとつという制限があります。ではこれまではどうしていたのかというと、以下の 2 つの方法でした。

  • Tuple 型などのクラス/構造体に詰めて返す
  • ref/out を使って引数で返す

多値戻り値が言語機能として提供されていなくても、それを代替する手法があるので何とかなっています。しかし、やはりそうは言っても「複数の値を返しているように書きたい」と思うのが開発者の人情ってもんです (たぶん。そこで C# 7 からはタプル構文を利用した擬似的な多値戻り値をサポートします。

ではなぜ「擬似的」かと言うと、先ほど説明した通りタプル構文の実態が ValueTuple 型の糖衣構文だからです。実際はひとつの型しか返していません。ですが、戻り値の書き味を見ると複数の値を戻しているように見えます

//--- これだとひとつの値を返しているように見えるけれど
ValueTuple<string, int, 性別> GetUserInfo()
{
    var name = "xin9le";
    var age = 31;
    var sex = 性別.男;
    return ValueTuple.Create(name, age, sex);
}

//--- これだと戻り値が複数あるように見える!(∩´∀`)∩
(string, int, 性別) GetUserInfo()
{
    var name = "xin9le";
    var age = 31;
    var sex = 性別.男;
    return (name, age, sex);
]

値へのアクセスとエイリアス

タプル構文の実態は ValueTuple 型なので、格納した値へのアクセスは以下のようになります。

var t = ("xin9le", 31, 性別.男);
Console.WriteLine(t.Item1);  //--- xin9le
Console.WriteLine(t.Item2);  //--- 31
Console.WriteLine(t.Item3);  //--- 男

...なのですが、これはあまりにも無慈悲な状況です。もちろんこれまでも同じような問題はありました。

  1. 複数の値を返すのにイチイチ型を作りたくない!
  2. よし、匿名型だ!
  3. (型推論でしか使えないので) 関数の戻り値にできない!
  4. 仕方ないから Tuple 型だ!
  5. Item1, Item2 だと何を表しているか分からない!
  6. 仕方ないからクラス作るか...(← イマココ

これを解決するため、タプル構文では Item1Item2 などの値へのアクセスに対するエイリアス機能が提供されます

タプル生成時にエイリアスを付与

この場合は以下のように書けます。

//--- エイリアスを付けてアクセス!
var t = (name: "xin9le", age: 31, sex: 性別.男);
Console.WriteLine(t.name);
Console.WriteLine(t.age);
Console.WriteLine(t.sex);

//--- ただのエイリアスなのでもちろん既定のアクセスも可能
Console.WriteLine(t.Item1);
Console.WriteLine(t.Item2);
Console.WriteLine(t.Item3);

関数の戻り値にエイリアスを付与

この場合は以下のように書きます。引数と対になる形で記述できることに注目です!

(int sum, int count) Tally(IEnumerable<int> list)
{
    var s = 0;
    var c = 0;
    foreach (var value in list)
    {
        s += value;
        c++;
    }
    return (s, c);
}

var t = Tally(new []{ 1, 2, 3 });
Console.WriteLine(t.sum);
Console.WriteLine(t.count);

ただし、この機能を使うには TupleElementNamesAttribute が必要です。.NET Core リポジトリの master ブランチにこの属性の実装が追加されています。

戻り値にエイリアスを付けた場合、以下のように属性として展開されます。

//--- こんな風にエイリアスが戻り値についていると
public static (int, int count) Tally(IEnumerable<int> values)
    => (values.Sum(), values.Count());

//--- 戻り値にこんな属性が付与される
[return: TupleElementNames(new []{ null, "count" })]
public static ValueTuple<int, int> Tally(IEnumerable<int> values)
    => new ValueTuple<int, int>(values.Sum(), values.Count<int>());

上記から、エイリアスは第 1 引数から順番に配列として展開されます。名前がない場合は null が埋め込まれます。また、エイリアス名がひとつも付与されていない場合は属性自体展開されません

値を受ける側でエイリアスを設定

ここまで値を返す側でのエイリアス設定について見てきました。このままでも十分便利なのですが、返された値に対して利用側でエイリアスを付けられるともっと使いやすいかもしれません。そんな要望があったからか、受け側でのエイリアス設定機能が追加されています。

//--- 右辺ではエイリアスを設定していないが、受け側である左辺で設定している
(string name, int age) t = ("xin9le", 31);
Console.WriteLine($"{t.name} is {t.age} years old.");

//--- 関数の戻り値でエイリアスが設定されていても変更できる
(int a, int b) t = Tally(new []{ 1, 2, 3 });
Console.WriteLine(t.a);
Console.WriteLine(t.b);

しかし、以下のような型推論を利用した書き方はコンパイルエラーになるので注意が必要です。

//--- こんな風に var を使うのはダメ
(var a, int b) t = Tally(new []{ 1, 2, 3 });

// CS0825 : The contextual keyword 'var' may only appear within a local variable declaration or in script code
// CS0029 : Cannot implicitly convert type '(int sum, int count)' to '(var a, int b)'

また、タプルをネストしていても同様に書くことができます。

(int a, int b, (int c, int d) nested, int e) t = (1, 2, (3, 4), 5); 
Console.WriteLine(t.a);         //--- 1
Console.WriteLine(t.b);         //--- 2
Console.WriteLine(t.nested.c);  //--- 3
Console.WriteLine(t.nested.d);  //--- 4
Console.WriteLine(t.e);         //--- 5

インテリセンスとの連携

現状すでにインテリセンスともシッカリと連携していて、以下のように表示されます。とても分かりやすい :)

f:id:xin9le:20160511020953p:plain

あくまでもエイリアス

ですので、いくらエイリアスを付けてもコンパイル結果は Item1, Item2 のような形に展開されてしまいます。賢いコンパイラが良きに計らってくれているということですね!

//--- これは
var t = (name: "xin9le", age: 31, sex: 性別.男);
Console.WriteLine(t.name);
Console.WriteLine(t.age);
Console.WriteLine(t.sex);

//--- やっぱりこう展開される
var t = new ValueTuple<string, int, 性別>("xin9le", 31, 性別.男);
Console.WriteLine(t.Item1);
Console.WriteLine(t.Item2);
Console.WriteLine(t.Item3);

エイリアス名の制約

大変便利なエイリアス名ですが、付けられる名称にはいくらかの制約があります。まず以下の名称は付けられません (大文字/小文字は完全一致)

  • CompareTo
  • Deconstruct
  • Equals
  • GetHashCode
  • Rest
  • ToString
//--- これはダメ
var t = (Deconstruct: 123, ToString: "abc");

また、エイリアス名でない Item1Item2... などの名称は引数の位置と一致しない限り付けることができません。(こんなコトする人はいないと思いますが...)

//--- これは OK
var t1 = (Item1: 123, "abc");

//--- Item1 は第 1 引数にアクセスするプロパティなので第 2 引数には付けられない
var t2 = (123, Item1: "abc");

エイリアス数の制約

タプルは複数の値をまとめるための機能なのでふたつ以上の値を持つ必要があります。そのため、以下のようにひとつしか値を持たないタプル構文はエラーになります。

//--- これはダメ
var t = (name: "xin9le");

//--- ひとつだけ値を取る ValueTuple 型はないので展開できない
var t = new ValueTuple<string>("xin9le");

エイリアスの有効範囲

タプル構文が実装された当初は、プロジェクトをビルドしてバイナリになった段階でエイリアス名は消失していました。つまり、エイリアスありのタプルを返す便利機能を共通ライブラリとして配布しても利用側ではエイリアスを使用できませんでした。

//--- CoreLib.dll にこんな拡張メソッドがあるとする
public static (int sum, int count) Tally(this IEnumerable<int> list)
{
    var s = 0;
    var c = 0;
    foreach (var value in list)
    {
        s += value;
        c++;
    }
    return (s, c);
}

//--- 当初は App.exe でエイリアスが使えなかった!
var numbers = new []{ 1, 2, 3 };
var t = numbers.Tally();
Console.WriteLine(t.sum);   //--- 当初はコンパイルエラーになっていた!(← 今はできる
Console.WriteLine(t.Item1); //--- これは当然 OK

しかし、TupleElementNamesAttribute が実装されたことによりアセンブリ内にエイリアス名を埋め込むことができるようになったので、そのような制約がなくなりました!(∩´∀`)∩

値の多いタプルへのアクセス

ValueTuple 型の実装を見ると、7 番目までは ItemX プロパティで、8 番目以降は Rest プロパティ (= 残りの部分という意味) でのアクセスを要求されていることが分かります。つまりこんな感じ。

//--- 10 個の値があるとこんな感じ
var t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Console.WriteLine(t.Item1);  //--- 1
Console.WriteLine(t.Item7);  //--- 7
Console.WriteLine(t.Rest);   //--- (8, 9, 10)

//--- 逆コンパイル結果
//--- 最後の 8 つ目以降は ValueTuple 型のネスト構造になる
var t = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int, int>>(1, 2, 3, 4, 5, 6, 7, new ValueTuple<int>(8, 9, 10));

そして実は値にアクセスする際も Rest プロパティを使わずに Item8Item10 のような書き方ができます!

//--- なんと Item プロパティのままアクセスできる!
var t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Console.WriteLine(t.Item1);  //--- 1
Console.WriteLine(t.Item7);  //--- 7
Console.WriteLine(t.Item8);  //--- 8
Console.WriteLine(t.Item10); //--- 10

//--- 逆コンパイル結果
//--- ネストしたタプルへのアクセスに書き換えてくれる
var t = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int, int>>(1, 2, 3, 4, 5, 6, 7, new ValueTuple<int>(8, 9, 10));
Console.WriteLine(t.Item1);       //--- 1
Console.WriteLine(t.Item7);       //--- 7
Console.WriteLine(t.Rest.Item1);  //--- 8
Console.WriteLine(t.Rest.Item3);  //--- 10

上記のようにコンパイラが良きに計らって展開してくれていることが分かります。実際の ValueTuple の実装を気にすることなく気軽にタプル構文を書けるのは素晴らしいですね!

最新の C# コンパイラ を利用してビルド/デバッグしてみた

そんな方法はひとつしかない、リポジトリをクローンしてビルドすることだ

ということでやってみましょう!ちなみにこの記事は 5/9 現在の情報を基に書いています。近い将来に変更になる可能性は多分にありますので、その点ご了承ください m( )m

Step.1 - Roslyn リポジトリをクローン

C# コンパイラ (Roslyn) はオープンソースなコンパイラで、GitHub で公開されています。HTTPS なり SSH なりでクローンしましょう。

f:id:xin9le:20160508231240p:plain

Step.2 - future ブランチに切り替え

執筆時点では C# 7 は鋭意開発中というステータスです。開発中の機能は基本的にそれごとにブランチが切られていますが、できたもの/いいところまで行ったものは順次 future ブランチにマージされていっています。なので、future ブランチに切り替えましょう。

f:id:xin9le:20160509031853p:plain

Step.3 - Roslyn のビルド

続いて Roslyn をビルドします。cibuild.cmd というバッチファイルがルートフォルダに置いてあるのでこれを使います。既定では Debug ビルドの設定になっているので、Release ビルドの設定に変更します。/release の引数を与えて実行するだけです。

C:\roslyn> cibuild /release

長時間 CPU を全力でブン回しながらビルド / 単体テストが行われます。無事完了したら roslyn\Binaries\Release フォルダに VBCSCompiler.execsc.exe ができているはずです。これを使って C# コードをビルドすれば OK でしょう。

Step.4 - C# プロジェクトで Roslyn コンパイラを参照する

プロジェクト単位での設定を書いているので何度も Hello World プロジェクトを作る方には若干メンドクサイですが、設定してしまえばオシマイです。まず通常のコンソールアプリのプロジェクトを作成します。ビルドした Roslyn コンパイラを利用するため、.csproj ファイルを開いて、PropertyGroup 配下に CscToolPath のタグ設定を追加します。

<PropertyGroup>
    <!-- ↓↓ C# コンパイラのあるフォルダを絶対パスで追加 ↓↓ -->
    <CscToolPath>C:\roslyn\Binaries\Release</CscToolPath>

    <!-- 以下いろいろ既定の設定 -->
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
</PropertyGroup>

続いて C# 7 の機能を有効化するため、条件付きコンパイルシンボルとして __DEMO__ を追加します。GUI 上で設定する場合は、以下の記事を参考にしてくださっても OK です。

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <!-- ↓↓ __DEMO__ を追加 ↓↓ -->
    <DefineConstants>TRACE;DEBUG;__DEMO__</DefineConstants>
</PropertyGroup>

何のコンパイルシンボルでどの C# 7 の機能が有効になるかは CSharpParseOptions.IsFeatureEnabled メソッドに定義されています。このあたりを追いかけておけば新しい機能への反応も早くなるのではないかと思います。

//--- こんな実装がある
internal bool IsFeatureEnabled(MessageID feature)
{
    switch (feature)
    {
        case MessageID.IDS_FeatureBinaryLiteral:
        case MessageID.IDS_FeatureDigitSeparator:
        case MessageID.IDS_FeatureLocalFunctions:
        case MessageID.IDS_FeatureRefLocalsReturns:
        case MessageID.IDS_FeaturePatternMatching:
        case MessageID.IDS_FeatureTuples:
            // in "demo" mode enable proposed new C# 7 language features.
            if (PreprocessorSymbols.Contains("__DEMO__"))
            {
                return true;
            }
            break;
        default:
            break;
    }

    //--- 以下略
}

Step.5 - C# 7 の機能を使ってデバッグ実行

ここまで設定すれば Visual Studio 2015 Update 2 上でコンパイルしてデバッグ実行することができます (∩´∀`)∩

f:id:xin9le:20160509015152p:plain

ただしどうやらインテリセンスが参照しているコンパイラはこの設定とは違うところにあるらしく、コード上では赤波線が出てしまいます。どこの設定をイジれば解決できるか分からないので現時点ではここまで。設定の在り処をご存知の方は教えていただけると嬉しいです m( )m

と、記事公開から数分で届く神回答...!

おまけ - Visual Studio Code でビルドする

Visual Sdutio Code + C# on Windows では現状デバッグはできませんが、ビルドして exe ファイルを作成することはできます.vscode\tasks.json に以下のような設定を入れれば OK!

{
    "version": "0.1.0",
    "command": "C:\\roslyn\\Binaries\\Release\\csc.exe",
    "showOutput": "always",
    "isShellCommand": true,
    "isBuildCommand": true,
    "args": [
        "${workspaceRoot}\\Program.cs",
        "/out:${workspaceRoot}\\Program.exe",
        "/features:binaryLiterals,digitSeparators,localFunctions,refLocalsAndReturns,patterns,tuples"
    ]
}

C# コンパイラに C# 7 の機能をひとつひとつ有効にしているのがお分かりいただけるかと思います。

ローカル関数の使いどころ - 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;
        }
    }
}