はなちるのマイノート

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

【C#】recordの一部プロパティだけで等価かどうかを判定するように変更(Equals・GetHashCode置き換え)する方法

はじめに

recordはとても便利な糖衣構文ですが、例えば一部のプロパティだけで等価かどうか判定したいような場合がありました。例えばDDDのEntityとかですね。

実際にrecordがどういう実装がされているのかは以下の記事を見ると分かると思います。
www.hanachiru-blog.com

今回は一部プロパティだけで等価か判定させる方法を紹介したいと思います。

やり方

GetHashCodeEqualsを実装してあげます。

// 本来はIdとNameが一致しているかで判定されているが、Idだけの判定の置き換える
public readonly record struct Hoge(
    int Id,
    string Name){

    public override int GetHashCode()
    {
        return EqualityComparer<int>.Default.GetHashCode(Id);
    }
        
    public bool Equals(Hoge other)
    {
        return EqualityComparer<int>.Default.Equals(Id, other.Id);
    }
}

内部実装

上記の実装を素朴な実装にSharpLabで変換してみます。

public struct Hoge : IEquatable<Hoge>
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly int <Id>k__BackingField;

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string <Name>k__BackingField;

    public int Id
    {
        [CompilerGenerated]
        get
        {
            return <Id>k__BackingField;
        }
        [CompilerGenerated]
        init
        {
            <Id>k__BackingField = value;
        }
    }

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

    public Hoge(int Id, string Name)
    {
        <Id>k__BackingField = Id;
        <Name>k__BackingField = Name;
    }

    public override int GetHashCode()
    {
        return EqualityComparer<int>.Default.GetHashCode(Id);
    }

    public bool Equals(Hoge other)
    {
        return EqualityComparer<int>.Default.Equals(Id, other.Id);
    }

    [System.Runtime.CompilerServices.NullableContext(0)]
    [CompilerGenerated]
    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Hoge");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(' ');
        }
        stringBuilder.Append('}');
        return stringBuilder.ToString();
    }

    [System.Runtime.CompilerServices.NullableContext(0)]
    [CompilerGenerated]
    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append("Id = ");
        builder.Append(Id.ToString());
        builder.Append(", Name = ");
        builder.Append((object)Name);
        return true;
    }

    [CompilerGenerated]
    public static bool operator !=(Hoge left, Hoge right)
    {
        return !(left == right);
    }

    [CompilerGenerated]
    public static bool operator ==(Hoge left, Hoge right)
    {
        return left.Equals(right);
    }

    [System.Runtime.CompilerServices.NullableContext(0)]
    [CompilerGenerated]
    public override bool Equals(object obj)
    {
        return obj is Hoge && Equals((Hoge)obj);
    }

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

正しく置き換えられています。

ちなみに

またプライマリーコンストラクタを利用していますが、コンストラクタでValidateしたいみたいな場合は普通に下記のように定義してあげればよしなにその他を自動生成してくれます。

public readonly record struct Hoge
{
    public int Id { get; init; }
    public string Name { get; init; }

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

    public override int GetHashCode()
    {
        return EqualityComparer<int>.Default.GetHashCode(Id);
    }

    public bool Equals(Hoge other)
    {
        return EqualityComparer<int>.Default.Equals(Id, other.Id);
    }
}

public struct Hoge : IEquatable<Hoge>
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly int <Id>k__BackingField;

    [System.Runtime.CompilerServices.Nullable(1)]
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string <Name>k__BackingField;

    public int Id
    {
        [CompilerGenerated]
        get
        {
            return <Id>k__BackingField;
        }
        [CompilerGenerated]
        init
        {
            <Id>k__BackingField = value;
        }
    }

    [System.Runtime.CompilerServices.Nullable(1)]
    public string Name
    {
        [System.Runtime.CompilerServices.NullableContext(1)]
        [CompilerGenerated]
        get
        {
            return <Name>k__BackingField;
        }
        [System.Runtime.CompilerServices.NullableContext(1)]
        [CompilerGenerated]
        init
        {
            <Name>k__BackingField = value;
        }
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    public Hoge(int id, string name)
    {
        Id = id;
        Name = name;
    }

    public override int GetHashCode()
    {
        return EqualityComparer<int>.Default.GetHashCode(Id);
    }

    public bool Equals(Hoge other)
    {
        return EqualityComparer<int>.Default.Equals(Id, other.Id);
    }

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

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

    [CompilerGenerated]
    public static bool operator !=(Hoge left, Hoge right)
    {
        return !(left == right);
    }

    [CompilerGenerated]
    public static bool operator ==(Hoge left, Hoge right)
    {
        return left.Equals(right);
    }

    [CompilerGenerated]
    public override bool Equals(object obj)
    {
        return obj is Hoge && Equals((Hoge)obj);
    }
}