💨

[C#]タプル、つかってる? 戻り値を複数持つ、最善の方法

に公開2

「ここだけ、ちょっと集合したデータを扱いたい……」
「ここだけ、戻り値を複数にしたい……」

コードを書いててそういう経験、ありませんか? タプルを使うとそういうことが簡単に実現可能です。
色々と例をあげてみますので、使えそうであれば使っちゃいましょう。

🎯メソッドの戻り値を複数にする

ほとんどの人は、これがタプルを使い始める理由かもしれません。

(bool isCritical, int damage) CheckCriticalDamage(int baseDamage, float criticalRate)
{
    bool isCritical = UnityEngine.Random.value < criticalRate;
    int  damage     = isCritical ? baseDamage * 2 : baseDamage;

    return (isCritical, damage);
}

void test()
{
    var result = CalculateAttack(10, 0.2f);
    if (result.isCritical)
    {
        Debug.Log($"Critical Hit! Damage: {result.damage}");
    }
    else
    {
        Debug.Log($"Normal Hit. Damage: {result.damage}");
    }
}

クリティカルかどうか判定し、クリティカルなら倍、そうでなければそのままのダメージを返すメソッドです。test() で使い方を示しておきます。

ダメージだけではなく「クリティカルかどうか」という情報は、その後のゲームメッセージやエフェクトにも絡むので、2つの戻り値を返しています。

なお、これを使わない場合は out bool isCritical のような引数を追加するか、戻り値クラスを別途用意する必要があります。どちらも少々面倒な記述ですね。

🎯簡易的なデータクラス

var person = new { id = 1, name = "John Doe", age = 30 };

Debug.Log($"ID: {person.id}, Name: {person.name}, Age: {person.age}");

タプルはデータ保持構造体を自動的に生成してくれる、というのが正しい認識です。

この例だけだと、変数3つ作ればいいと思われそうなので実用的な例も挙げます。
例えば GPT-API にアクセスする時……。

var assistantInfo = new
{
    name         = data.Name,
    instructions = data.Instructions,
    model        = data.GptModel
};

string jsonData = JsonConvert.SerializeObject(assistantInfo);

using (UnityWebRequest request = UnityWebRequest.Put(GPTApiConnect.URL_ASSISTANTS, jsonData))
・
・
・

このように、データ通信に必要な構造体を一時的に生成し、json に変換、なんてこともできます。

🎯データリスト

var peopleList = new List<(int id, string name, int age)>
{
    (1, "Taro",   28),
    (2, "Hanako", 25),
    (3, "Jiro",   31)
};

peopleList.ForEach(p => Debug.Log($"ID: {p.id}, Name: {p.name}, Age: {p.age}"));

データリストも簡単に生成できます。

なお、タプルを使わない場合は、以下のように長い記述が必要です。
クラスを前もって用意したり、new Person を毎回記述するなど、だいぶ面倒が増えているのがわかります。

class Person
{
    public int    id;
    public string name;
    public int    age;
    public Person(int id, string name, int age)
    {
        this.id   = id;
        this.name = name;
        this.age  = age;
    }
}

var peopleList = new List<Person>
{
    new Person(1, "Taro", 28),
    new Person(2, "Hanako", 25),
    new Person(3, "Jiro", 31)
};

🎯(おまけ)値を入れ替える

あまり需要はないかもしれませんが、こんなことも出来ちゃいます。

float a = 4;
float b = 3;

(a, b) = (b, a);

Debug.Log($"a: {a}, b: {b}");

タプルを使わないと、こんなコードが必要ですよね。

float temp = a;
a = b;
b = temp;

タプルに向いてないケース

ここまでいいことばかり書いてみましたが、向いてないケースもあります。

値の変更がやりづらいケース

var values = new List<(int v1, string v2)>
{
    (1, "Test"),
};
// ★このコードはエラー
values[0].v1 = 2;

// こっちなら通るが、面倒
var p = values[0];
p.v1 = 2;
values[0] = p;

構造体由来のためか、こういう微妙な制約があったりします。
タプルはあくまで一時的に使うデータと認識しておくと無難かもしれません。

メソッドも持てない

データ保持構造体を自動的に作成できる、といいましたがその宣言にメソッドを含めることもできません。

❗TupleElementNamesAttribute がない、とエラーが出る

これは向いてないケース、というわけではなくシンプルにタプル非対応バージョンの C# を使っているのが原因です。

C#7 以降が使える開発環境にするか、NuGet パッケージマネージャーで System.ValueTuple を入れるなど各々で対応してください。

Discussion

junerjuner

厳密にはクラスというより「型」が正しいのですが、イメージしやすさ重視でクラスと表現します。

クラス を出すなら構造体では?感(暗黙的な記法の方は ValueTuple なので(Tuple でも Deconstructor は実装されているので暗黙的ではないが同様の使い方はできる


タプルはあくまで一時的に使うデータなので、値の変更・追加・削除はできません。

フィールドの追加/プロパティの追加レベルのことはできませんが、値の変更自体は
ValueTuple の フィールドは readonly ではないのでできます。

using System;

#region 初期値
(int v1, string v2) value= (1,"test");
Console.WriteLine(value);
#endregion

#region with expression によるインスタンスへの再代入
value = value with {
    v2 = "hoge",
};
Console.WriteLine(value);
#endregion

#region フィールドの参照を ref で取得してその参照変数に代入して変更
ref var v3 = ref value.v2;
v3 = "fuga";

Console.WriteLine(value);
#endregion


#region 勿論フィールドは読み取り専用ではないので変更できます。
value.v2 = "piyo";
Console.WriteLine(value);
#endregion

https://sharplab.io/#v2:C4LgTgrgdgPgAgJgIwFgBQ6DEYCmBzASwHsoACQXCVB8c0BIFdACgKmFIDckAaUuJABlYQCUrAIYAbCDgC8pOhwBEwHAGdgcgQG503AJx0WYiRqw4oAE1yES6LBeJkA7gWAALUjgAeAB1xKld0oDWDIARDIDRDIAlDIDPDICdDID9DNGAHgyAdgyAsYqAx3KAporo+uI4pNLZEqSOLqQA3uiklfx5pHLORHg4cuzoAL6aaDp6BjhGaJgm5vh21v22JKSAqwyAxQyAPwyA1wyAkwyJgEPKgOaOgEkMpLgAZqSA5gyAa8qA6fqA6gyAZgyAugyra4CQmoAOpgEZF7eAL2bouyJgrADMNZ8FHAAOhYCA6LD+0jkOwgeGEcg6WiQukBfQGZnGUFGNmGE0A/0qANaiZgtFoB7BkAtVGAfwZDoAohkAQDqACld9qTAFYMgBEGRL7N77QCyDIA/BkAmgyAIAYsj0QQganJPAQAJ5EBFIlE9NGDTFAA=

読み取り専用なのは 暗黙的ではないタプル……つまり Tupleの方ですね。

using System;

#region 初期値
Tuple<int,string> value= Tuple.Create(1,"test");
Console.WriteLine(value);
#endregion

#region 変数への再代入
value = Tuple.Create(1, "hoge");
Console.WriteLine(value);
#endregion

#region 勿論プロパティは読み取り専用なので変更できません。
// value.Item1 = "piyo";
// Console.WriteLine(value);
#endregion

https://sharplab.io/#v2:C4LgTgrgdgPgAgJgIwFgBQ6DEYCmBzASwHsoACQXCVB8c0BIFdAFQgAcAbHAHgKmABo4kAGAHykAbgENmEHAF5SDFjgB0AYVxjgOABRJuAIg0BnYLoCUAbnR8AnJvGSc5rDigATXIRLos74mUCQmoAOpoAeDIB2DICxioDHcoCmiuh2UqSy8qwqahra3KS6ABZEeDimFmjWthJSjmiYzm74vl4VPiSkgP9KgGtRgOsMgLcMgIsMgGMMgMUMgPYMgLVRgP4MgGvKgFEMgEA6gBSugFYMIYDmDH6AL2aLgLIMgH4MgNoMgMkMgEAM6AD0R6KlSgCSGgC2SAmZjAQAnkS6hSekxXEOhZWuDVAgA

catsnipecatsnipe

ご指摘ありがとうございます。勉強になります。

記事についてはタプルを使ったことのない人が、へーそんなのあるんだ、と使ってみるための記事、と思ってください。厳密性はあまり担保していません。
(まあ、私自身がてきとーな性格ということも要因ではあります……)

クラス を出すなら構造体では?

Unity/C# をかじったレベルだと構造体使ったことのない人も多そうですので、あえてクラスという書き方にしました。
が、Tuple がクラス由来で ValueTuple が構造体由来(記事の説明は大体こっち)だったりするので誤解を生みそうですね。書き直しておきます。

値の変更・追加・削除はできません。

ちょっと乱暴&私が Tuple と ValueTuple をごっちゃにしてたようです。記事を訂正しておきますね。