rksoftware

Visual Studio とか C# とかが好きです

C# 9.0 の確認「レコード」

C# 9.0 の確認の目次はこちら

■ レコードの基本

ドキュメントはこちら

GetHashCode()ToString()== なんかをいい感じに勝手に実装してくれる素敵な型です。
値を表現するクラスをいい感じに作ってくれます。

class を作る際に、class と書いていた部分を record と書くことで使えます。

public record Saitama
{
    public string Value { get; }

    public Saitama(string value) => Value = value;
}

ただ、一番の使い方は上の書き方ではなく次のような書き方になるでしょう。

public record Saitama(string Value)
{
}

なんとこれで同じ意味です。他言語でできて C# にも欲しいと (私が) 思っていたやつです。うれしいですね。

レコードはドキュメントを読めば使う分には問題ないと思います。そして実際に使う際にはあまり変な使い方はせず変更不可能な値を保持する型として行儀よくしか使わないと思うので、いろいろ確認しなくても使いこなせるでしょう。
そこで今回は、使わなそうな挙動も含めて動かしてみたいと思います。

■ ToString() の確認

ログやデバッグなどで使うことになるでしょう。

ToString() の結果例

Saitama { Value = せんべい }

Saitama が型で、Value がプロパティです。型とプロパティがいい感じに見られるようになっていますね。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        Console.WriteLine(saitama);
    }
}

public record Saitama
{
    public string Value { get; }

    public Saitama(string value) => Value = value;
}

■ プロパティ宣言不要

前述していますが、プロパティを宣言したりコンストラクタでプロパティに値を詰めたりしなくて良い書き方です。
値を詰める時に書き間違えたり、プロパティを追加した際にコンストラクタの変更を忘れたりといったミスに怯えなくて済みます。素敵。

ToString() の結果例

Saitama { Value = せんべい }

前述の結果と同じになっています。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        Console.WriteLine(saitama);
    }
}

public record Saitama(string Value)
{
}

注意点

同名のプロパティがあるとダメなようです。

ToString() の結果例

Saitama { Value =  }

プロパティに値が設定されていません。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        Console.WriteLine(saitama);
    }
}

public record Saitama(string Value)
{
    public string Value { get; set; }
}

■ 複数の引数

複数のプロパティも行けます。その場合、書いた順になります。

ToString() の結果例

Saitama { Value2 = せんべい, Value1 = かもねぎ鍋 }

プロパティ名の文字列順とかでなく、引数の順になっています。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい", "かもねぎ鍋");
        Console.WriteLine(saitama);
    }
}

public record Saitama(string Value2, string Value1)
{
}

■ 分解

分解も自動で作られます。

分解結果の例

せんべい, かもねぎ鍋

分解も、ToString() と同様に引数の順になっています。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい", "かもねぎ鍋");
        var (v1, v2) = saitama;
        Console.WriteLine($"{v1}, {v2}");
    }
}

public record Saitama(string Value2, string Value1)
{
}

■ with 式によるコピー

with 式により、一部のプロパティが変更されたコピーを作ることができます。
record はプロパティの変更不可を絶対として、一部のプロパティを変更したい場合は新しいインスタンスを作って元の値の変更はしない使い方をしたいところです。その際にとてもうれしい機能です。

プロパティ変更不可のデータ型を使うというスタイルはもしかしたら慣れるまでに時間のかかる方もいるかもしれませんが、時代の流れです。備えましょう。

確認結果例

Saitama { Value2 = せんべい, Value1 = かもねぎ鍋 }
Saitama { Value2 = せんべい, Value1 = まんじゅう }

with を使った際に指定していないほうのプロパティ (Value2) の値が同じになっています。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい", "かもねぎ鍋");
        var saitama1 = saitama with { Value1 = "まんじゅう"};
        Console.WriteLine(saitama);
        Console.WriteLine(saitama1);
    }
}

public record Saitama(string Value2, string Value1)
{
}

■ 値型でなくクラス

ドキュメントに書かれていますが、値型ではなく参照型です。実際に動かして確認してみましょう。

確認結果例

ValueType? => False
Class?     => True

確かにクラスになっています。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        Console.WriteLine($"ValueType? => {saitama.GetType().IsValueType}");
        Console.WriteLine($"Class?     => {saitama.GetType().IsClass}");
    }
}

public record Saitama
{
    public string Value { get; }

    public Saitama(string value) => Value = value;
}

■ null 可能

クラスということは null にできます。

確認結果

null? => True

確かに null になります。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        saitama = null;
        Console.WriteLine($"null? => {saitama == null}");
    }
}

public record Saitama
{
    public string Value { get; }

    public Saitama(string value) => Value = value;
}

■ null 参照例外

null になるということは null 参照例外の可能性があります。

確認結果

NullReferenceException

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        saitama = null;
        try
        {
            Console.WriteLine($"Value => {saitama.Value}");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.GetType().Name);
        }
    }
}

public record Saitama
{
    public string Value { get; }

    public Saitama(string value) => Value = value;
}

■ 変更可能にできる

必ずプロパティが変更不可になるものではなく、変更可能なプロパティを書けば変更可能になります。
間違って変更可能なプロパティが混入しないよう気を付けましょう

確認結果

かもねぎ鍋

コンストラクタで与えた せんべいかもねぎ鍋 に変更できてしまいました。

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        saitama.Value = "かもねぎ鍋";
        Console.WriteLine(saitama.Value);
    }
}

public record Saitama
{
    public string Value { get; set; }

    public Saitama(string value) => Value = value;
}

■ 変更可能でも == 判定は大丈夫

プロパティを変更可能にしても == の判定はプロパティが同じ値であれば true になります。
これを活用するコードのあるかもしれませんが、基本的にはコーディングミスが多いでしょう。覚えておくことで変更可能になってしまっている可能性に気づいて助かる日が来るかもしれません。

確認結果

せんべい == かもねぎ鍋 => False
せんべい == せんべい => True

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        var saitama1 = new Saitama("かもねぎ鍋");
        var saitama2 = new Saitama("せんべい");
        Console.WriteLine($"{saitama.Value} == {saitama1.Value} => {saitama == saitama1}");
        Console.WriteLine($"{saitama.Value} == {saitama2.Value} => {saitama == saitama2}");
    }
}

public record Saitama
{
    public string Value { get; set; }

    public Saitama(string value) => Value = value;
}

■ プロパティでなくフィールドにしたら?

プロパティと同じように == が実装されるようです。

確認結果

せんべい == かもねぎ鍋 => False
せんべい == せんべい => True

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        var saitama1 = new Saitama("かもねぎ鍋");
        var saitama2 = new Saitama("せんべい");
        Console.WriteLine($"{saitama.Value} == {saitama1.Value} => {saitama == saitama1}");
        Console.WriteLine($"{saitama.Value} == {saitama2.Value} => {saitama == saitama2}");
    }
}

public record Saitama
{
    public string Value;

    public Saitama(string value) => Value = value;
}

■ プロパティを変えてしまったら?

インスタンス生成時の値が同じ二つのインスタンスで、一方だけ生成後にプロパティを変更したら == は false になってくれます。

確認結果

せんべい == かもねぎ鍋 => False
せんべい == まんじゅう => False

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        var saitama1 = new Saitama("かもねぎ鍋");
        var saitama2 = new Saitama("せんべい");
        saitama2.Value = "まんじゅう";
        Console.WriteLine($"{saitama.Value} == {saitama1.Value} => {saitama == saitama1}");
        Console.WriteLine($"{saitama.Value} == {saitama2.Value} => {saitama == saitama2}");
    }
}

public record Saitama
{
    public string Value { get; set; }

    public Saitama(string value) => Value = value;
}

■ record でなく class にしたら?

基本に立ち戻って、class との違いを確認します。class では、プロパティの値が同じでもインスタンスが違えば == は false です (当たり前ですが)。

確認結果

せんべい == かもねぎ鍋 => False
せんべい == せんべい => False

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        var saitama1 = new Saitama("かもねぎ鍋");
        var saitama2 = new Saitama("せんべい");
        Console.WriteLine($"{saitama.Value} == {saitama1.Value} => {saitama == saitama1}");
        Console.WriteLine($"{saitama.Value} == {saitama2.Value} => {saitama == saitama2}");
    }
}

public class Saitama
{
    public string Value { get; set; }

    public Saitama(string value) => Value = value;
}

■ HashCodeは?

いい感じに実装してくれているので、プロパティが同じなら同じはずです。

確認結果

せんべい => -296195340
かもねぎ鍋 => -404744188
せんべい => -296195340

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama("せんべい");
        var saitama1 = new Saitama("かもねぎ鍋");
        var saitama2 = new Saitama("せんべい");
        Console.WriteLine($"{saitama.Value} => {saitama.GetHashCode()}");
        Console.WriteLine($"{saitama1.Value} => {saitama1.GetHashCode()}");
        Console.WriteLine($"{saitama2.Value} => {saitama2.GetHashCode()}");
    }
}

public record Saitama
{
    public string Value { get; set; }

    public Saitama(string value) => Value = value;
}

■ プロパティを参照にした場合

プロパティを参照にした場合は、同じ値を持った別の参照がプロパティにセットされていれば == は false です。

確認結果

System.Int32[] == System.Int32[] => False
System.Int32[] == System.Int32[] => False

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama(new[] { 1 });
        var saitama1 = new Saitama(new[] { 2 });
        var saitama2 = new Saitama(new[] { 1 });
        Console.WriteLine($"{saitama.Value} == {saitama1.Value} => {saitama == saitama1}");
        Console.WriteLine($"{saitama.Value} == {saitama2.Value} => {saitama == saitama2}");
    }
}

public record Saitama
{
    public int[] Value { get; set; }

    public Saitama(int[] value) => Value = value;
}

■ プロパティに同一の参照をセットした場合

プロパティに同一の参照をセットすれば、== は true です。

確認結果

System.Int32[] == System.Int32[] => False
System.Int32[] == System.Int32[] => True

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama(new[] { 1 });
        var saitama1 = new Saitama(new[] { 2 });
        var saitama2 = new Saitama(saitama.Value);
        Console.WriteLine($"{saitama.Value} == {saitama1.Value} => {saitama == saitama1}");
        Console.WriteLine($"{saitama.Value} == {saitama2.Value} => {saitama == saitama2}");
    }
}

public record Saitama
{
    public int[] Value { get; set; }

    public Saitama(int[] value) => Value = value;
}

■ プロパティを record にした場合

参照が違ってもプロパティの型が record であれば同じ値を持っていれば == はtrue です。

確認結果

Saitama { Value =  } ==  => False
Saitama { Value =  } == Saitama { Value =  } => True

検証コード

class Program
{
    static void Main(string[] args)
    {
        var saitama = new Saitama(new Saitama(null));
        var saitama1 = new Saitama(null);
        var saitama2 = new Saitama(new Saitama(null));
        Console.WriteLine($"{saitama.Value} == {saitama1.Value} => {saitama == saitama1}");
        Console.WriteLine($"{saitama.Value} == {saitama2.Value} => {saitama == saitama2}");
    }
}

public record Saitama
{
    public Saitama Value { get; set; }

    public Saitama(Saitama value) => Value = value;
}

■ まとめ

まあ、当たり前の結果になりました。
けれど、当たり前だからこそ敢えて確認しなければ確認しないですし、そもそもこんなコードを書かないし読む機会もないということを確認してみました。

事実は小説よりも奇なり

実際にはこんなことになっているコードに出会う機会もあるでしょう。そんな時にシュッとバグをとれるよう備えましょう。