vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Play records

はじめに

この記事は C# その2 Advent Calendar 2020 - Qiita の四日目の記事です。

今回は C# 9 から登場した record で遊んでみたいと思います。

Environments

  • .NET ver.5.0.100

変換で使用

まずはおさらい

まずは record とはどんなものか、を簡単にまとめておきます。

下記のように書くことで、初期化の時のみ値をセットできる、init only setters のプロパティが自動生成されます。

Book.cs

public record Book(int Id, string Name);

中身はほぼこれと同じです。

Book.cs

public record Book
{
    public int Id { get; }
    public string Name { get; }

    public Book(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

ほぼ、と書いたのはインスタンスを作るときに若干の違いがあるためです。

Program.cs

using System;

class Program
{
    static void Main(string[] args)
    {
        // 最初の書き方ならOK。後の書き方だと new Book(1, "Programming C# 8.0"); でないとエラー
        var book = new Book(Id: 1, Name: "Programming C# 8.0");
        Console.WriteLine($"Book ID: {book.Id} Name: {book.Name}");
    }
}

また、後述しますが中身は class であるため、継承やメソッドを持つことができます。

この場合は後者の書き方にする必要があります。

ISomething.cs

public interface ISomething
{
    void Message();
}

Book.cs

public record Book: ISomething
{
    public int Id { get; }
    public string Name { get; }

    public Book(int id, string name)
    {
        Id = id;
        Name = name;
    }
    public void Message()
    {
        System.Console.WriteLine("Hello");
    }
}

要注意?なところでは、下記のように書くと(当然といえば当然ですが) init only setter にならず、好きに変更できてしまいます。

Book.cs

public record Book
{
    public int Id { get; set; }
    public string Name { get; set; }
}

開始早々だいぶ脇道に逸れている気はしますが。

比較

record には他にも特徴があり、比較を値で行ってくれます。

ClassSample.cs

public class ClassSample
{
    public int Id { get; init; }
    public string Name { get; init; }

    public ClassSample(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

Program.cs

...
    static void Main(string[] args)
    {
        var book = new Book(1, "Programming C# 8.0");
        var book2 = new Book(1, "Programming C# 8.0");

        var class1 = new ClassSample(1, "Programming C# 8.0");
        var class2 = new ClassSample(1, "Programming C# 8.0");
        // 結果は record: True class: False
        Console.WriteLine($"record: {book == book2} class: {class1 == class2}");
    }
...

With

また元のインスタンスの一部の値を変更する with を使うことができます。

Program.cs

    static void Main(string[] args)
    {
        var book = new Book(1, "Programming C# 8.0");
        var book2 = book with { Name = "Programming C# 5.0" };
        // 結果は book Id: 1 Name: Programming C# 8.0 book2 Id: 1 Name: Programming C# 5.0
        Console.WriteLine($"book Id: {book.Id} Name: {book.Name} book2 Id: {book2.Id} Name: {book2.Name}");
    }

中を見てみる

触ってると中がどうなってるのか気になりますよね~、ということで、VSCodeILSpy先生の力を借りて C# (dll) -> IL -> C# に変換したコードを見てみます。

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

public class Book : IEquatable<Book>
{
    protected virtual Type EqualityContract
    {
        [CompilerGenerated]
        get
        {
            return typeof(Book);
        }
    }

    public int Id
    {
        get;
        set;
    }

    public string Name
    {
        get;
        set;
    }

    public Book(int Id, string Name)
    {
        this.Id = Id;
        this.Name = Name;
        base..ctor();
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Book");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }

    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("Id");
        builder.Append(" = ");
        builder.Append(Id.ToString());
        builder.Append(", ");
        builder.Append("Name");
        builder.Append(" = ");
        builder.Append((object?)Name);
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator !=(Book? r1, Book? r2)
    {
        return !(r1 == r2);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator ==(Book? r1, Book? r2)
    {
        if ((object)r1 != r2)
        {
            return r1?.Equals(r2) ?? false;
        }
        return true;
    }

    public override int GetHashCode()
    {
        return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(Id)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name);
    }

    public override bool Equals(object? obj)
    {
        return Equals(obj as Book);
    }

    public virtual bool Equals(Book? other)
    {
        if ((object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<int>.Default.Equals(Id, other!.Id))
        {
            return EqualityComparer<string>.Default.Equals(Name, other!.Name);
        }
        return false;
    }

    public virtual Book <Clone>$()
    {
        return new Book(this);
    }

    protected Book(Book original)
    {
        Id = original.Id;
        Name = original.Name;
    }

    public void Deconstruct(out int Id, out string Name)
    {
        Id = this.Id;
        Name = this.Name;
    }
}

Wao!

一行のコードが実はこんなに、という感じですが、行数的に多いのはやはり比較。

ここをもっと追いかけたいところですが、長くなりそうなので次回以降ということで。

あと地味に ToString() で「Book { Id = 1, Name = Programming C# 8.0 }」のように表示される、というのも見どころかと。 デバッグがはかどりますね。

変換

record の中身は class というのはわかりましたが、例えば Entity Framework Core のモデルクラスの代わりに使ったり、JSONに変換することはできるのかも見てみます。

。。。結論から書くと、難なく変換できました。

Entity Framework Core

この時の記事を元に、モデルクラスのみ record に差し替えてみました。

Book.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

[Table("Books")]
public record Book
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id{ get; init; }
    [Required]
    public string Name { get; init; }
    public Book(int id, string name)
    {
        Id = id;
        Name = name;
    } 
}

JSON も問題なしですね。

MainController.cs

using System;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

public class MainController
{
    private readonly ConsoleSampleContext db;
    public MainController(ConsoleSampleContext db)
    {
        this.db = db;
    }
    public async Task StartAsync()
    {
        var book = await db.Books
            .Where(b => b.Id >= 0)
            .FirstOrDefaultAsync();
        Console.WriteLine($"Book: {book}");
        var jsonText = JsonSerializer.Serialize(book);
        Console.WriteLine($"JSON: {jsonText}");
        var deserializedBook = JsonSerializer.Deserialize<Book>(jsonText);
        Console.WriteLine($"Deserialized: {deserializedBook}");
    }
}

Result

Book: Book { Id = 1, Name = Book: 1999 }
JSON: {"Id":1,"Name":"Book: 1999"}
Deserialized: Book { Id = 1, Name = Book: 1999 }

おわりに

まだ軽く触っただけとはいえ、init only setter や record は必要な場面では結構気軽に使ってしまって良いのでは?という気がしました。

まぁ Database については更新時は値を変更する必要があるため、実際に使えるのは参照専用のデータ(所謂マスターと呼ばれるやつ)とか、普通のクラスとしてデータを取得したあと record に詰めて表示などに利用、といった使い方になるとは思いますが。

明日はjsakamotoさんです。よろしくお願いいたします(..)_