こんにちは。阿部です。
C#では、バージョン7.0からタプルという機能が使えるようになりました。
この機能は、今まで一つのオブジェクトしか渡すことができなかった状況で、複数のオブジェクトを渡すことを可能にします。
複数戻り値
タプルの代表的な使用方法は、メソッドから複数の値を返すことです。
これまで、メソッドの戻り値は0個(void
の場合)か1個でしたが、タプルを使用することで2個以上の戻り値を記述することができます。
1
2
3
4
5
|
// 複数戻り値のメソッド定義
(string name, int age) GetNameAndAge()
{
return ("John", 23);
}
|
メソッドを呼び出す側では、複数の変数に結果を受け取ることができます。
これを、タプルの分解(deconstruction)と言います。
1
2
3
4
5
6
7
|
string name0;
int age0;
// 複数の変数に同時に代入できる
(name0, age0) = GetNameAndAge();
// 複数の変数を宣言と同時に初期化することもできる
var (name1, age1) = GetNameAndAge();
|
ところで、「GetName()
とGetAge()
にメソッドを分ければ良いのでは?」という声が聞こえてきそうですが、「同時に取得したほうが計算コストが低い。」とか「2回に分けて取得すると値が変わってしまう。」といった理由で分けられないシチュエーションもあると思います。
コレクションの要素として利用する
タプルは、List
などのコレクションの要素として利用することができます。
コレクションはforeach
で回すことが多いと思いますが、取り出した要素を複数変数で受け取ることができます。
1
2
3
4
5
6
7
8
9
|
// タプルのリストを作ることができる
var list = new List<(string name, int age)>();
list.Add(("John", 23));
list.Add(("Taro", 45));
// foreachで回す際は、nameとageに分解して受け取ることができる
foreach (var (name, age) in list)
{
}
|
一方で、LINQの条件式内では、複数変数に分解することができないため、タプルとして受け取ることになります。この場合、クラスメンバのように、”タプル.要素名
” で中身にアクセスできます。
1
2
3
4
5
|
// Whereの条件式では、タプルとして受け取って処理する
var u35 = list.Where(tuple => tuple.age < 35);
// これはできない
// var u35 = list.Where(((name, age)) => age < 35);
|
Dictionaryで複数キーを実現
C#の連想配列であるDictionary
の場合は、Key
にもValue
にもタプルを利用することができます。Key
としてタプルを利用した場合は、データベース(RDB)における複数キーのような構造を表現することができます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// タプルを使った連想配列
var dic = new Dictionary<(int id, int subId), (string name, int age)>();
dic.Add((1, 1), ("John", 23));
dic.Add((1, 2), ("Peter", 24));
dic.Add((2, 1), ("Taro", 45));
// 取り出す際、out引数では分解することができないため、タプルとして受け取る
(string name, int age) tuple0;
dic.TryGetValue((1, 1), out tuple0);
// C#7から導入されたout var構文を使う場合も、分解はできない。
dic.TryGetValue((1, 1), out var tuple1);
// こう書ければ良いのだけれど
// dic.TryGetValue((1, 1), out var (name, age));
|
使いどころの見極め
タプルを使えるとしても、基本的には、まず構造体やクラスにすることを検討すべきだと思います。
ここまでの例であれば、(string name, int age)
ではなく、次のような型を用意したほうが適当かもしれません。
1
2
3
4
5
|
struct Person
{
public string Name;
public int Age;
}
|
こうすれば、ただの「名前」と「年齢」ではなく、「人」というデータであることが表現できます。タプルにすると、この情報は失われてしまいます。
一方、ここで、Person
ではなくNameAndAge
やNameAgePair
という、あまり意味のない名前になってしまうなら、おそらくタプルの出番です。
1
2
3
4
5
6
|
// 冗長な型名。型名に情報量がない。
struct NameAndAge
{
public string Name;
public int Age;
}
|
このような場合は、積極的にタプルを使用しても良いと思います。
楽をするために使う
メソッドの戻り値をまとめるのに、わざわざ型を作りたくないですよね。
タプルを使って楽をしましょう。
しかし、タプルを使用すると、型名として持つべき情報が失われてしまいます。そこで、タプルの乱用を防ぐために、ある程度の指針は必要だと思います。例えば、
- クラス内のみでの使用。(
private
な範囲での使用。) - 型名がなくても、要素名だけで十分な情報を表現できている場合。
- とにかく急いでいる場合や、使い捨てのコードの場合。
最後の指針は難しいところです。今書いているコードが、絶対に再利用されないと保証できるでしょうか。意外と、そのまま製品に使われてしまったりして……。
最後に
タプルの使い方は、型推論のvar
をどう使うかという議論と似たところがあります。
いずれも、コードを簡潔にし、冗長性を取り除くことができますが、使い方によっては、コードの持つ情報量が減ってしまう可能性があります。
むしろ、型推論であれば、全部var
にしてしまっても、IntelliSenseで、すぐに型名が見られるのですが、タプルの場合は、「型名」という情報が本当に消え去ってしまいます。
いろいろなところで使えそうな機能ではあるのですが、実際に使うべき場面の見極めが重要です。