😺

C# LINQのUnionを使ったオブジェクトリストの和集合

2024/11/08に公開

LINQのUnionメソッド

和集合を求める集合演算用のメソッド(重複データは除かれる)

https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.union?view=net-8.0

何をしたいのか

オブジェクトリストの和集合をとりたい
(プロパティが一致しているなら同じオブジェクトとみなしたい)

Program.cs
public class Sample
{
    private string? _name;
    private int _age;

    public string? Name
    {
        get => _name;
        set => _name = value;
    }

    public int Age
    {
        get => _age;
        set => _age = value;
    }
}

public class Program
{
    public static void Main()
    {
        var sampleA = new List<Sample>
        {
            new Sample{ Name = "taro", Age = 10},
            new Sample{ Name = "ziro", Age = 10}
        };

        var sampleB = new List<Sample>
        {
            new Sample{ Name = "taro", Age = 10},
            new Sample{ Name = "taro", Age = 20},
            new Sample{ Name = "saburo", Age = 12}
        };

        var result = sampleA.Union(sampleB);
        foreach (var item in result)
        {
            Console.WriteLine($"Name: {item.Name}, Age: {item.Age}");
        }
    }
}

上記実装だとこうなってしまう

実行結果
Name: taro, Age: 10
Name: ziro, Age: 10
Name: taro, Age: 10
Name: taro, Age: 20
Name: saburo, Age: 12

以下のようにしたい

理想の実行結果
Name: taro, Age: 10
Name: ziro, Age: 10
Name: taro, Age: 20
Name: saburo, Age: 12

実装にあたっての調査

Microsoftのドキュメントより

カスタム データ型のオブジェクトのシーケンスを比較する場合は、ヘルパー クラスにジェネリック インターフェイスを IEquatable<T> 実装する必要があります。

上記から比較対象クラスにIEquatable<T>インタフェースを実装するようにしてみる
実装しなかった場合に使用される既定の等値比較子のEquals、GetHashCodeメソッドは参照の等価性(同じインスタンスかどうか)を比較するので理想の実行結果にならないみたいです
つまりEquals、GetHashCodeメソッドをオーバーライドして、プロパティ値の等価性を比較するように実装すればよいということになりそうですね

実装してみる

Program.cs
public class Sample : IEquatable<Sample>
{
    private string? _name;
    private int _age;

    public string? Name
    {
        get => _name;
        set => _name = value;
    }

    public int Age
    {
        get => _age;
        set => _age = value;
    }

    public bool Equals(Sample? other)
    {
        if (other is null)
            return false;

        return this.Name == other.Name && this.Age == other.Age;
    }

    // Sampleクラス以外のオブジェクトを安全に比較
    public override bool Equals(object? obj) => Equals(obj as Sample);
    public override int GetHashCode() => (Name, Age).GetHashCode();
}

public class Program
{
    public static void Main()
    {
        var sampleA = new List<Sample>
        {
            new Sample{ Name = "taro", Age = 10},
            new Sample{ Name = "ziro", Age = 10}
        };

        var sampleB = new List<Sample>
        {
            new Sample{ Name = "taro", Age = 10},
            new Sample{ Name = "taro", Age = 20},
            new Sample{ Name = "saburo", Age = 12}
        };

        var result = sampleA.Union(sampleB);
        foreach (var item in result)
        {
            Console.WriteLine($"Name: {item.Name}, Age: {item.Age}");
        }
    }
}

GetHashCode:Name, Ageの値をもとにハッシュ値を生成
Equals:プロパティ同士の比較

比較対象のオブジェクトのGetHashCodeでName, Ageが同じ値なら同じハッシュ値を生成する
さらにハッシュの衝突を考慮し、Equalsで厳密な比較を行う

実行結果
Name: taro, Age: 10
Name: ziro, Age: 10
Name: taro, Age: 20
Name: saburo, Age: 12

Discussion