はじめに
C# では、関数の戻り値は void
または任意の型をひとつ指定します。複数の戻り値を返したい場合にそのままでは返せないため、タプルを使うのが有効です。
(int age, string name) GetPerson() => (20, "Taro");
// タプルで受け取り
var person = GetPerson();
// 分解して受け取り
(var age, var name) = GetPerson();
タプル要素名の大文字小文字問題
https://ufcpp.net/blog/2018/12/tupleelementcase/
C# では public
なメンバーは PascalCase が一般的です。メソッドの引数は camelCase が一般的です。ここで、タプル要素名の大文字小文字問題が起こります。
- 立場1: 構造体のフィールド派
- 立場2: 引数の一般化派
両者は殴り合いに発展し、ついに「public
なところでタプルは使うな」というお触れが出るに至ります。
この問題は後述のレコード型を使うことで穏便に解消できそうです。
レコード型
https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/types/records
C# 9.0 で、レコード型 record class
が、C# 10.0 で値レコード型 record struct
が追加されました。
file record ClassRecord(int X, int Y);
file record struct ValueRecord(int X, int Y);
// 参照型のレコード
ClassRecord classRecord = new ClassRecord(1, 2);
// 分解
(x, y) = classRecord;
// with 式
(x, y) = classRecord with { X = 3 };
// 値型のレコード
ValueRecord valueRecord = new ValueRecord(1, 2);
// 分解
(x, y) = valueRecord;
// with 式
(x, y) = valueRecord with { X = 3 };
1行で型定義できるため手間がかかりません。それだけではなく、この手の型では毎回必要だった Equals()
や GetHashCode()
のオーバーライド、IEquatable<T>
の継承もコンパイラが自動で実装してくれます。
一応、通常の値型を定義したときに標準で機能する Equals()
と GetHashCode()
はありますが、リフレクションを使ったものであり、あまりパフォーマンスはよくないです。
参照引数問題
C# にタプルがない頃は別の方法を使う必要がありました。戻り値型を定義する方法と、参照引数を使う方法です。
戻り値型を定義する方法は、戻り値を表すことにしか使い道のない型を作るという微妙な作業が必要でした。現在ではレコード型があるため煩わしさも減り、この方法が有効です。file
型修飾子も追加されたため、より可視性を狭めることも可能です。
参照引数(ref
out
)を使う伝統的な方法もあります。
void GetPersonReference(out int age, out string name)
{
age = 20;
name = "Taro";
}
しかしながら、今日ではこの方法は微妙とされています。参照引数はローカル関数やラムダ式からアクセスできないですし、非同期処理でも使えません。
void GetPersonReference(out int age, out string name)
{
age = 20;
name = "Taro";
void SetAge() => age = 30; // エラー!
}
async Task GetPersonAsync(out int age, out string name) // エラー!
{
age = await Task.Run(() => 20);
name = await Task.Run(() => "Taro");
}
一応 ↓ の場合に、参照引数を採用する余地がありそうです。
- 複数のオーバーロードを用意したい場合(タプルというか、戻り値だとオーバーロードできない)
Try
パターンなんかだと、単一のbool
を返す意味がある。if
ステートメントの条件式とかで使えるように
https://ufcpp.net/blog/2016/8/rickuproslyn0827/
var str = "123";
if (int.TryParse(str, out var value))
{
// value を使う
}
古い機能
これらは .NET Framework 時代からの機能ですが、やや古く機能が少ないので、特に理由がなければ値型タプルやレコード型を使うほうがよさそうです。
- 参照型タプル https://learn.microsoft.com/ja-jp/dotnet/api/system.tuple-2?view=net-8.0
- 匿名型 https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/types/anonymous-types
古い機能
int x, y;
// 参照型タプル
System.Tuple<int, int> classTuple = Tuple.Create(1, 2);
// 分解
(x, y) = classTuple;
// with 式は使えない!
// (x, y) = classTuple with { X = 3 }; // エラー
// 匿名型(参照型)
var anonymousType = new { X = 1, Y = 2 };
// 分解できない!
// (x, y) = anonymousType; // エラー
// with 式
var anonymousTypeWith = anonymousType with { X = 3 };
おわりに
パフォーマンス比較します。見た感じ、特に理由がなければ ValueTuple
か ValueRecord
を使うのがよさそうです。
Test | Score | % | CG0 |
---|---|---|---|
ValueTuple | 1,929 | 100.0% | 0 |
ClassRecord | 407 | 21.1% | 116 |
ValueRecord | 1,814 | 94.0% | 0 |
ClassTuple | 394 | 20.4% | 113 |
AnonymousType | 21 | 1.1% | 30 |
実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- ValueTuple と ValueRecord は値型のため高速です。
- ClassRecord と ClassTuple は参照型のためヒープにメモリが確保され、値型に比べると少しパフォーマンスが下がります。
- AnonymousType は
dynamic
を使用しているため低速です。
テストコード
using Xunit;
file record ClassRecord(int X, int Y);
file record struct ValueRecord(int X, int Y);
public class _TupleTest
{
(int age, string name) GetPerson() => (20, "Taro");
void GetPersonReference(out int age, out string name)
{
age = 20;
name = "Taro";
// void SetAge() => age = 30;
}
// async Task GetPersonAsync(out int age, out string name)
// {
// age = await Task.Run(() => 20);
// name = await Task.Run(() => "Taro");
// }
void ExampleTryPattern()
{
var str = "123";
if (int.TryParse(str, out var value))
{
// value を使う
}
}
/// <summary>
/// 戻り値のタプル要素名は省略できる<br/>
/// 戻り値の第1要素は X 成分を表す<br/>
/// 戻り値の第2要素は Y 成分を表す
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
(int, int) GetTuple(int x, int y) => (x, y);
void Example1()
{
// 受け取り側で名前をつける
(var x, var y) = GetTuple(1, 2);
}
void Example2()
{
int x, y;
// 値型のタプル
(int X, int Y) valueTuple = (1, 2);
// 分解
(x, y) = valueTuple;
// with 式
(x, y) = valueTuple with { X = 3 };
// 参照型のレコード
ClassRecord classRecord = new ClassRecord(1, 2);
// 分解
(x, y) = classRecord;
// with 式
(x, y) = classRecord with { X = 3 };
// 値型のレコード
ValueRecord valueRecord = new ValueRecord(1, 2);
// 分解
(x, y) = valueRecord;
// with 式
(x, y) = valueRecord with { X = 3 };
}
void Example3()
{
int x, y;
// 参照型のタプル
System.Tuple<int, int> classTuple = Tuple.Create(1, 2);
// 分解
(x, y) = classTuple;
// with 式は使えない!
// (x, y) = classTuple with { X = 3 }; // エラー
// 匿名型(参照型)
var anonymousType = new { X = 1, Y = 2 };
// 分解できない!
// (x, y) = anonymousType; // エラー
// with 式
var anonymousTypeWith = anonymousType with { X = 3 };
}
[Fact]
void TupleTest()
{
var tuple = (1, 2);
(var x, var y) = tuple;
var tuple2 = GetTuple2(x, y);
Assert.Equal(tuple, tuple2);
}
static (int, int) GetTuple2(int x, int y) => (x, y);
static Action ValueTuplePerformance()
{
(int x, int y) GetTuple(int n) => (n, n * 2);
return () =>
{
var sum = 0;
for (int n = 0; n < 100000; ++n)
{
var (x, y) = GetTuple(n);
sum += x + y;
}
};
}
static Action ClassRecordPerformance()
{
ClassRecord GetClassRecord(int n) => new ClassRecord(n, n * 2);
return () =>
{
var sum = 0;
for (int n = 0; n < 100000; ++n)
{
var record = GetClassRecord(n);
sum += record.X + record.Y;
}
};
}
static Action ValueRecordPerformance()
{
ValueRecord GetValueRecord(int n) => new ValueRecord(n, n * 2);
return () =>
{
var sum = 0;
for (int n = 0; n < 100000; ++n)
{
var record = GetValueRecord(n);
sum += record.X + record.Y;
}
};
}
static Action ClassTuplePerformance()
{
System.Tuple<int, int> GetTuple(int n) => Tuple.Create(n, n * 2);
return () =>
{
var sum = 0;
for (int n = 0; n < 100000; ++n)
{
var tuple = GetTuple(n);
sum += tuple.Item1 + tuple.Item2;
}
};
}
static Action AnonymousTypePerformance()
{
object GetAnonymousType(int n) => new { X = n, Y = n * 2 };
return () =>
{
var sum = 0;
for (int n = 0; n < 100000; ++n)
{
dynamic anonymousType = GetAnonymousType(n);
sum += anonymousType.X + anonymousType.Y;
}
};
}
}