【C#】recordのススメ

ダメージ計算があるゲームなどにおいて、
パラメータの値のみを格納しているデータクラス
使用したい場面があったりします。

public class Parameter
{
    public string name { get; init; }
    public int hp { get; init; }
    public int attack { get; init; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="name">名前</param>
    /// <param name="hp">体力</param>
    /// <param name="attack">攻撃力</param>
    public Parameter(string name, int hp, int attack)
    {
        this.name = name;
        this.hp = hp;
        this.attack = attack;
    }

    /// <summary>
    /// 比較
    /// </summary>
    public override bool Equals(object other)
    {
        if (other == null || this.GetType() != other.GetType())
        {
            return false;
        }

        Parameter otherPara = (Parameter)other;
        return this.name == otherPara.name &&
               this.hp == otherPara.hp &&
               this.attack == otherPara.attack;
    }
}

こんな感じの単純にプロパティのみを持ったクラスを使いたい場合は、
recordという選択肢があります。 learn.microsoft.com

下準備

Unityでrecordを使用するにあたり、Initアクセサがあると便利です。
Scriptを新規作成し、以下のソースコードを記述します。

using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
    [EditorBrowsable(EditorBrowsableState.Never)]
    internal class IsExternalInit { }
}


stackoverflow.com

使い方

冒頭で記述したclassを、recordを使用して最小で記述するとこうなります。

public record Parameter(string name, int hp, int attack);

なんと1行です。
record名の横に記述した引数名がそのままgetプロパティとなります。
record名の横に引数を記述し、{}の中に関数を書くことも可能です。

ただ、一行で書くのであればローカル変数として使うか、
タプル型を使用した方が良いでしょう。

丁寧に書くとこんな感じです。
個人的にはこっちの方がスキ。

public record Parameter
{
    public string name { get; init; }
    public int hp { get; init; }
    public int attack { get; init; }

    public ParameterRecord(string name, int hp, int attack)
    {
        this.name = name;
        this.hp = hp;
        this.attack = attack;
    }
}

Equal(比較演算)

class動作の違いはEqual(比較演算)です。
冒頭で上げたプロパティを持つ、classrecordを作成します。
プロパティの内容はどちらも同じです。

それぞれ2つずつインスタンスを作成します。
プロパティの内容は同じにし、Equalで調べるとどうなるでしょうか?

using UnityEngine;

/// <summary> 例 </summary>
public class Example : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 定数
    //-----------------------------------------------------------------------

    // 固定値
    private const string Name = "Human";
    private const int Hp = 10;
    private const int Attack = 5;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        DataClassEqualTest();

        DataRecordEqualTest();
    }

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// クラス比較
    /// </summary>
    private void DataClassEqualTest()
    {
        var para1 = new ParameterClass(Name, Hp, Attack);
        var para2 = new ParameterClass(Name, Hp, Attack);

        Debug.Log($"{nameof(ParameterClass)} IsEqual : {para1.Equals(para2)}");
    }

    /// <summary>
    /// レコード比較
    /// </summary>
    private void DataRecordEqualTest()
    {
        var para1= new ParameterRecord(Name,Hp,Attack);
        var para2= new ParameterRecord(Name,Hp,Attack);

        Debug.Log($"{nameof(ParameterRecord)} IsEqual : {para1.Equals(para2)}");
    }

}




結果は以下のようになります。

`classOのEqualは同一のインスタンスかどうかを調べます。
別々のインスタンスなので結果はfalseとなります。

一方、recordのEqualは変数の値で比較を行います。
同じ値を使用しているためtrueとなります。

with(一部分コピー)

また、元々あるrecord一部分のプロパティをコピーして
新規recordインスタンスが作成できる
機能 withを使用できます。

// var【コピー先】 = 【コピー元】 with { プロパティ名 = 値, ... }

var original= new ParameterRecord(Name,Hp,Attack);
var copy = original with { name = "copyHuman" };

参考サイト

レコード - C# リファレンス | Microsoft Learn
init キーワード - C# リファレンス | Microsoft Learn
c# 9.0 - Predefined type 'System.Runtime.CompilerServices.IsExternalInit' is not defined or imported - Stack Overflow