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 の実装を気にすることなく気軽にタプル構文を書けるのは素晴らしいですね!