Open17

More Effective C# 6.0/7.0 のメモ(第1章)

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目1 アクセス可能なデータメンバーの代わりにプロパティを使おう

  • プログラミングにおける全体的な流れとして、データ(フィールド)と、その操作(メソッド)を切り離そうという物がある。
  • C#のプロパティを使えば、publicフィールドにアクセスするかのように利用でき、その実装はメソッドが行っている。
  • 例えばUserクラスの名前を「空白禁止」にしたい場合、プロパティであればプロパティNameの定義箇所のSetを書き換えれば良いが、フィールドの場合はnameに対する設定箇所を全て見る必要がある。
自動プロパティによってバッキングフィールドができる内部コード
public class Program {
    public String Name { get; set; }
}

上記のC#コードをSharpLabを通して変換すると以下が出力される

public class Program
{
    [CompilerGenerated]
    private string <Name>k__BackingField;

    public string Name
    {
        [CompilerGenerated]
        get
        {
            return <Name>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Name>k__BackingField = value;
        }
    }
}
  • ちなみにただのフィールドの場合
// 元のC#コード
public class Program {
    public String Name;
}
// SharpLabによって中間言語から再度翻訳されたC#コード
public class Program
{
    [Nullable(1)]
    public string Name;
}
インデックス参照も作成可能
public class Program
{
    public static void Main()
    {
        var sample = new SampleIndex();
        Console.WriteLine(sample[3]);
    }
}

public class SampleIndex
{
    private int[] theValues = { 10, 20, 30, 50, 80 };
    public int this[int index]
    {
        get => theValues[index];
        set => theValues[index] = value;
    }
}

出力

50
  • public string Name(フィールド)とpublic string Name { get; set; }(プロパティ)は、ソースコードとしては互換性がある(今までフィールドのNameにアクセスしていた箇所を、Nameをプロパティに変わったとしても、変更する必要がない)
    • しかし、バイナリとしては異なる。
    • ゆえ、もし途中でフィールドをプロパティに変更したら、Nameの定義側だけではなく使う側のバイナリも変更する必要がある。
    • だから最初からプロパティを利用しよう。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目2 可変データには暗黙のプロパティを使おう

  • 暗黙的プロパティを使うメリット(上の項目1で書いたことと同じだが)
    • クラスへの変更がバイナリ互換性を持つことになる
    • 検証を行う場所がただ1箇所にまとまる
  • { get; }だけのプロパティはコンストラクタから設定する
public string Name { get; }
  • バッキングフィールドに直接アクセスしないこと!フィールドの検証コードはプロパティの1箇所にまとめること。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目3 値型は可変より不変が好ましい

今回の項目のターゲット

  • 例えば住所(Address)のデータは、関連する複数のフィールドで構成されるが、全体として一つのデータである。
    • 郵便番号、県、市区町村などに整合性が必要で、何かが変更される時に他のフィールドに対しても検証が必要。
    • 何か個別のフィールドが他に影響を与えることなく変わることはない。
    • アトミックな値型と呼ぶ
  • それに対して、顧客(Customer)のデータは、名前や電話番号などの複数のデータを複合的に持っている。
  • 今回の話は住所などの部類のデータが対象。

方針

  • アトミックな値型は不変とする。更新したい場合はオブジェクトごと置き換える。
不変な値型を利用する具体的な実装
public struct Address
{
    public string Line { get; }
    public string City { get; }
    public string State { get; }
    public int Zip { get; }
    
    public Address(string line, string city, string state, int zip)
    {
        Line = line;
        City = city;
        ValidateState(state);
        State = state;
        ValidateZip(zip);
        Zip = zip;
    }

    private static void ValidateState(string state)
    {
        // 整合性が取れないデータであれば例外を発生させる
    }
    
    private static void ValidateZip(int zip) { /* 省略 */ }
}

public class Program
{
    public static void Main()
    {
        var a1 = new Address("123 Main St.", "Brooklyn", "NY", 12345);
        // 住所を変更する場合
        a1 = new Address("456 Main St.", "AAAA", "LA", 67890);
        
        Console.WriteLine(a1.Line);
    }
}
  • 不変型であっても、参照型のデータを持っている場合は、同じ箇所を参照する他の場所から内部を変更されてしまう。
    • ImmutableList<T>を利用して防御的なコピーを作ることで解決できる。
不変型に見えるが内部を変更されてしまう例と、その解決策
  • まずはだめなケース
using System.Collections.Immutable;

public struct SnackList
{
    private readonly string[] snacks;

    public SnackList(string[] snacksValue)
    {
        snacks = snacksValue;
        Console.WriteLine($"snacks.GetHashCode: {snacks.GetHashCode()}");
    }
    
    public void ShowAll()
    {
        foreach (var snack in snacks)
        {
            Console.WriteLine(snack);
        }
    }
}

public class Program
{
    public static void Main()
    {
        var snackArray = new[] { "gumi", "choko", "candy" };
        Console.WriteLine($"snackArray.GetHashCode: {snackArray.GetHashCode()}");
        
        var snackList = new SnackList(snackArray);
        snackList.ShowAll();
        Console.WriteLine("-----");
        snackArray[0] = "hard gumi";
        snackList.ShowAll();
    }
}

出力

  • 引数として渡されるsnackArrayとStructの内部のsnacksが同じHashCode
  • gumiがhard gumiに変更されている
snackArray.GetHashCode: 32854180
snacks.GetHashCode: 32854180
gumi
choko
candy
-----
hard gumi
choko
candy
  • OKなケース
using System.Collections.Immutable;

public struct SnackList
{
    // ImmutableListに変更する
    private readonly ImmutableList<string> snacks;

    public SnackList(string[] snacksValue)
    {
        snacks = snacksValue.ToImmutableList(); // ImmutableListに変更する
        Console.WriteLine($"snacks.GetHashCode: {snacks.GetHashCode()}");
    }
    
    public void ShowAll() // 同じなので省略
}

public class Program // 同じなので省略

出力

  • HashCodeは変更されている。出力は前後で変更されていない。
snackArray.GetHashCode: 32854180
snacks.GetHashCode: 43942917
gumi
choko
candy
-----
gumi
choko
candy
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目4 値型と参照型の違いを正しく理解しよう

  • 値型(struct)...データを保存する場所として使う。以下の6つの条件に全て当てはまる時に利用する。
    • この型の主な役割は、データの保存か
    • この型は不変にできるか
    • この型は小さくすべきか
    • publicインターフェースを、データメンバーをアクセスするプロパティだけで定義できるか
    • この型は、将来も決してサブクラスを持たないと確信できるか
    • この型は、将来も決してポリモーフィズム的に扱われないと確信できるか
  • 値型は値を格納し、参照型は振る舞いを格納する。
  • 参照型では、継承、可変性の管理なども容易。インターフェイスの実装時にボックス化やボックス化解除の処理がいらない。
  • struct(値型)の中に、振る舞いまで含めてしまうと問題となることがある。
  • 用途の予想に確信を持ていないときは、参照型を使う。

メモリ効率の違いの観点から

MyType[] myTypes = new MyType[100]
  • MyTypeが値型の場合、MyTypeオブジェクトの100倍のサイズのメモリ割り当てが1回行われる
  • MyTypeが値型の場合、配列の中身がnullなので、まずは1回のメモリ割り当てが発生する。その後、全てのMyTypeを初期化すると追加で100回の割り当てが行われる。1回に比べて効率は良くない
    • さらに参照型の場合、確保するヒープ領域が断片化するため、メモリ効率が悪くなる
  • だが、メモリ効率の観点は、値型か参照型かを判断する上ではそこまで重要ではない模様。

structとclassの挙動の違い

  • structでは値型なので、代入されるときにコピーされる
挙動確認実験コード
public struct MyStruct
{
    public int Num;

    public MyStruct(int num)
    {
        Num = num;
    }
}

public class MyClass
{
    public int Num;

    public MyClass(int num)
    {
        Num = num;
    }
}

public class Program
{
    public static void Main()
    {
        var myStruct = new MyStruct(10);
        var myStruct2 = myStruct;
        Console.WriteLine($"myStruct変更前: {myStruct.Num}");
        Console.WriteLine($"myStruct2変更前: {myStruct2.Num}");
        myStruct2.Num = 20;
        Console.WriteLine($"myStruct変更後: {myStruct.Num}");
        Console.WriteLine($"myStruct2変更後: {myStruct2.Num}");
        Console.WriteLine($"myStruct.GetHashCode(): {myStruct.GetHashCode()}");
        Console.WriteLine($"myStruct2.GetHashCode(): {myStruct2.GetHashCode()}");
        
        Console.WriteLine();
        
        var myClass = new MyClass(10);
        var myClass2 = myClass;
        Console.WriteLine($"myClass変更前: {myClass.Num}");
        Console.WriteLine($"myClass2変更前: {myClass2.Num}");
        myClass2.Num = 20;
        Console.WriteLine($"myClass変更後: {myClass.Num}");
        Console.WriteLine($"myClass2変更後: {myClass2.Num}");
        Console.WriteLine($"myClass.GetHashCode(): {myClass.GetHashCode()}");
        Console.WriteLine($"myClass2.GetHashCode(): {myClass2.GetHashCode()}");
    }
}

出力

myStruct変更前: 10
myStruct2変更前: 10
myStruct変更後: 10
myStruct2変更後: 20
myStruct.GetHashCode(): 271750784
myStruct2.GetHashCode(): 271750814

myClass変更前: 10
myClass2変更前: 10
myClass変更後: 20
myClass2変更後: 20
myClass.GetHashCode(): 27252167
myClass2.GetHashCode(): 27252167

あらためて、スタックとヒープの違い

https://tech.tinybetter.com/Article/175277d5-e2b9-e132-d8ac-3a0160870d2f/View

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目5 値型では0も有効な状態にしよう

  • .NETのデフォルトの初期化では、全て0に設定される。
  • 例えばenumでは、0が有効な選択肢とならないようにしてはならない。
enumで0が無効なBadケース
public enum Planet
{
    Mercury = 1,
    Venus,
    Earth,
    Mars
    // 略
}

public class Program
{
    public static void Main()
    {
        var planet = new Planet();
        Console.WriteLine($"planet: {planet}");
    }
}

出力

planet: 0
  • 最初の= 1を削除することで、
public enum Planet
{
    Mercury,
    Venus,
    Earth,
    Mars
    // 略
}

出力

planet: Mercury
  • もし初期化時点で、enumのどれか決定されておらず、かといって1つ目の選択肢を割り当てるのもおかしい場合、Noneなどを作成する。
  • Noneを、「後で更新可能だが、まだ初期化されていない値」として使う。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目6 プロパティはデータらしく実装しよう

  • プロパティを利用する開発者は、プロパティに対してデータメンバーとしての振る舞いを期待する。
    • 同じプロパティに対して繰り返し呼び出し(get)を行った時に、同じ値が返ってくる。
    • プロパティが大量の仕事をするとは想定していない。
  • プロパティのsetで、検証ロジックを挟む(例: nullチェック)などはOK
  • ちょっとした計算を挟むことも基本問題ないが、もし計算が重い処理となるのであればキャッシュを利用しておくなどの策を考えるのもあり。
  • get時にリモートのデータベースに問い合わせたり、set時にリモートのデータベースに保存したりするのは無し
    • 実行に時間がかかる
    • 例外が発生する可能性がある
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目7 匿名型やタプルは型のスコープを限定するのに使える(匿名型について)

  • オブジェクトやデータ構造を表現する型は、classやstructだけではない!匿名型やタプルを使いこなそう!

匿名型

  • 匿名型は不変である(中のプロパティはreadonlyな扱い)
  • コンパイラがクラスのようなものを生成してくれる。記述が短いので作業が少ない、クラスを書くのに比べてミスが少ない、などの利点。
  • スコープがメソッド内に限定されるので、名前空間を汚さないし!
実際のコード
public class Program
{
    public static void Main()
    {
        var anonymousPerson = new { Name = "Taro", Age = 30 };
        Console.WriteLine($"{anonymousPerson.Name} is {anonymousPerson.Age} years old.");
        // Anonymous type property 'Name' is immutable. The assignment target must be an assignable variable, property, or indexer
        // anonymousPerson.Name = "Jiro";
    }
}

出力

Taro is 30 years old.
  • 匿名型はobjectとして戻り値にすることはできるが、受け取った側でプロパティにアクセスできない
public class Program
{
    public static void Main()
    {
        var person = ReturnAnonymousType();
        // Console.WriteLine(person.Name); // コンパイルエラー
    }
    
    private static object ReturnAnonymousType()
    {
        return new { Name = "Taro", Age = 30 };
    }
}
SharpLabによって、匿名型がどうコンパイルされるかを見る
  • readonlyなprivateメンバーと、それに対するgetのみのプロパティが生成される。
  • コンストラクタでprivateメンバーに対して値を入れる
  • Equals, GetHashCode, ToStringが実装されている
public class Program
{
    public static void Main()
    {
        var anonymousPerson = new { Name = "Taro", Age = 30 };
    }
}
[CompilerGenerated]
internal sealed class <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Name>j__TPar <Name>i__Field;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Age>j__TPar <Age>i__Field;

    public <Name>j__TPar Name
    {
        get
        {
            return <Name>i__Field;
        }
    }

    public <Age>j__TPar Age
    {
        get
        {
            return <Age>i__Field;
        }
    }

    [DebuggerHidden]
    public <>f__AnonymousType0(<Name>j__TPar Name, <Age>j__TPar Age)
    {
        <Name>i__Field = Name;
        <Age>i__Field = Age;
    }

    [DebuggerHidden]
    public override bool Equals(object value)
    {
        <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar> anon = value as <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>;
        if (this != anon)
        {
            if (anon != null && EqualityComparer<<Name>j__TPar>.Default.Equals(<Name>i__Field, anon.<Name>i__Field))
            {
                return EqualityComparer<<Age>j__TPar>.Default.Equals(<Age>i__Field, anon.<Age>i__Field);
            }
            return false;
        }
        return true;
    }

    [DebuggerHidden]
    public override int GetHashCode()
    {
        return (-2097246416 * -1521134295 + EqualityComparer<<Name>j__TPar>.Default.GetHashCode(<Name>i__Field)) * -1521134295 + EqualityComparer<<Age>j__TPar>.Default.GetHashCode(<Age>i__Field);
    }

    [DebuggerHidden]
    [return: Nullable(1)]
    public override string ToString()
    {
        object[] array = new object[2];
        <Name>j__TPar val = <Name>i__Field;
        array[0] = ((val != null) ? val.ToString() : null);
        <Age>j__TPar val2 = <Age>i__Field;
        array[1] = ((val2 != null) ? val2.ToString() : null);
        return string.Format(null, "{{ Name = {0}, Age = {1} }}", array);
    }
}

public class Program
{
    public static void Main()
    {
        new <>f__AnonymousType0<string, int>("Taro", 30);
    }
}
ジェネリックを用いて匿名型を利用するメソッドを作る
public class Program
{
    public static void Main()
    {
        var sample = new { X = 3, Y = 4 };
        Console.WriteLine(sample);
        
        var result = Transform(sample, pair => new { X = pair.X * 2, Y = pair.Y * 2 });
        var result2 = Transform2(pair => new { X = pair.X * 2, Y = pair.Y * 2 }, sample);
        
        Console.WriteLine(result);
        Console.WriteLine(result2);
    }
    
    private static T Transform<T>(T element, Func<T, T> transformFunc)
    {
        return transformFunc(element);
    }

    // 引数の順番を逆にしてみる
    private static T Transform2<T>(Func<T, T> transformFunc, T element)
    {
        return transformFunc(element);
    }
}

出力

{ X = 3, Y = 4 }
{ X = 6, Y = 8 }
{ X = 6, Y = 8 }
  • Tの型が何を根拠に決まるのかを確かめるための実験
    • T elementに渡されるsample(2)引数を見て型を決定している模様(Funcではなく)
public class Program
{
    public static void Main()
    {
        // var sample = new { X = 3, Y = 4 };
        var sample2 = new { A = 3, B = 4 };

        // 以下2行はコンパイルエラー
        // Cannot resolve symbol 'X'とCannot resolve symbol 'Y'
        var result3 = Transform(sample2, pair => new { X = pair.X * 2, Y = pair.Y * 2 });
        var result4 = Transform2(pair => new { X = pair.X * 2, Y = pair.Y * 2 }, sample2);
    }
    
    private static T Transform<T>(T element, Func<T, T> transformFunc)
    {
        return transformFunc(element);
    }

    // 引数の順番を逆にしてみる
    private static T Transform2<T>(Func<T, T> transformFunc, T element)
    {
        return transformFunc(element);
    }
}

匿名型の比較について
  • Equalsメソッドを使うと、同値性の比較が実行される
public class Program
{
    public static void Main()
    {
        var taro = new { Name = "Taro", Age = 30 };
        var taro2 = new { Name = "Taro", Age = 30 };
        var jiro = new { Name = "Jiro", Age = 30 };
        
        Console.WriteLine($"taro == taro2: {taro == taro2}");
        Console.WriteLine($"taro.Equals(taro2): {taro.Equals(taro2)}");
        Console.WriteLine($"taro == jiro: {taro == jiro}");
        Console.WriteLine($"taro.Equals(jiro): {taro.Equals(jiro)}");
    }
}

出力

taro == taro2: False
taro.Equals(taro2): True
taro == jiro: False
taro.Equals(jiro): False
匿名型を複数作成した時に、コンパイラがクラスを複数作るか、共通のものと判断するか
  • 同じプロパティ名で同じ順番であれば、コンパイラは匿名型を共通化する。

共通化するケース

public class Program
{
    public static void Main()
    {
        var taro = new { Name = "Taro", Age = 30 };
        var jiro = new { Name = "Jiro", Age = 30 };
    }
}
[CompilerGenerated]
internal sealed class <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Name>j__TPar <Name>i__Field;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Age>j__TPar <Age>i__Field;

    public <Name>j__TPar Name
    {
        get
        {
            return <Name>i__Field;
        }
    }

    public <Age>j__TPar Age
    {
        get
        {
            return <Age>i__Field;
        }
    }

    [DebuggerHidden]
    public <>f__AnonymousType0(<Name>j__TPar Name, <Age>j__TPar Age)
    {
        <Name>i__Field = Name;
        <Age>i__Field = Age;
    }

    [DebuggerHidden]
    public override bool Equals(object value)
    {
        <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar> anon = value as <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>;
        if (this != anon)
        {
            if (anon != null && EqualityComparer<<Name>j__TPar>.Default.Equals(<Name>i__Field, anon.<Name>i__Field))
            {
                return EqualityComparer<<Age>j__TPar>.Default.Equals(<Age>i__Field, anon.<Age>i__Field);
            }
            return false;
        }
        return true;
    }

    [DebuggerHidden]
    public override int GetHashCode()
    {
        return (-2097246416 * -1521134295 + EqualityComparer<<Name>j__TPar>.Default.GetHashCode(<Name>i__Field)) * -1521134295 + EqualityComparer<<Age>j__TPar>.Default.GetHashCode(<Age>i__Field);
    }

    [DebuggerHidden]
    [return: Nullable(1)]
    public override string ToString()
    {
        object[] array = new object[2];
        <Name>j__TPar val = <Name>i__Field;
        array[0] = ((val != null) ? val.ToString() : null);
        <Age>j__TPar val2 = <Age>i__Field;
        array[1] = ((val2 != null) ? val2.ToString() : null);
        return string.Format(null, "{{ Name = {0}, Age = {1} }}", array);
    }
}

public class Program
{
    public static void Main()
    {
        new <>f__AnonymousType0<string, int>("Taro", 30);
        new <>f__AnonymousType0<string, int>("Jiro", 30);
    }
}

共通化されずに複数作られるケース

public class Program
{
    public static void Main()
    {
        var taro = new { Name = "Taro", Age = 30 };
        var jiro = new { Name = "Jiro", Age = 30 };
        var saburo = new { Age = 30, Name = "Saburo" };
    }
}
[CompilerGenerated]
internal sealed class <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Name>j__TPar <Name>i__Field;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Age>j__TPar <Age>i__Field;

    public <Name>j__TPar Name
    {
        get
        {
            return <Name>i__Field;
        }
    }

    public <Age>j__TPar Age
    {
        get
        {
            return <Age>i__Field;
        }
    }

    [DebuggerHidden]
    public <>f__AnonymousType0(<Name>j__TPar Name, <Age>j__TPar Age)
    {
        <Name>i__Field = Name;
        <Age>i__Field = Age;
    }

    [DebuggerHidden]
    public override bool Equals(object value)
    {
        <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar> anon = value as <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>;
        if (this != anon)
        {
            if (anon != null && EqualityComparer<<Name>j__TPar>.Default.Equals(<Name>i__Field, anon.<Name>i__Field))
            {
                return EqualityComparer<<Age>j__TPar>.Default.Equals(<Age>i__Field, anon.<Age>i__Field);
            }
            return false;
        }
        return true;
    }

    [DebuggerHidden]
    public override int GetHashCode()
    {
        return (-2097246416 * -1521134295 + EqualityComparer<<Name>j__TPar>.Default.GetHashCode(<Name>i__Field)) * -1521134295 + EqualityComparer<<Age>j__TPar>.Default.GetHashCode(<Age>i__Field);
    }

    [DebuggerHidden]
    [return: Nullable(1)]
    public override string ToString()
    {
        object[] array = new object[2];
        <Name>j__TPar val = <Name>i__Field;
        array[0] = ((val != null) ? val.ToString() : null);
        <Age>j__TPar val2 = <Age>i__Field;
        array[1] = ((val2 != null) ? val2.ToString() : null);
        return string.Format(null, "{{ Name = {0}, Age = {1} }}", array);
    }
}

[CompilerGenerated]
internal sealed class <>f__AnonymousType1<<Age>j__TPar, <Name>j__TPar>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Age>j__TPar <Age>i__Field;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <Name>j__TPar <Name>i__Field;

    public <Age>j__TPar Age
    {
        get
        {
            return <Age>i__Field;
        }
    }

    public <Name>j__TPar Name
    {
        get
        {
            return <Name>i__Field;
        }
    }

    [DebuggerHidden]
    public <>f__AnonymousType1(<Age>j__TPar Age, <Name>j__TPar Name)
    {
        <Age>i__Field = Age;
        <Name>i__Field = Name;
    }

    [DebuggerHidden]
    public override bool Equals(object value)
    {
        <>f__AnonymousType1<<Age>j__TPar, <Name>j__TPar> anon = value as <>f__AnonymousType1<<Age>j__TPar, <Name>j__TPar>;
        if (this != anon)
        {
            if (anon != null && EqualityComparer<<Age>j__TPar>.Default.Equals(<Age>i__Field, anon.<Age>i__Field))
            {
                return EqualityComparer<<Name>j__TPar>.Default.Equals(<Name>i__Field, anon.<Name>i__Field);
            }
            return false;
        }
        return true;
    }

    [DebuggerHidden]
    public override int GetHashCode()
    {
        return (-1032811872 * -1521134295 + EqualityComparer<<Age>j__TPar>.Default.GetHashCode(<Age>i__Field)) * -1521134295 + EqualityComparer<<Name>j__TPar>.Default.GetHashCode(<Name>i__Field);
    }

    [DebuggerHidden]
    [return: Nullable(1)]
    public override string ToString()
    {
        object[] array = new object[2];
        <Age>j__TPar val = <Age>i__Field;
        array[0] = ((val != null) ? val.ToString() : null);
        <Name>j__TPar val2 = <Name>i__Field;
        array[1] = ((val2 != null) ? val2.ToString() : null);
        return string.Format(null, "{{ Age = {0}, Name = {1} }}", array);
    }
}

public class Program
{
    public static void Main()
    {
        new <>f__AnonymousType0<string, int>("Taro", 30);
        new <>f__AnonymousType0<string, int>("Jiro", 30);
        new <>f__AnonymousType1<int, string>(30, "Saburo");
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目7 匿名型やタプルは型のスコープを限定するのに使える(タプル型について)

コード
public class Program
{
    public static void Main()
    {
        var sample = (X: 3, Y: 4);
        Console.WriteLine($"sample: {sample}");
        Console.WriteLine($"X: {sample.X}, Y: {sample.Y}");
        Console.WriteLine($"sample.GetType: {sample.GetType()}");
    }
}

出力

sample: (3, 4)
X: 3, Y: 4
sample.GetType: System.ValueTuple`2[System.Int32,System.Int32]
  • 別の型でタプルを作ることも可能
public class Program
{
    public static void Main()
    {
        var sample = (Name: "Taro", Age: 30);
        Console.WriteLine($"sample: {sample}");
        Console.WriteLine($"sample.GetType: {sample.GetType()}");
    }
}

出力

sample: (Taro, 30)
sample.GetType: System.ValueTuple`2[System.String,System.Int32]
SharpLabによるコンパイル後の形
public class Program
{
    public static void Main()
    {
        var sample = (Name: "Taro", Age: 30);
        System.Console.WriteLine(sample);
    }
}
public class Program
{
    public static void Main()
    {
        Console.WriteLine(new ValueTuple<string, int>("Taro", 30));
    }
}

タプル型の比較について
  • 匿名型と異なり、==でもtrueと判定される
public class Program
{
    public static void Main()
    {
        var taro = (Name: "Taro", Age: 30);
        var taro2 = (Name: "Taro", Age: 30);
        var jiro = (Name: "Jiro", Age: 30);
        
        Console.WriteLine($"taro == taro2 : {taro == taro2}");
        Console.WriteLine($"taro.Equals(taro2) : {taro.Equals(taro2)}");
        Console.WriteLine($"taro == jiro : {taro == jiro}");
        Console.WriteLine($"taro.Equals(jiro) : {taro.Equals(jiro)}");
    }
}
taro == taro2 : True
taro.Equals(taro2) : True
taro == jiro : False
taro.Equals(jiro) : False
  • タプルでは、ラベルは同一性、同値性判定に影響を与えない
public class Program
{
    public static void Main()
    {
        var sampleTuple = (X: 5, Y: 10);
        var sampleTuple2 = (A: 5, B: 10);
        Console.WriteLine($"sampleTuple == sampleTuple2: {sampleTuple == sampleTuple2}");
        Console.WriteLine($"sampleTuple.Equals(sampleTuple2): {sampleTuple.Equals(sampleTuple2)}");
        
        var sampleAnonymous = new { X = 5, Y = 10 };
        var sampleAnonymous2 = new { A = 5, B = 10 };
        // 以下の行はコンパイルエラー: Cannot apply operator '==' to operands of type '{int X, int Y}' and '{int A, int B}'
        // Console.WriteLine($"sampleAnonymous == sampleAnonymous2: {sampleAnonymous == sampleAnonymous2}");
        Console.WriteLine($"sampleAnonymous.Equals(sampleAnonymous2): {sampleAnonymous.Equals(sampleAnonymous2)}");
    }
}

出力

sampleTuple == sampleTuple2: True
sampleTuple.Equals(sampleTuple2): True
sampleAnonymous.Equals(sampleAnonymous2): False
タプルは可変である
public class Program
{
    public static void Main()
    {
        var sample = (X: 5, Y: 10);
        Console.WriteLine(sample);
        sample.X = 6;
        Console.WriteLine(sample);
    }
}

出力

(5, 10)
(6, 10)
タプルの名前は左辺が優先される
public class Program
{
    public static void Main()
    {
        var sample = (X: 5, Y: 10);
        Console.WriteLine(sample.X);
        
        var sample2 = sample;
        Console.WriteLine(sample2.X);
        
        (int A, int B) sample3 = sample;
        Console.WriteLine(sample3.A); //sample3.Xはコンパイルエラー Cannot resolve symbol 'X'
    }
}

出力

5
5
5
ValueTupleとTuple
  • ValueTupleでは名前を設定可能。また、ToTupleでタプルになる。
public class Program
{
    public static void Main()
    {
        var valueTuple = ReturnValueTuple();
        Console.WriteLine(valueTuple);
        Console.WriteLine(valueTuple.X);
        Console.WriteLine(valueTuple.Item3);
        Console.WriteLine(valueTuple.GetType());
        
        Console.WriteLine();
        
        var toTupled = valueTuple.ToTuple();
        Console.WriteLine(toTupled);
        // Console.WriteLine(toTupled.X); // 名前にアクセスしようとするとコンパイルエラー
        Console.WriteLine(toTupled.Item3);
        Console.WriteLine(toTupled.GetType());
        
        Console.WriteLine();
        var tuple = ReturnTuple();
        Console.WriteLine(tuple);
        Console.WriteLine(tuple.Item3);
        Console.WriteLine(tuple.GetType());
    }

    private static (int X, int Y, string Z) ReturnValueTuple()
    {
        return (X: 10, Y: 20, Z: "Hello");
    }
    
    private static Tuple<int, int, string> ReturnTuple()
    {
        return Tuple.Create(10, 20, "Hello");
    }
}

出力

(10, 20, Hello)
10
Hello
System.ValueTuple`3[System.Int32,System.Int32,System.String]

(10, 20, Hello)
Hello
System.Tuple`3[System.Int32,System.Int32,System.String]

(10, 20, Hello)
Hello
System.Tuple`3[System.Int32,System.Int32,System.String]
  • ValueTupleは値型なのでnullを許容しないが、Tupleは参照型なので許容する
public class Program
{
    public static void Main()
    {
        // ValueTupleは値型で、nullを許容しない
        (int, string) vt = (1, "Hello");
        // 以下はエラー
        // Cannot convert initializer type 'null' to target type '(int, string)'
        // (int, string) vt2 = null;
        
        // Tupleは参照型で、nullを許容する
        Tuple<int, string> t = Tuple.Create(1, "Hello");
        Tuple<int, string> t2 = null;
    }
}
  • ValueTupleはミュータブル。Tupleと匿名型はイミュータブル
public class Program
{
    public static void Main()
    {
        (int, string) vt = (1, "Hello");
        vt.Item1 = 2;
        Console.WriteLine(vt); // 出力: (2, Hello)
        
        Tuple<int, string> t = Tuple.Create(3, "Hello");
        // 以下はエラー The property 'System.Tuple<T1,T2>.Item1' has no setter
        // t.Item1 = 4;
        
        var ano = new { Id = 5, Name = "Hello" };
        // 以下はエラー
        // Anonymous type property 'Id' is immutable. The assignment target must be an assignable variable, property, or indexer
        // ano.Id = 6;
    }
}
  • 詳細はこちら

https://qiita.com/dyoneda/items/e45fcd9a379922e8484a

匿名型 or タプル

  • 匿名型
    • 不変なので、コレクションの複合キーに適している。
    • 中関結果を追跡する必要があって、しかもその結果が不変型のモデルに適合する
  • タプル型
    • 構造的な型付けに従う(シグネチャで型を判断される)ので、メソッドの戻り値や引数に適している(匿名型だと型が決まらないから戻り値や引数にするのは難しい。ジェネリックを使って打開するなど)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目8 匿名型にローカル関数を定義する

  • タプルと違って匿名型は戻り値や引数としにくいが、ジェネリックを利用することで可能。
  • 同じ匿名型を3回以上使ったり、ロジックが複雑になった場合には、匿名型をやめて具体的なクラス名などをつけよう。
ジェネリックメソッドを作ることで引数に匿名型を渡す例
public class Program
{
    public static void Main()
    {
        // BEGIN: IEnumerable<{Number,Word}>の素材を作る準備
        var dict = new Dictionary<int, string>
        {
            { 1, "one" },
            { 2, "two" },
            { 3, "three" },
        };
        
        var nums = new List<int> { 1, 2, 3 };
        var tupleEnumerable = from n in nums
            select new { Number = n, Word = dict[n] };
        foreach(var item in tupleEnumerable)
        {
            Console.WriteLine(item);
        }
        
        Console.WriteLine();
        // END: IEnumerable<{Number,Word}>の素材を作る準備
        
        var result = from n in FindValue(tupleEnumerable, new { Number = 2, Word = "two" })
            select n;
        
        foreach (var item in result)
        {
            Console.WriteLine(item);
        }
    }

    // ジェネリックとすることで引数に匿名型を渡すことができる
    static IEnumerable<T> FindValue<T>(IEnumerable<T> enumerable, T value)
    {
        foreach (T element in enumerable)
        {
            if (element.Equals(value))
            {
                yield return element;
            }
        }
    }
}

出力

{ Number = 1, Word = one }
{ Number = 2, Word = two }
{ Number = 3, Word = three }

{ Number = 2, Word = two }
  • 型推論にも注目
ジェネリックを使う例2
public class Program
{
    public static void Main()
    {
        var nums = new List<int> { 1, 2, 3, 4, 5 };

        var sequence = (from x in nums
            let y = x * x
            select new { x, y })
            .TakeWhile(pair => pair.y < 15);
// TakeWhileのシグネチャで、戻り値にも引数にもTSourceが使われているため、匿名型のプロパティにアクセス可能。↓定義元
// public static IEnumerable<TSource> TakeWhile<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

        foreach (var item in sequence)
        {
            Console.WriteLine(item);
        }
    }
}

出力

{ x = 1, y = 1 }
{ x = 2, y = 4 }
{ x = 3, y = 9 }
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目9 さまざまな同一性が、どういう関係にあるかを把握しよう

  • staticなObject.ReferenceEquals()と、Object.Equals()を再定義することは決して無い
Object.ReferenceEqualsとObject.Equalsの内部実装
  • Object.ReferenceEquals
Object.cs
// 参照の同一性を検証する
[NonVersionable]
public static bool ReferenceEquals(object? objA, object? objB)
{
    return objA == objB;
}
  • staticのObject.Equalsと、インスタンスメソッドのEquals
Object.cs
// デフォルトのEqualsメソッドは、Object.ReferenceEqualsと全く同じように振る舞う。これが自分の型にとってふさわしくない時にoverrideする。
public virtual bool Equals(object? obj)
{
    return this == obj;
}

public static bool Equals(object? objA, object? objB)
{
    // ReferenceEqualsと同じ記述。参照の同一性を検証する。
    if (objA == objB)
    {
        return true;
    }
    // 両方がnullの場合は、上の行でtrue判定される。// Console.WriteLine(ReferenceEquals(null, null)); => true
    if (objA == null || objB == null)
    {
        return false;
    }
    // その型のEqualsインスタンスメソッドに移譲する。
    return objA.Equals(objB);
}

値型について

  • 2つの値型は(の参照先は)、「もし2つが同じ型で」「内容も同じならば」等しい。
    • ValueType.Equalsはそのふるまいを実装する。
    • 基底クラスのValueTypeが実装するEqualsでは、全ての派生クラスに対応できるように、リフレクションを使って全てのメンバーフィールドの値をかくにんしているため効率が良くない。同一性の判定は高頻度で行われるのでパフォーマンスは大切にしたい。
  • よって、値型を作る場合は、ValueType.Equalsのオーバーライドを作成するべきである。
値型のEqualsの内部実装
  • ValueTypeクラス: structキーワードを使って作る全ての値型の基底クラス
  • ObjectクラスのEqualsメソッドpublic virtual bool Equals(object? obj)をoverrideしている。
ValueType.cs
/*============================================================
**
**
**
** Purpose: Base class for all value classes.
**
**
===========================================================*/

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace System
{
    [Serializable]
    [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public abstract class ValueType
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern",
            Justification = "Trimmed fields don't make a difference for equality")]
        public override unsafe bool Equals([NotNullWhen(true)] object? obj)
        {
            if (null == obj)
            {
                return false;
            }

            if (GetType() != obj.GetType())
            {
                return false;
            }

            // if there are no GC references in this object we can avoid reflection
            // and do a fast memcmp
            if (CanCompareBits(this))
            {
                return SpanHelpers.SequenceEqual(
                    ref RuntimeHelpers.GetRawData(this),
                    ref RuntimeHelpers.GetRawData(obj),
                    RuntimeHelpers.GetMethodTable(this)->GetNumInstanceFieldBytes());
            }

            FieldInfo[] thisFields = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

            for (int i = 0; i < thisFields.Length; i++)
            {
                object? thisResult = thisFields[i].GetValue(this);
                object? thatResult = thisFields[i].GetValue(obj);

                if (thisResult == null)
                {
                    if (thatResult != null)
                        return false;
                }
                else
                if (!thisResult.Equals(thatResult))
                {
                    return false;
                }
            }

            return true;
        }
int型での派生
  • intのインスタンスメソッドのEquals
Int32.cs
        // override元はValueType.Equalsであり、さらにその上のObject.Equalsである。
        public override bool Equals([NotNullWhen(true)] object? obj)
        {
            if (!(obj is int))
            {
                return false;
            }
            return m_value == ((int)obj).m_value;
        }

        [NonVersionable]
        public bool Equals(int obj)
        {
            return m_value == obj;
        }
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目9 さまざまな同一性が、どういう関係にあるかを把握しよう(実験コード)

値型(int)の比較
public class Program
{
    public static void Main()
    {
        int i = 5;
        int j = 5;
        
        // iとjの比較
        Console.WriteLine($"Object.ReferenceEquals(i, j): {Object.ReferenceEquals(i, j)}");
        Console.WriteLine($"Object.Equals(i, j): {Object.Equals(i, j)}");
        Console.WriteLine($"i.Equals(j): {i.Equals(j)}");
        Console.WriteLine($"i == j: {i == j}");
        
        Console.WriteLine();
        
        // iとiの比較
        // ReferenceEqualsがFalseとなるのは、ボックス化による影響(それぞれ別々にボックス化するので参照が異なる)
        Console.WriteLine($"Object.ReferenceEquals(i, i): {Object.ReferenceEquals(i, i)}");
        Console.WriteLine($"Object.Equals(i, i): {Object.Equals(i, i)}");
        Console.WriteLine($"i.Equals(i): {i.Equals(i)}");
        Console.WriteLine($"i == i: {i == i}");
    }
}

出力

Object.ReferenceEquals(i, j): False
Object.Equals(i, j): True
i.Equals(j): True
i == j: True

Object.ReferenceEquals(i, i): False
Object.Equals(i, i): True
i.Equals(i): True
i == i: True
  • 参照型(レコード型を除く)の==は、ReferenceEqualsと同じで参照の等価性(同一性)を検証する
実験コード
public class Program
{
    public static void Main()
    {
        var a = new MyClass(1);
        var b = new MyClass(1);
        var c = a;
        
        Console.WriteLine($"a == b: {a == b}"); // False
        Console.WriteLine($"ReferenceEquals(a, b): {ReferenceEquals(a, b)}"); // False
        Console.WriteLine($"a == c: {a == c}"); // True
        Console.WriteLine($"ReferenceEquals(a, c): {ReferenceEquals(a, c)}"); // True

    }
}

public class MyClass
{
    private int id;

    public MyClass(int idParam)
    {
        id = idParam;
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目9に関連する、マイクロソフト公式ドキュメント

https://learn.microsoft.com/ja-jp/dotnet/csharp/programming-guide/statements-expressions-operators/equality-comparisons

  • 「等価」は広い概念

2 つの値が等しいかどうかを比較することが必要な場合があります。 そのような場合、値が等価であること (等価性と呼ばれる) をテストすることになります。

参照の等価性(同一性)

参照の等価とは、2 つのオブジェクト参照が同一の基になるオブジェクトを参照していることを意味します。

2つの変数がメモリ内の同一の基になるオブジェクトを参照しているかどうかを確認する必要がある場合もあります。 このタイプの等価は、参照の等価性または同一性と呼ばれます。

2つの参照が同じオブジェクトを参照しているかどうかを判断するには、ReferenceEquals メソッドを使用します。

参照の等価性の概念は参照型のみに適用されます。 値型オブジェクトには参照の等価性がありません。これは、値型のインスタンスが変数に代入される場合、値のコピーが作成されるためです。 そのため、ボックス化を解除した 2 つの構造体でメモリ内の同じ場所を参照することはできません。 さらに、ReferenceEquals を使用して 2 つの値型を比較する場合、オブジェクトに含まれている値がすべて同一である場合でも、結果は常に false になります。 これは、各変数が個別のオブジェクト インスタンスにボックス化されているためです。

値の等価性

値が等価であるとは、2 つのオブジェクトが同じ値を含むことを意味します。 int、bool などのプリミティブ値型では、値が等価であることをテストするのは簡単です。 次の例に示すように、== 演算子を使用できます。

それ以外のほとんどの型については、値が等価であることをテストするのは、もっと複雑です。特定の型で等価性がどのように定義されるかを理解する必要があるからです。 複数のフィールドまたはプロパティを含むクラスおよび構造体では、多くの場合、値が等価であるとは、すべてのフィールドまたはプロパティが同一の値を含むことであると定義されます。

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目9: インスタンスのEquals()メソッドのオーバーライドについて

  • 参照の等価性(同一性)ではなく、値の等価性で判断したい場合に、インスタンスのObject.Equals()をオーバーライドするべき。
    • デフォルトでは、Object.ReferenceEqualsと全く同じように振る舞う(参照の等価性を判定する)

オーバーライドの一般的な例

  1. 右辺のnullチェック
  2. 左辺と右辺の参照の等価性(同一性)のチェック
  3. 左辺と右辺の型が一致しているかチェック
  4. 定義しているクラスの型でEquals
Equalsのオーバーライドの一般的な例
public class Foo : IEquatable<Foo>
{
    public override bool Equals(object? right)
    {
        // rightのnullチェック。nullなら早くreturn falseしたい。
        // leftがnullであった場合はNullReferenceExceptionが発生するので、このメソッドの中では調べる必要がない
        if (object.ReferenceEquals(right, null))
        {
            return false;
        }

        // 同一性チェック。同じオブジェクトなら早くreturn trueしたい。
        // これは効率が良いチェック。オブジェクトの各フィールドを比べなくても、メモリアドレスだけ見て判斷ができる
        if (object.ReferenceEquals(this, right))
        {
            return true;
        }
        
        // 型が一致していない場合は、もしその先のEqualsでtrueと判定される可能性があったとしても(フィールド比較なら同値となりうる)
        // 型が違うのでこの段階でfalseとする。
        if (this.GetType() != right.GetType())
        {
            return false;
        }
        // もしこの部分が型チェックではなくてキャストだった場合、Fooの派生クラスは基底のFooクラスとなり得るのでreturnせずに通過する
        // その場合、Foo型とFooの派生型が比較されることになり、Equalsがtrueとなる場合がある。しかし、型が同じではないものは等しくないと判断するべきである。
        // Foo rightAsFoo = right as Foo;
        // if (rightAsFoo == null) return false;
        
        // また、Fooの派生クラスをFooChildとした場合、FooChildのこの部分をキャストを使って書くと以下のようになる。
        // FooChild rightAsChild = right as FooChild;
        // もしrightとしてFooクラスのインスタンスが渡された場合、rightAsChildはnullとなり、return falseされる。
        // このことより、fooChild.Equals(foo)とfoo.Equals(fooChild)は結果が、型の自動変換が原因で異なる。
        // これは、Equalsメソッドの対象性を破ってしまっているので不適切である。

        return this.Equals(right as Foo);
    }

    public bool Equals(Foo? other)
    {
        // 省略
        return true;
    }
}

以下のコードはEquals実装において、基底と派生の順番で結果が違う例を作りたかったが、オーバーロードの関係で意図しないコードが呼ばれて実験できなかった。そのコードの墓場で置いておく。
public class Parent : IEquatable<Parent>
{
    public override bool Equals(object? right)
    {
        Console.WriteLine("Parent.Equals(object)");
    
        // nullチェックと同一性チェック
        if (object.ReferenceEquals(right, null)) return false;
        if (object.ReferenceEquals(this, right)) return true;
    
        Parent rightAsParent = right as Parent;
        if (rightAsParent == null) return false;
    
        return this.Equals(rightAsParent);
    }
    
    public bool Equals(Parent? other)
    {
        Console.WriteLine("Parent.Equals(Parent)");
        return true; // 省略
    }
}

public class Child : Parent, IEquatable<Child>
{
    public override bool Equals(object? right)
    {
        Console.WriteLine("Child.Equals(object)");
        // nullチェックと同一性チェック
        if (object.ReferenceEquals(right, null)) return false;
        if (object.ReferenceEquals(this, right)) return true;

        Child rightAsChild = right as Child;
        if (rightAsChild == null) return false;
        
        return this.Equals(rightAsChild);
    }
    
    public bool Equals(Child? other)
    {
        Console.WriteLine("Child.Equals(Child)");
        return true; // 省略
    }
}

public class Program
{
    public static void Main()
    {
        var parent = new Parent();
        var child = new Child();
        
        Console.WriteLine("== parent.Equals(parent) ==");
        Console.WriteLine(parent.Equals(parent));
        Console.WriteLine("== parent.Equals(child) ==");
        Console.WriteLine(parent.Equals(child));
        Console.WriteLine("== child.Equals(child) ==");
        Console.WriteLine(child.Equals(child));
        Console.WriteLine("== child.Equals(parent) ==");
        Console.WriteLine(child.Equals(parent));
    }
}

出力

== parent.Equals(parent) ==
Parent.Equals(Parent)
True
== parent.Equals(child) ==
Parent.Equals(Parent)
True
== child.Equals(child) ==
Child.Equals(Child)
True
== child.Equals(parent) ==
Parent.Equals(Parent)
True
  • Equals()をオーバーライドするなら、IEquatable<T>を実装すること。
    • その型Tが、型安全な同一性比較をサポートすることを示す。

方針まとめ

  • 値型を作るときは、Equals()をオーバーライドしよう。
    • 合わせて、operetor==()もオーバーライドしよう。
  • 参照型を作るときは、Equals()が参照の等価性を判定するのが嫌であれば、Equals()をオーバーライド仕様。
  • オーバーライドするのであれば上記の一般的な実装手順に従うこと。また、GetHashCode()もオーバーライドすること。
  • Equals()をオーバーライドするときは、必ずIEquatable()を実装すること。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目10 GetHashCode()の罠に注意

  • GetHashCode関数は、ハッシュをベースとするコレクションで使うキーのハッシュ値を定義するために利用される。
    • ハッシュ値の集合ごとにバケットを作り、そこにオブジェクトを格納する。そのバケットの中だけを検索すれば良いので検索効率が高まる、というのがハッシュコードの目的。

GetHashCodeが達成するべき3つのルール

  1. 2つのオブジェクトがインスタンスEquals()で等しいと判定されたら、同じハッシュ値を生成しなければならない。
  2. オブジェクトAは常に同じハッシュ値を返す
  3. あらゆるオブジェクトのハッシュ値は、一様に分散される。

System.ObjectのGetHashCode

  • System.Objectは参照型で、Object.GetHashCode()ではクラスの内部フィールドを使ってハッシュ値が生成される。
  • operator==()やインスタンスEquals()では、デフォルトでは参照の等価性で判定される。よってルール1を満たす。もしEquals()をオーバーライドするならGetHashCode()も変更する必要がある。
  • System.ObjectのGetHashCode()では、変化することのない「オブジェクトID」を使っている。よってルール2を満たす。
  • ルール3については詳しく書かれていないが、System.Objectの実装はいい感じらしい。

System.ValueTypeのGetHashCode

  • 型で定義されている最初のフィールドからハッシュコードを生成する
  • もし、Equalsで第一に定義されるフィールドが使われないなどすると、Equalsでは等しいと判定されるが第一フィールドが変わっていて、その結果ハッシュコードも変わるということが発生する。これはルール1に違反する。
  • ルール2に関しても、structの最初のフィールドが不変でなければならない。途中で変えることができたら達成されない。(実験コードにある)
  • ルール3については、最初のフィールドに一様に値が入るようになってしまうので怪しい。フィールドの型と使い方に依存する。

どうしたら正しいGetHashCode()が実装できるか

  • System.ValueTypeの場合: ルール1を守るために、Equals()の判定に利用されるフィールドは全てハッシュコードの生成にも使われるべき。
    • Equals()判定に使われていないフィールドでハッシュコードが生成されるとルール1に違反する可能性がある。
  • ルール2を守るために、GetHashCodeを上書きするとしたら(参照型を上書きするとしたら、オブジェクトIDを使わなくなるとしたら)、その際に利用するフィールドは不変なものとすること。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目10 GetHashCode()の実験コード

実験コードたち

structのGetHashCode
  • 本では、この2つは同じ値になるって書いてあったんだけど違うのはなんでだろう??
public class Program
{
    public static void Main()
    {
        var myStruct = new MyStruct("Hello", 1);
        Console.WriteLine(myStruct.GetHashCode()); // -705423093
        Console.WriteLine(myStruct.Msg.GetHashCode()); // -1002108453
    }
}

public struct MyStruct
{
    public string Msg;
    private int _id;

    public MyStruct(string msg, int id)
    {
        Msg = msg;
        _id = id;
    }
}

classとstructのGetHashCodeの値の実験
  • GetHashCodeの値は毎回変わるので、とある実行時の結果
public class Program
{
    public static void Main()
    {
        var taro1 = new MyStruct("Hello, I am Taro", 1);
        var taro2 = new MyStruct("Hello, I am Taro", 2);
        var taro1a = new MyStruct("Hello, I am Taro", 1);
        var jiro1 = new MyStruct("Hello, I am Jiro", 1);
        Console.WriteLine($"taro1.GetHashCode(): {taro1.GetHashCode()}"); // 334636704
        Console.WriteLine($"taro2.GetHashCode(): {taro2.GetHashCode()}"); // 334636704
        Console.WriteLine($"taro1a.GetHashCode(): {taro1a.GetHashCode()}"); // 334636704
        Console.WriteLine($"jiro1.GetHashCode(): {jiro1.GetHashCode()}"); // 1297748808
        
        Console.WriteLine($"taro1.Equals(taro2): {taro1.Equals(taro2)}"); // False
        Console.WriteLine($"taro1.Equals(taro1a): {taro1.Equals(taro1a)}"); // True
        Console.WriteLine($"taro1.Equals(jiro1): {taro1.Equals(jiro1)}"); // False
        
        Console.WriteLine();
        
        var classTaro1 = new MyClass("Hello, I am Taro", 1);
        var classTaro2 = new MyClass("Hello, I am Taro", 2);
        var classTaro1a = new MyClass("Hello, I am Taro", 1);
        var classJiro1 = new MyClass("Hello, I am Jiro", 1);
        Console.WriteLine($"classTaro1.GetHashCode(): {classTaro1.GetHashCode()}"); // 27252167
        Console.WriteLine($"classTaro2.GetHashCode(): {classTaro2.GetHashCode()}"); // 43942917
        Console.WriteLine($"classTaro1a.GetHashCode(): {classTaro1a.GetHashCode()}"); // 59941933
        Console.WriteLine($"classJiro1.GetHashCode(): {classJiro1.GetHashCode()}"); // 2606490
        
        Console.WriteLine($"classTaro1.Equals(classTaro2): {classTaro1.Equals(classTaro2)}"); // False
        Console.WriteLine($"classTaro1.Equals(classTaro1a): {classTaro1.Equals(classTaro1a)}"); // False
        Console.WriteLine($"classTaro1.Equals(classJiro1): {classTaro1.Equals(classJiro1)}"); // False
    }
}

public struct MyStruct
{
    private string _msg;
    private int _id;
    
    public MyStruct(string msg, int id)
    {
        _msg = msg;
        _id = id;
    }
}

public class MyClass
{
    private string _msg;
    private int _id;
    
    public MyClass(string msg, int id)
    {
        _msg = msg;
        _id = id;
    }
}
メンバーフィールドが書き換わった際のHashCodeの変化
  • 参照型(class)ではフィールドを書き換えてもHashCodeの値は変わらない。
  • 値型のstructでは、1つ目のフィールドを変更することでHashCodeが変化する。
  • ちなみに、MyStructで、Msgよりも前に_idを定義すると、HashCodeの値は変わらない。
    • やはり最初に定義されている値が利用されていることがわかる。
public class Program
{
    public static void Main()
    {
        var structTaro = new MyStruct("Hello, I am Taro", 1);
        Console.WriteLine(structTaro.GetHashCode()); // 429014959
        structTaro.Msg = "Hi!, I am Taro";
        Console.WriteLine(structTaro.GetHashCode()); // -595405511
        
        Console.WriteLine();
        
        var classTaro = new MyClass("Hello, I am Taro", 1);
        Console.WriteLine(classTaro.GetHashCode()); // 32854180
        classTaro.Msg = "Hi!, I am Taro";
        Console.WriteLine(classTaro.GetHashCode()); // 32854180
    }
}

public struct MyStruct
{
    public string Msg;
    private int _id;
    
    public MyStruct(string msg, int id)
    {
        Msg = msg;
        _id = id;
    }
}

public class MyClass
{
    public string Msg;
    private int _id;
    
    public MyClass(string msg, int id)
    {
        Msg = msg;
        _id = id;
    }
}
classのGetHashCodeを、可変なフィールドを利用するように上書きした場合
  • GetHashCodeを上書きしなければ、不変なオブジェクトIDを利用するためHashCodeは変わらない。
  • しかし、Msgフィールドを利用するように上書きした。その上で、Msgを途中で変更すると、HashCodeも変わった。
public class Program
{
    public static void Main()
    {
        var classTaro = new MyClass("Hello, I am Taro", 1);
        Console.WriteLine(classTaro.GetHashCode()); // 1838472550
        classTaro.Msg = "Hi!, I am Taro";
        Console.WriteLine(classTaro.GetHashCode()); // -594399877
    }
}

public class MyClass
{
    public string Msg;
    private int _id;
    
    public MyClass(string msg, int id)
    {
        Msg = msg;
        _id = id;
    }
    
    public override int GetHashCode()
    {
        return Msg.GetHashCode();
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

structの実験

  • ドメインオブジェクトのようなものを自作し、dictionaryのkeyに用いる。
  • 意図した挙動になるかを実験
structをkeyにつかう
public class Program
{
    public static void Main()
    {
        // struct型の挙動調査
        var id1 = new PlayerId("1");
        var id2 = new PlayerId("2");
        var id3 = new PlayerId("1");

        WriteLine(id1 == id2); // false
        WriteLine(id1 == id3); // true

        WriteLine(id1.Equals(id2)); // false
        WriteLine(id1.Equals(id3)); // true

        var dic = new Dictionary<PlayerId, string>();
        dic.Add(id1, "コンテンツ1");
        dic.Add(id2, "コンテンツ2");
        // dic.Add(id3, "コンテンツ3"); // Unhandled exception. System.ArgumentException: An item with the same key has already been added. Key: 1

        WriteLine(dic[id1]); // コンテンツ1
        WriteLine(dic[id2]); // コンテンツ2
        WriteLine(dic[id3]); // コンテンツ1

        // string型の挙動調査
        var strId1 = "1";
        var strId2 = "2";
        var strId3 = "1";

        var dic2 = new Dictionary<string, string>();
        dic2.Add(strId1, "コンテンツ1");
        dic2.Add(strId2, "コンテンツ2");
        // dic2.Add(strId3, "コンテンツ3"); // Unhandled exception. System.ArgumentException: An item with the same key has already been added. Key: 1

        WriteLine(dic2[strId1]); // コンテンツ1
        WriteLine(dic2[strId2]); // コンテンツ2
        WriteLine(dic2[strId3]); // コンテンツ1
    }
}
public readonly struct PlayerId
{
    private readonly string _id;

    public PlayerId(string id) => _id = id  ?? throw new ArgumentNullException(nameof(id));

    public override bool Equals(object obj)
    {
        if (obj is PlayerId other)
        {
            return _id == other._id;
        }
        return false;
    }

    public static bool operator ==(PlayerId a, PlayerId b)
    {
        return a._id == b._id;
    }

    public static bool operator !=(PlayerId a, PlayerId b)
    {
        return !(a == b);
    }

    public override int GetHashCode()
    {
        return _id?.GetHashCode() ?? 0;
    }

    public override string ToString() => _id;
}