ラムダとDistinct()?


746

そうです、私は列挙可能であり、そこから明確な値を取得したいと考えています。

使用してSystem.Linq、と呼ばれる拡張メソッドは、もちろんありますDistinct。単純なケースでは、次のようにパラメーターなしで使用できます。

var distinctValues = myStringList.Distinct();

ええと、いいのですが、等価性を指定する必要があるオブジェクトの列挙可能オブジェクトがある場合、使用できる唯一のオーバーロードは次のとおりです。

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

等価比較引数はのインスタンスでなければなりませんIEqualityComparer<T>。もちろん、私はこれを行うことができますが、それはいくぶん冗長で、まあまあです。

私が期待していたのは、ラムダをとるオーバーロード、たとえばFunc <T、T、bool>です。

var distinctValues
    = myCustomerList.Distinct((c1, c2) => c1.CustomerId == c2.CustomerId);

誰かがそのような拡張機能が存在するかどうか、または同等の回避策を知っていますか?それとも何か不足していますか?

または、インラインでIEqualityComparerを指定する方法はありますか(私を当惑させます)?

更新

この件に関するMSDNフォーラムの投稿に対するAnders Hejlsbergからの返信を見つけました。彼は言う:

あなたが遭遇する問題は、2つのオブジェクトが等しい場合、それらは同じGetHashCode戻り値を持つ必要があることです(そうしないと、Distinctによって内部的に使用されるハッシュテーブルが正しく機能しません)。EqualsとGetHashCodeの互換性のある実装を単一のインターフェイスにパッケージ化するため、IEqualityComparerを使用します。

それは理にかなっていると思います


2
GroupByを使用したソリューションについては、stackoverflow.com

17
Anders Hejlsbergのアップデートをありがとう!
Tor Haugen、2009

いいえ、意味がありません-同じ値を含む2つのオブジェクトが2つの異なるハッシュコードをどのように返すことができますか?
GY 2015年

-それは助けることができるソリューションのために.Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId))、とGetHashCodeメソッド()が正常に動作することが重要である理由を説明します。
marbel82 2016年

回答:


1029
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());

12
優秀な!これも、拡張メソッドにカプセル化するのは本当に簡単ですDistinctByDistinct署名が一意になるため)。
Tomas Aschan

1
私にはうまくいきません!<メソッド 'First'は、最後のクエリ操作としてのみ使用できます。代わりに、このインスタンスでメソッド「FirstOrDefault」の使用を検討してください。>「FirstOrDefault」を試しても機能しませんでした。
JatSing、2011

63
@TorHaugen:これらすべてのグループの作成にはコストがかかることに注意してください。これは入力をストリーミングできず、結果を返す前にすべてのデータをバッファリングすることになります。それはもちろんあなたの状況には関係ないかもしれませんが、私はDistinctByの優雅さを好みます:)
Jon Skeet

2
@JonSkeet:これは、1つの機能だけに追加のライブラリをインポートしたくないVB.NETプログラマにとって十分なものです。ASync CTPがない場合、VB.NETはyieldステートメントをサポートしないため、ストリーミングは技術的に不可能です。回答ありがとうございます。C#でコーディングするときに使用します。;-)
Alex Essilfie

2
@BenGripka:それはまったく同じではありません。顧客IDのみを提供します。私は顧客全体が欲しいです:)
ライアンマン'16年

496

あなたが望むようにそれは私には見えますDistinctByからMoreLINQ。その後、次のように書くことができます:

var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId);

これは、DistinctBy(nullityチェックや独自のキー比較子を指定するオプションがない)の簡略版です。

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

14
Jon Skeetが投稿のタイトルを読むだけでベストアンサーが投稿されることを知っていました。それがLINQと何か関係がある場合、スキートはあなたの男です。「C#In Depth」を読んで、神のようなlinqの知識を習得してください。
nocarrier 2014年

2
素晴らしい答え!!! また、yield+追加のlib に関するすべてのVB_Complainersの場合、foreachは次のようにreturn source.Where(element => knownKeys.Add(keySelector(element)));
書き直す

5
@ sudhAnsu63これはLinqToSql(および他のlinqプロバイダー)の制限です。LinqToXの目的は、C#ラムダ式をXのネイティブコンテキストに変換することです。つまり、LinqToSqlはC#をSQLに変換し、そのコマンドを可能な限りネイティブに実行します。つまり、C#に存在するメソッドは、SQL(または使用しているlinqプロバイダー)で表現する方法がない場合、linqProviderを「パススルー」できません。これは、データオブジェクトをビューモデルに変換する拡張メソッドに見られます。この問題を回避するには、DistinctBy()の前にToList()を呼び出してクエリを「具体化」します。
マイケルブラックバーン

1
そして、この質問に戻るたびに、なぜ彼らは少なくともいくつかのMoreLinqをBCLに採用しないのか疑問に思っています。
Shimmy Weitzhandler 2017年

2
@シミー:私は確かにそれを歓迎します...私は実現可能性が何であるかわかりません。私は.NET Foundationでそれを上げることができます...
Jon Skeet

39

物事を包むために。私のようにここに来たほとんどの人は、ライブラリを使用せずに、可能限り最高のパフォーマンスで可能な最も簡単なソリューションを望んでいると思います

(私がメソッドで受け入れたグループは、パフォーマンスの面で過剰だと思います。)

以下は、IEqualityComparerインターフェイスを使用した簡単な拡張メソッドで、null値に対しても機能します。

使用法:

var filtered = taskList.DistinctBy(t => t.TaskExternalId).ToArray();

拡張メソッドコード

public static class LinqExtensions
{
    public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
    {
        GeneralPropertyComparer<T, TKey> comparer = new GeneralPropertyComparer<T,TKey>(property);
        return items.Distinct(comparer);
    }   
}
public class GeneralPropertyComparer<T,TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> expr { get; set; }
    public GeneralPropertyComparer (Func<T, TKey> expr)
    {
        this.expr = expr;
    }
    public bool Equals(T left, T right)
    {
        var leftProp = expr.Invoke(left);
        var rightProp = expr.Invoke(right);
        if (leftProp == null && rightProp == null)
            return true;
        else if (leftProp == null ^ rightProp == null)
            return false;
        else
            return leftProp.Equals(rightProp);
    }
    public int GetHashCode(T obj)
    {
        var prop = expr.Invoke(obj);
        return (prop==null)? 0:prop.GetHashCode();
    }
}

19

このため、そのような拡張メソッドのオーバーロードはありません。私はこれまでに自分をイライラさせてきたので、この問題に対処するためのヘルパークラスを作成します。目標はa Func<T,T,bool>をに変換することIEqualityComparer<T,T>です。

public class EqualityFactory {
  private sealed class Impl<T> : IEqualityComparer<T,T> {
    private Func<T,T,bool> m_del;
    private IEqualityComparer<T> m_comp;
    public Impl(Func<T,T,bool> del) { 
      m_del = del;
      m_comp = EqualityComparer<T>.Default;
    }
    public bool Equals(T left, T right) {
      return m_del(left, right);
    } 
    public int GetHashCode(T value) {
      return m_comp.GetHashCode(value);
    }
  }
  public static IEqualityComparer<T,T> Create<T>(Func<T,T,bool> del) {
    return new Impl<T>(del);
  }
}

これにより、次のように書くことができます

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));

8
それは厄介なハッシュコードの実装があります。作成するために、簡単にそれをですIEqualityComparer<T>:投影からstackoverflow.com/questions/188120/...
ジョンスキート

7
(ハッシュコードに関する私のコメントを説明するだけです-このコードを使用すると、Equals(x、y)== trueになるのは非常に簡単ですが、GetHashCode(x)!= GetHashCode(y)です。 。)
ジョンスキート、

ハッシュコードの異議に同意します。それでも、パターンの+1。
Tor Haugen、

@ジョン、ええ、GetHashcodeの元の実装は最適ではない(怠惰だった)ことに同意します。少し標準的なEqualityComparer <T> .Default.GetHashcode()を使用するように切り替えました。正直なところ、このシナリオでGetHashcode実装が機能することが保証されているのは、単に定数値を返すことだけです。ハッシュテーブル検索を強制終了しますが、機能的に正しいことが保証されています。
JaredPar 2009

1
@JaredPar:その通りです。ハッシュコードは、使用している等値関数と一致している必要があります。これはおそらくデフォルトのものではないので、それ以外の場合は気にしないでしょう:)これが、プロジェクションの使用を好む理由です。そのようにコーディングしてください。また、呼び出しコードの重複が少なくなります。確かにそれは、同じ投影を2回必要とする場合にのみ機能しますが、それは私が実際に見たすべてのケースです:)
Jon Skeet

18

簡単な解決策

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());

1
これが改善された理由について説明を追加していただけますか?
キースピンソン

これは、Konradがうまくいかなかったのに、実際にはうまく機能しました。
neoscribe

13

これはあなたが望むことをしますが、私はパフォーマンスについて知りません:

var distinctValues =
    from cust in myCustomerList
    group cust by cust.CustomerId
    into gcust
    select gcust.First();

少なくともそれは冗長ではありません。


12

ここに私が必要とすることを行う単純な拡張メソッドがあります...

public static class EnumerableExtensions
{
    public static IEnumerable<TKey> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.GroupBy(selector).Select(x => x.Key);
    }
}

彼らがこのような明確な方法をフレームワークに焼き付けなかったのは残念ですが、ほらほら。


これは、そのライブラリmorelinqを追加する必要がない最良のソリューションです。
toddmo 2014

しかし、戻り値をx.Keyx.First()IEnumerable<T>
toddmo

@toddmoフィードバックをありがとう:-)ええ、論理的に聞こえます...さらに調査した後で答えを更新します。
デビッドカークランド2014

1
解決策への感謝、シンプルでクリーンなことを言うのに遅すぎることは決してありません
Ali

4

私が使用したもので、うまくいった。

/// <summary>
/// A class to wrap the IEqualityComparer interface into matching functions for simple implementation
/// </summary>
/// <typeparam name="T">The type of object to be compared</typeparam>
public class MyIEqualityComparer<T> : IEqualityComparer<T>
{
    /// <summary>
    /// Create a new comparer based on the given Equals and GetHashCode methods
    /// </summary>
    /// <param name="equals">The method to compute equals of two T instances</param>
    /// <param name="getHashCode">The method to compute a hashcode for a T instance</param>
    public MyIEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        if (equals == null)
            throw new ArgumentNullException("equals", "Equals parameter is required for all MyIEqualityComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = getHashCode;
    }
    /// <summary>
    /// Gets the method used to compute equals
    /// </summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }
    /// <summary>
    /// Gets the method used to compute a hash code
    /// </summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null)
            return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

@Mukusここでクラス名を尋ねられる理由がわかりません。IEqualityComparerを実装するためにクラスに名前を付ける必要があったので、Myのプレフィックスを付けました。
Kleinux、2015年

4

ここで私が見たすべてのソリューションは、すでに比較可能なフィールドを選択することに依存しています。ただし、別の方法で比較する必要がある場合、このソリューションは次のような場合に一般的に機能するようです。

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()

LambdaComparerとは何ですか、どこからインポートしますか?
Patrick Graham、

@PatrickGrahamは答えにリンク:brendan.enrick.com/post/...
ドミトリーLedentsov

3

別の方法をとります:

var distinctValues = myCustomerList.
Select(x => x._myCaustomerProperty).Distinct();

シーケンスは個別の要素を返し、プロパティ '_myCaustomerProperty'でそれらを比較します。


1
これを言うためにここに来ました。これは受け入れられる答えでなければなりません
Still.Tony

5
いいえ、必要なのがカスタムプロパティの個別の値である場合を除いて、これは受け入れられる答えではありません。一般的なOPの質問は、オブジェクトの特定のプロパティに基づいて個別のオブジェクトを返す方法でした。
tomo

2

InlineComparerを使用できます

public class InlineComparer<T> : IEqualityComparer<T>
{
    //private readonly Func<T, T, bool> equalsMethod;
    //private readonly Func<T, int> getHashCodeMethod;
    public Func<T, T, bool> EqualsMethod { get; private set; }
    public Func<T, int> GetHashCodeMethod { get; private set; }

    public InlineComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        if (equals == null) throw new ArgumentNullException("equals", "Equals parameter is required for all InlineComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = hashCode;
    }

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

    public int GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null) return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

使用例

  var comparer = new InlineComparer<DetalleLog>((i1, i2) => i1.PeticionEV == i2.PeticionEV && i1.Etiqueta == i2.Etiqueta, i => i.PeticionEV.GetHashCode() + i.Etiqueta.GetHashCode());
  var peticionesEV = listaLogs.Distinct(comparer).ToList();
  Assert.IsNotNull(peticionesEV);
  Assert.AreNotEqual(0, peticionesEV.Count);

ソース:https :
//stackoverflow.com/a/5969691/206730 UnionにIEqualityComparerを使用
する明示的な型コンパレーターをインラインで指定できますか?


2

LambdaEqualityComparerを使用できます。

var distinctValues
    = myCustomerList.Distinct(new LambdaEqualityComparer<OurType>((c1, c2) => c1.CustomerId == c2.CustomerId));


public class LambdaEqualityComparer<T> : IEqualityComparer<T>
    {
        public LambdaEqualityComparer(Func<T, T, bool> equalsFunction)
        {
            _equalsFunction = equalsFunction;
        }

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

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

        private readonly Func<T, T, bool> _equalsFunction;
    }

1

これを行うためのトリッキーな方法はAggregate()キープロパティ値をキーとして辞書をアキュムレータとして使用し、拡張を使用することです。

var customers = new List<Customer>();

var distincts = customers.Aggregate(new Dictionary<int, Customer>(), 
                                    (d, e) => { d[e.CustomerId] = e; return d; },
                                    d => d.Values);

そしてGroupByスタイルのソリューションは次を使用していToLookup()ます:

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());

いいですが、Dictionary<int, Customer>代わりにを作成しないのはなぜですか?
ルフィン

0

IEnumerableがあり、デリゲートの例では、c1とc2がこのリストの2つの要素を参照するようにしたいと思いますか?

私はこれを自己結合var differentResults = from myList in c1 join myList in myList on


0

Distinct()固有の結果が生成されない場合は、次のことを試してください。

var filteredWC = tblWorkCenter.GroupBy(cc => cc.WCID_I).Select(grp => grp.First()).Select(cc => new Model.WorkCenter { WCID = cc.WCID_I }).OrderBy(cc => cc.WCID); 

ObservableCollection<Model.WorkCenter> WorkCenter = new ObservableCollection<Model.WorkCenter>(filteredWC);


0

方法は次のとおりです。

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query,
                                                    Func<T, V> f, 
                                                    Func<IGrouping<V,T>,T> h=null)
    {
        if (h==null) h=(x => x.First());
        return query.GroupBy(f).Select(h);
    }
}

このメソッドを使用すると、のような1つのパラメーター.MyDistinct(d => d.Name)を指定して使用できますが、次のように、2番目のパラメーターとして条件を指定することもできます。

var myQuery = (from x in _myObject select x).MyDistinct(d => d.Name,
        x => x.FirstOrDefault(y=>y.Name.Contains("1") || y.Name.Contains("2"))
        );

注:これにより、たとえば他の関数も指定できます.LastOrDefault(...)


条件だけを公開したい場合は、次のように実装することでさらに簡単にすることができます。

public static IEnumerable<T> MyDistinct2<T, V>(this IEnumerable<T> query,
                                                Func<T, V> f,
                                                Func<T,bool> h=null
                                                )
{
    if (h == null) h = (y => true);
    return query.GroupBy(f).Select(x=>x.FirstOrDefault(h));
}

この場合、クエリは次のようになります。

var myQuery2 = (from x in _myObject select x).MyDistinct2(d => d.Name,
                    y => y.Name.Contains("1") || y.Name.Contains("2")
                    );

NBここでは、式はより単純ですが、注記.MyDistinct2.FirstOrDefault(...)暗黙的に使用しています。


注:上記の例では、次のデモクラスを使用しています。

class MyObject
{
    public string Name;
    public string Code;
}

private MyObject[] _myObject = {
    new MyObject() { Name = "Test1", Code = "T"},
    new MyObject() { Name = "Test2", Code = "Q"},
    new MyObject() { Name = "Test2", Code = "T"},
    new MyObject() { Name = "Test5", Code = "Q"}
};

0

IEnumerable ラムダ拡張:

public static class ListExtensions
{        
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, int> hashCode)
    {
        Dictionary<int, T> hashCodeDic = new Dictionary<int, T>();

        list.ToList().ForEach(t => 
            {   
                var key = hashCode(t);
                if (!hashCodeDic.ContainsKey(key))
                    hashCodeDic.Add(key, t);
            });

        return hashCodeDic.Select(kvp => kvp.Value);
    }
}

使用法:

class Employee
{
    public string Name { get; set; }
    public int EmployeeID { get; set; }
}

//Add 5 employees to List
List<Employee> lst = new List<Employee>();

Employee e = new Employee { Name = "Shantanu", EmployeeID = 123456 };
lst.Add(e);
lst.Add(e);

Employee e1 = new Employee { Name = "Adam Warren", EmployeeID = 823456 };
lst.Add(e1);
//Add a space in the Name
Employee e2 = new Employee { Name = "Adam  Warren", EmployeeID = 823456 };
lst.Add(e2);
//Name is different case
Employee e3 = new Employee { Name = "adam warren", EmployeeID = 823456 };
lst.Add(e3);            

//Distinct (without IEqalityComparer<T>) - Returns 4 employees
var lstDistinct1 = lst.Distinct();

//Lambda Extension - Return 2 employees
var lstDistinct = lst.Distinct(employee => employee.EmployeeID.GetHashCode() ^ employee.Name.ToUpper().Replace(" ", "").GetHashCode()); 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.