質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.83%

2つのコレクションを比較し、特定のプロパティが異なる要素を取り出したい

解決済

回答 7

投稿 編集

  • 評価
  • クリップ 3
  • VIEW 12K+

ytabuchi

JapanXamarinUserGroup主宰

前提・実現したいこと

ローカルに保持したデータとWebから引っ張ってきたデータの値(ここではName)を比較したいと考えています。Idが各Listで異なるため(ここは現時点では仕様なので仕方ないものとして考えてください。)、ListのNameのみを比較して、差集合を得たいです。

以下のコードの例では、localPerson(1,"test1")webPerson(2,"test1")を同じ要素として扱いたい。という意図です。

Nameのみをstring[]に抜き出して差集合を出して再度別のListを作るという非常に汚らしいコードはできたのですが、よりエレガントな方法があれば知りたいです。

よろしくお願いいたします。

 欲しい結果

List<Person>
の以下の要素です。
4,"test4"
5,"test5"

該当のソースコード

class Class1
{
    static void Main(string[] args)
    {
        var local = new List<Person>
        {
            new Person(1,"test1"),
            new Person(2,"test2"),
            new Person(3,"test3"),
            new Person(4,"test4"),
            new Person(5,"test5"),
        };

        var web = new List<Person>
        {
            new Person(2,"test1"),
            new Person(3,"test2"),
            new Person(4,"test3"),
        };

        // エレガントな方法はないのでしょうか?
        string[] arrLocal = local.Select(x => x.Name).ToArray();
        string[] arrWeb = web.Select(x => x.Name).ToArray();
        var diff = arrLocal.Except(arrWeb).ToArray();

        var diffList = new List<Person>();
        foreach (var item in diff)
        {
            diffList.Add(local.FirstOrDefault(x => x.Name == item));
        }
    }
}
public class Person
{
    public int Id { get; private set; }
    public string Name { get; private set; }

    public Person(int id, string name)
    {
        this.Id = id;
        this.Name = name;
    }
}

試したこと

foreachを入れ子にしたり、Hashsetを使ったりを考えたのですが、条件設定がうまくいかず、欲しい結果が得られませんでした。

補足情報(言語/FW/ツール等のバージョンなど)

C# 6.0 です。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 7

+3

var hash = new HashSet<string>(web.Select(n => n.Name));
var diffList = local.Where(n => hash.Contains(n.Name) == false).ToArray();
これでいいのでは?

HashSetは探すことでは、早いものとして知られています。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

checkベストアンサー

+2

こんにちは。

ExceptメソッドをIEqualityComparer<T>付きで使えば良いと思います。
自分の場合は以下のように書きます。


こんな感じのユーティリティクラスを予め用意しておいて、

public class LambdaEqualityComparer<T> : EqualityComparer<T>
{
    private readonly Func<T, T, bool> _equals;

    private readonly Func<T, int> _getHash;

    public LambdaEqualityComparer(Func<T, T, bool> equals) : this(equals, x => x.GetHashCode())
    { }

    public LambdaEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHash)
    {
        this._equals = equals;
        this._getHash = getHash;
    }

    public override bool Equals(T x, T y)
        => this._equals(x, y);

    public override int GetHashCode(T obj)
        => this._getHash(obj);
}

こんな感じで、

var diffList = local.Except(web, new LambdaEqualityComparer<Person>((x, y) => x.Name == y.Name, x => x.Name.GetHashCode()));

どうですかね?


もう一つの方法として、既存のクラスに変更を加えることが可能なら、
PersonクラスにIEquatable<T>を実装し、object.Equals(object)とobject.GetHashCode()をオーバーライドすることで、Exceptの動作を意図したものにすることが可能です。

public class Person : IEquatable<Person>
{
    public int Id { get; private set; }
    public string Name { get; private set; }

    public Person(int id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public bool Equals(Person obj) => this.Name == obj?.Name;

    public override bool Equals(object obj) => this.Equals(obj as Person);

    public override int GetHashCode() => this.Name.GetHashCode();
}
var diffList = local.Except(web);

この場合、Nameが同じであるPersonは本当に「同じモノである」として扱われることになるので、それによって何か不都合が生じないように注意する必要があります。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+2

webとlocalの双方から余分な要素を取得する事まで考慮するならば、和集合と積集合の差集合である排他的論理和をLinqで以下のように求められます。
※webにtest6を追加して実行するとtest4, test5, test6を返します

var union = local.Union(web, person => person.Name); //和集合
var intersect = local.Intersect(web, person => person.Name); //積集合
var diffList = union.Except(intersect, person => person.Name); //和集合と積集合の差集合


ここで第2引数は既出の通りIEqualityComparer<T>なのですが、自前で作るのは面倒なのでneueccさん作成のAnonymousComparerの利用がお薦めです。
AnonymousComparerの拡張メソッドによりラムダ式で簡単に記述できます。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

むしろコード増えちゃいました。

using System;
using System.Collections.Generic;
using System.Linq;

class Class1
{
    static void Main(string[] args)
    {
        var local = new List<Person>
        {
            new Person(1,"test1"),
            new Person(2,"test2"),
            new Person(3,"test3"),
            new Person(4,"test4"), // これと
            new Person(5,"test5"), // これがほしい
        };

        var web = new List<Person>
        {
            new Person(2,"test1"),
            new Person(3,"test2"),
            new Person(4,"test3"),
        };

        var result = local.Except(web, new PersonComparer());
        foreach(var x in result)
            Console.WriteLine($"{x.Id},{x.Name}");


    }
}
public class Person
{
    public int Id { get;  }
    public string Name { get; }

    public Person(int id, string name)
    {
        this.Id = id;
        this.Name = name;
    }
}

public class PersonComparer : IEqualityComparer<Person>{
    public bool Equals(Person p1, Person p2)
    {
        // nullチェック省略
        return (p1.Name == p2.Name);
    }

    public int GetHashCode(Person p)
    {
        return p.Name.GetHashCode();
    }
}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

      var diffList =
        (from l in local
        join w in web on l.Name equals w.Name into x
        where x.Count() == 0
        select l).ToList();


というのが好きで使っています。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

IEqualityComparer を作らないといけないですが、次のようなコードはどうでしょうか?

var set = new HashSet<Person>(local, PersonEquality.Instance);
set.SymmetricExceptWith(web);
// set に差集合が格納されています。

// 手抜きです
class PersonEquality : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y) => x.Name == y.Name;
    public int GetHashCode(Person obj) => obj.Name.GetHashCode();
    public static PersonEquality Instance { get; } = new PersonEquality();
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

前述で書かれているのと差がないですが、
MSDNのIEqualityComparer<T>(日本語は翻訳内容がキツイので英語を...)的という意味で、IEqualityComparerを使った基礎な書き方の紹介です。

public class PersonComparer : IEqualityComparer<Person>
{
    public int GetHashCode(Person p) => p?.Name.GetHashCode() ?? 0;

    public bool Equals(Person p1, Person p2)
    { 
        if (object.ReferenceEquals(p1, p2)) return true;
        if (object.ReferenceEquals(p1, null) || object.ReferenceEquals(p2, null)) return false;

        return p1.Name == p2.Name;
    }
}


で、

var diff = local.Except(web, new PersonComparer()).ToList();


という感じです。
(LINQ自体遅いけど標準的という意味で)速度的にも悪くないはずです。

この動作をおさえた上で、めんどくて燃えそうと感じた場合、Generics対応するなり拡張メソッド作っちゃうとかです♪
そこら辺は、「generic」「IEqualityComparer」でググると英語の記事だと基本的なのがたくさんでます。あとはneue先生の記事が参考になると(個人的に)感じてます♪

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.83%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る