はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】record (class)・record struct・readonly record structがどういう実装になるのかSharpLabでデコンパイルして覗いてみる

はじめに

record (class)record structreadonly record structはとても便利な機能なのですが、たまにEqualの処理どうなってたっけ・プロパティのアクセシビリティなんだっけと私はよくなります。

// record : record classと同じ意味
public record Record(string Name, int Age)
{
    public bool IsValid() => !string.IsNullOrEmpty(Name) && Age > 0;
}

// record class : recordと同じ意味
public record class RecordClass(string Name, int Age)
{
    public bool IsValid() => !string.IsNullOrEmpty(Name) && Age > 0;
}

// record struct
public record struct RecordStruct(string Name, int Age)
{
    public bool IsValid() => !string.IsNullOrEmpty(Name) && Age > 0;
}

// readonly record struct
public readonly record struct ReadOnlyRecordStruct(string Name, int Age)
{
    public bool IsValid() => !string.IsNullOrEmpty(Name) && Age > 0;
}

そこで公式ドキュメントを見に行くわけですが、それをするよりもSharpLabで糖衣構文から素朴な実装に変換してしまうのが一番正しく簡単に理解できることに気が付きました。

その方法と実際の中身を紹介したいと思います。

SharpLab

SharpLabはブラウザ上でC#コードをコンパイルしてどういうILになるかを確認したり、構文木を見たりすることができる神サイトです。

ILを確認している様子

またちょっと面白い使い方として、C#で記述したコードをコンパイルした後にILをデコンパイルして表示することができます。これを用いることで、糖衣構文を素朴な実装として確認することができちゃいます。

// C#10以上ではstring-interpolationがDefaultInterpolatedStringHandlerを用いた実装に変換されるようになっているのですが、それもSharpLab上で確認できます
var x = 10;
var y = 20;
var str = $"{x}, {y}";
// 変換後
int value = 10;
int value2 = 20;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(2, 2);
defaultInterpolatedStringHandler.AppendFormatted(value);
defaultInterpolatedStringHandler.AppendLiteral(", ");
defaultInterpolatedStringHandler.AppendFormatted(value2);
defaultInterpolatedStringHandler.ToStringAndClear();

本題

Attributeといったあまり重要でない箇所は除いています。

record, record class

public record Record(string Name, int Age)
{
    public bool IsValid() => !string.IsNullOrEmpty(Name) && Age > 0;
}

public class Record : IEquatable<Record>
{
    private readonly string <Name>k__BackingField;

    private readonly int <Age>k__BackingField;

    protected virtual Type EqualityContract
    {
        get
        {
            return typeof(Record);
        }
    }

    public string Name
    {
        get
        {
            return <Name>k__BackingField;
        }
        init
        {
            <Name>k__BackingField = value;
        }
    }

    public int Age
    {
        get
        {
            return <Age>k__BackingField;
        }
        init
        {
            <Age>k__BackingField = value;
        }
    }

    public Record(string Name, int Age)
    {
        <Name>k__BackingField = Name;
        <Age>k__BackingField = Age;
        base..ctor();
    }

    public bool IsValid()
    {
        if (!string.IsNullOrEmpty(Name))
        {
            return Age > 0;
        }
        return false;
    }

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

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

    public static bool operator !=(Record left, Record right)
    {
        return !(left == right);
    }

    public static bool operator ==(Record left, Record right)
    {
        if ((object)left != right)
        {
            if ((object)left != null)
            {
                return left.Equals(right);
            }
            return false;
        }
        return true;
    }

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

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

    public virtual bool Equals(Record other)
    {
        if ((object)this != other)
        {
            if ((object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(<Name>k__BackingField, other.<Name>k__BackingField))
            {
                return EqualityComparer<int>.Default.Equals(<Age>k__BackingField, other.<Age>k__BackingField);
            }
            return false;
        }
        return true;
    }

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

    protected Record(Record original)
    {
        <Name>k__BackingField = original.<Name>k__BackingField;
        <Age>k__BackingField = original.<Age>k__BackingField;
    }

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

それぞれのプロパティでinitが利用されていたり、Equalメソッドでちゃんとプロパティが考慮されていたりすることが伺えます。

record struct

public record struct RecordStruct(string Name, int Age)
{
    public bool IsValid() => !string.IsNullOrEmpty(Name) && Age > 0;
}

public struct RecordStruct : IEquatable<RecordStruct>
{
    private string <Name>k__BackingField;

    private int <Age>k__BackingField;

    public string Name
    {
        get
        {
            return <Name>k__BackingField;
        }
        set
        {
            <Name>k__BackingField = value;
        }
    }

    public int Age
    {
        get
        {
            return <Age>k__BackingField;
        }
        set
        {
            <Age>k__BackingField = value;
        }
    }

    public RecordStruct(string Name, int Age)
    {
        <Name>k__BackingField = Name;
        <Age>k__BackingField = Age;
    }

    public bool IsValid()
    {
        if (!string.IsNullOrEmpty(Name))
        {
            return Age > 0;
        }
        return false;
    }

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

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append("Name = ");
        builder.Append((object)Name);
        builder.Append(", Age = ");
        builder.Append(Age.ToString());
        return true;
    }

    public static bool operator !=(RecordStruct left, RecordStruct right)
    {
        return !(left == right);
    }

    public static bool operator ==(RecordStruct left, RecordStruct right)
    {
        return left.Equals(right);
    }

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

    public override bool Equals(object obj)
    {
        if (obj is RecordStruct)
        {
            return Equals((RecordStruct)obj);
        }
        return false;
    }

    public bool Equals(RecordStruct other)
    {
        if (EqualityComparer<string>.Default.Equals(<Name>k__BackingField, other.<Name>k__BackingField))
        {
            return EqualityComparer<int>.Default.Equals(<Age>k__BackingField, other.<Age>k__BackingField);
        }
        return false;
    }

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

実はDeconstructがあるので、以下のような書き方ができたりします。

var (name, age) = new RecordStruct();

readonly record struct

public struct ReadOnlyRecordStruct : IEquatable<ReadOnlyRecordStruct>
{
    private readonly string <Name>k__BackingField;

    private readonly int <Age>k__BackingField;

    public string Name
    {
        get
        {
            return <Name>k__BackingField;
        }
        init
        {
            <Name>k__BackingField = value;
        }
    }

    public int Age
    {
        get
        {
            return <Age>k__BackingField;
        }
        init
        {
            <Age>k__BackingField = value;
        }
    }

    public ReadOnlyRecordStruct(string Name, int Age)
    {
        <Name>k__BackingField = Name;
        <Age>k__BackingField = Age;
    }

    public bool IsValid()
    {
        if (!string.IsNullOrEmpty(Name))
        {
            return Age > 0;
        }
        return false;
    }

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

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append("Name = ");
        builder.Append((object)Name);
        builder.Append(", Age = ");
        builder.Append(Age.ToString());
        return true;
    }

    public static bool operator !=(ReadOnlyRecordStruct left, ReadOnlyRecordStruct right)
    {
        return !(left == right);
    }

    public static bool operator ==(ReadOnlyRecordStruct left, ReadOnlyRecordStruct right)
    {
        return left.Equals(right);
    }

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

    public override bool Equals(object obj)
    {
        if (obj is ReadOnlyRecordStruct)
        {
            return Equals((ReadOnlyRecordStruct)obj);
        }
        return false;
    }

    public bool Equals(ReadOnlyRecordStruct other)
    {
        if (EqualityComparer<string>.Default.Equals(<Name>k__BackingField, other.<Name>k__BackingField))
        {
            return EqualityComparer<int>.Default.Equals(<Age>k__BackingField, other.<Age>k__BackingField);
        }
        return false;
    }

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

プロパティがちゃんとinitになってます。