web-dev-qa-db-ja.com

特定のプロパティに対するLINQのDistinct()

LINQでそれを学ぶために遊んでいますが、単純なリストがない場合にDistinctを使用する方法を理解することはできません(単純な整数のリストを作成するのは簡単です。これは問題ではありません)。 one または more オブジェクトのプロパティ上のオブジェクトのリストに Distinct を使用したい場合はどうすればよいですか。

例:オブジェクトがPersonで、プロパティがIdの場合。どのようにしてすべてのPersonを取得し、オブジェクトのプロパティDistinctを使用してそれらにIdを使用することができますか?

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

Person1とPerson3だけを取得するにはどうすればいいですか?それは可能ですか?

それがLINQで不可能であるならば、.NET 3.5のその特性のいくつかに従ってPersonのリストを持つための最良の方法は何でしょうか?

921

_ edit _ :これは MoreLINQ の一部になりました。

あなたが必要としているのは、効果的に "distinct-by"です。それは現時点ではLINQの一部ではないと思いますが、書くのはかなり簡単です。

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
    (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> seenKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (seenKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}

そのため、Idプロパティだけを使用して個別の値を見つけるには、次のようにします。

var query = people.DistinctBy(p => p.Id);

また、複数のプロパティを使用するために、等価性を適切に実装する匿名型を使用できます。

var query = people.DistinctBy(p => new { p.Id, p.Name });

テストされていませんが、動作するはずです(そして少なくともコンパイルは完了しました)。

ただし、キーのデフォルトの比較子を想定しています。等価比較子を渡す場合は、HashSetコンストラクタに渡してください。

1074
Jon Skeet

one または more プロパティに基づいて個別のリストを取得する場合はどうすればよいですか。

シンプル!あなたはそれらをグループ化し、そのグループから勝者を選びたいのです。

List<Person> distinctPeople = allPeople
  .GroupBy(p => p.PersonId)
  .Select(g => g.First())
  .ToList();

複数のプロパティにグループを定義したい場合は、次のようにします。

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();
1631
Amy B

LINQのように見せたい場合は、クエリ構文を使用することもできます。

var uniquePeople = from p in people
                   group p by new {p.ID} //or group by new {p.ID, p.Name, p.Whatever}
                   into mygroup
                   select mygroup.FirstOrDefault();
71
Chuck Rostance

つかいます:

List<Person> pList = new List<Person>();
/* Fill list */

var result = pList.Where(p => p.Name != null).GroupBy(p => p.Id).Select(grp => grp.FirstOrDefault());

whereはエントリをフィルタリングするのに役立ち(より複雑かもしれません)、groupbyselectは異なる機能を実行します。

58
karcsi

私はそれで十分だと思います:

list.Select(s => s.MyField).Distinct();
55
Ivan

最初のフィールドでソリューションをグループ化してから、firstまたはdefault itemを選択します。

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
32
cahit beyaz

標準の Linq.ToLookup() でこれを行うことができます。これにより、一意のキーごとに値のコレクションが作成されます。コレクションの最初の項目を選択するだけです

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
24
David Fahlander

次のコードは、 Jon Skeetの答え と機能的に同等です。

.NET 4.5でテスト済みで、LINQの以前のバージョンでも動作するはずです。

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  HashSet<TKey> seenKeys = new HashSet<TKey>();
  return source.Where(element => seenKeys.Add(keySelector(element)));
}

偶然にも、 Jon Skeetの最新バージョンのDistinctBy.csをGoogle Codeでチェックしてください

16
Contango

私はあなたが以下のようにすることができるようにDistinct関数を拡張する方法を説明する記事を書きました:

var people = new List<Person>();

people.Add(new Person(1, "a", "b"));
people.Add(new Person(2, "c", "d"));
people.Add(new Person(1, "a", "b"));

foreach (var person in people.Distinct(p => p.ID))
    // Do stuff with unique list here.

ここに記事があります:LINQを拡張する - 特殊な関数でプロパティを指定する

11
Timothy Khouri

あなたはそれをすることができます(すぐに落雷ではないにしても):

people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));

つまり、「同じIDを持つ別の人がリストに含まれていないすべての人を選択する」ということです。

あなたの例では、それはちょうど人3を選択することになることを念頭に置いてください。

5
mqp

複数のプロパティに対してDistinctメソッドが必要な場合は、my PowerfulExtensions libraryを調べてください。現在はまだ非常に若い段階にありますが、既にDistinct、Union、Intersectなどのメソッドをいくつでも使用できます。

これはあなたがそれをどのように使うかです:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
5
Andrzej Gis

私は個人的には次のクラスを使います。

public class LambdaEqualityComparer<TSource, TDest> : 
    IEqualityComparer<TSource>
{
    private Func<TSource, TDest> _selector;

    public LambdaEqualityComparer(Func<TSource, TDest> selector)
    {
        _selector = selector;
    }

    public bool Equals(TSource obj, TSource other)
    {
        return _selector(obj).Equals(_selector(other));
    }

    public int GetHashCode(TSource obj)
    {
        return _selector(obj).GetHashCode();
    }
}

それから、拡張メソッド:

public static IEnumerable<TSource> Distinct<TSource, TCompare>(
    this IEnumerable<TSource> source, Func<TSource, TCompare> selector)
{
    return source.Distinct(new LambdaEqualityComparer<TSource, TCompare>(selector));
}

最後に、使用目的

var dates = new List<DateTime>() { /* ... */ }
var distinctYears = dates.Distinct(date => date.Year);

私がこのアプローチを使用して見つけた利点は、LambdaEqualityComparerを受け入れる他のメソッドのためのIEqualityComparerクラスの再使用です。 (ああ、私はyieldのものを元のLINQ実装に任せています...)

4
Joel

私たちのプロジェクトでそのようなタスクに直面したとき、コンパレーターを構成するための小さなAPIを定義しました。

そのため、ユースケースは次のようになりました。

var wordComparer = KeyEqualityComparer.Null<Word>().
    ThenBy(item => item.Text).
    ThenBy(item => item.LangID);
...
source.Select(...).Distinct(wordComparer);

そしてAPI自体は次のようになります。

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

public static class KeyEqualityComparer
{
    public static IEqualityComparer<T> Null<T>()
    {
        return null;
    }

    public static IEqualityComparer<T> EqualityComparerBy<T, K>(
        this IEnumerable<T> source,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc);
    }

    public static KeyEqualityComparer<T, K> ThenBy<T, K>(
        this IEqualityComparer<T> equalityComparer,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc, equalityComparer);
    }
}

public struct KeyEqualityComparer<T, K>: IEqualityComparer<T>
{
    public KeyEqualityComparer(
        Func<T, K> keyFunc,
        IEqualityComparer<T> equalityComparer = null)
    {
        KeyFunc = keyFunc;
        EqualityComparer = equalityComparer;
    }

    public bool Equals(T x, T y)
    {
        return ((EqualityComparer == null) || EqualityComparer.Equals(x, y)) &&
                EqualityComparer<K>.Default.Equals(KeyFunc(x), KeyFunc(y));
    }

    public int GetHashCode(T obj)
    {
        var hash = EqualityComparer<K>.Default.GetHashCode(KeyFunc(obj));

        if (EqualityComparer != null)
        {
            var hash2 = EqualityComparer.GetHashCode(obj);

            hash ^= (hash2 << 5) + hash2;
        }

        return hash;
    }

    public readonly Func<T, K> KeyFunc;
    public readonly IEqualityComparer<T> EqualityComparer;
}

詳細は私たちのサイトにあります:LINQのIEqualityComparer

DistinctBy機能を取得するためだけにMoreLinqライブラリをプロジェクトに追加したくない場合は、Distinct引数を取るLinqのIEqualityComparerメソッドのオーバーロードを使用して同じ最終結果を取得できます。

まず、ラムダ構文を使用してジェネリッククラスの2つのインスタンスのカスタム比較を実行するジェネリックカスタム等価性比較クラスを作成します。

public class CustomEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> _comparison;
    Func<T, int> _hashCodeFactory;

    public CustomEqualityComparer(Func<T, T, bool> comparison, Func<T, int> hashCodeFactory)
    {
        _comparison = comparison;
        _hashCodeFactory = hashCodeFactory;
    }

    public bool Equals(T x, T y)
    {
        return _comparison(x, y);
    }

    public int GetHashCode(T obj)
    {
        return _hashCodeFactory(obj);
    }
}

それからあなたのメインコードでは、こんな感じでそれを使います:

Func<Person, Person, bool> areEqual = (p1, p2) => int.Equals(p1.Id, p2.Id);

Func<Person, int> getHashCode = (p) => p.Id.GetHashCode();

var query = people.Distinct(new CustomEqualityComparer<Person>(areEqual, getHashCode));

ほら! :)

上記の前提は次のとおりです。

  • プロパティPerson.Idint型です
  • peopleコレクションにnull要素が含まれていません

コレクションにnullが含まれる可能性がある場合は、単にnullをチェックするようにラムダを書き換えます。

Func<Person, Person, bool> areEqual = (p1, p2) => 
{
    return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};

_編集_

このアプローチは、Vladimir Nesterovskyの答えにあるものと似ていますが、より単純です。

これもJoelの答えにあるものと似ていますが、複数のプロパティを含む複雑な比較ロジックを可能にします。

しかし、あなたのオブジェクトがIdだけ異なることができるのであれば、あなたのPersonクラスのGetHashCode()Equals()のデフォルト実装をオーバーライドして、そのまま使用できるDistinct()を使用することが他のユーザーから正しい答えを得ました。重複を除外するLinqの方法。

3
Caspian Canuck
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
2
Arindam

オーバーライド 等しい(object obj) および GetHashCode() methods:

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

    public override bool Equals(object obj)
    {
        return ((Person)obj).Id == Id;
        // or: 
        // var o = (Person)obj;
        // return o.Id == Id && o.Name == Name;
    }
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

それからただ電話してください:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();

他の.NETバージョンと互換性があるようにするには、EqualsとGetHashをオーバーライドしてこれを処理するのが最善の方法です(Stack Overflow questionを参照)。このコードでは異なる値を返します。匿名型とは対照的な型付きコレクション)ですが、コード全体を通して一般的なものが必要な場合は、この記事の解決策が最適です。

2
gcoleman0828

DistinctBy()を使用すると、オブジェクトプロパティによって個別のレコードを取得できます。使用する前に次の文を追加するだけです。

microsoft.Ajax.Utilitiesを使用する。

そしてそれを次のように使います。

var listToReturn = responseList.DistinctBy(x => x.Index).ToList();

ここで 'インデックス'は私がデータを区別したいプロパティです。

0
Harry .Naeem

実際にEquals on Person.idを実行するには、PersonのEqualsをオーバーライドする必要があります。これはあなたが期待している振る舞いをもたらすはずです。

0
GWLlosa
    List<string> colors = new List<string> { "blue", "red", "black", "blue", "yellow", "blue" };
    IEnumerable<string> distinctColors = colors.Distinct();
0
uguronline