LINQ to Objectsとは機能しない


120
class Program
{
    static void Main(string[] args)
    {
        List<Book> books = new List<Book> 
        {
            new Book
            {
                Name="C# in Depth",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },
                     new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },                       
                }
            },
            new Book
            {
                Name="LINQ in Action",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Fabrice", LastName="Marguerie"
                    },
                     new Author 
                    {
                        FirstName = "Steve", LastName="Eichert"
                    },
                     new Author 
                    {
                        FirstName = "Jim", LastName="Wooley"
                    },
                }
            },
        };


        var temp = books.SelectMany(book => book.Authors).Distinct();
        foreach (var author in temp)
        {
            Console.WriteLine(author.FirstName + " " + author.LastName);
        }

        Console.Read();
    }

}
public class Book
{
    public string Name { get; set; }
    public List<Author> Authors { get; set; }
}
public class Author
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public override bool Equals(object obj)
    {
        return true;
        //if (obj.GetType() != typeof(Author)) return false;
        //else return ((Author)obj).FirstName == this.FirstName && ((Author)obj).FirstName == this.LastName;
    }

}

これは、「LINQ in Action」の例に基づいています。リスト4.16

これはJon Skeetを2回印刷します。どうして?AuthorクラスのEqualsメソッドをオーバーライドしてみました。それでもDistinctは機能していないようです。何が欠けていますか?

編集:==および!=演算子のオーバーロードも追加しました。まだ助けはありません。

 public static bool operator ==(Author a, Author b)
    {
        return true;
    }
    public static bool operator !=(Author a, Author b)
    {
        return false;
    }

回答:


159

LINQ Distinctは、カスタムオブジェクトに関してはそれほどスマートではありません。

リストを見て、2つの異なるオブジェクトがあることを確認するだけです(メンバーフィールドの値が同じであってもかまいません)。

回避策の1つは、次に示すようにIEquatableインターフェイスを実装することです

Authorクラスを次のように変更すると、機能するはずです。

public class Author : IEquatable<Author>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public bool Equals(Author other)
    {
        if (FirstName == other.FirstName && LastName == other.LastName)
            return true;

        return false;
    }

    public override int GetHashCode()
    {
        int hashFirstName = FirstName == null ? 0 : FirstName.GetHashCode();
        int hashLastName = LastName == null ? 0 : LastName.GetHashCode();

        return hashFirstName ^ hashLastName;
    }
}

DotNetFiddleとしてお試しください


22
IEquatableは問題ありませんが、不完全です。常に Object.Equals()とObject.GetHashCode()を一緒に実装する必要があります。IEquatable <T> .EqualsはObject.Equalsをオーバーライドしないため、強く型付けされていない比較を行うと失敗します。これは、フレームワークで頻繁に発生し、常に非ジェネリックコレクションで発生します。
AndyM 2009

それで、Rex Mが示唆したように、IEqualityComparer <T>を取るDistinctのオーバーライドを使用する方が良いでしょうか?落とし穴に落ちたくない場合はどうすればいいのか。
タンモイ2009

3
@Tanmoyそれは依存します。Authorを通常のオブジェクトのように動作させたい(つまり、参照の等価性のみ)が、Distinctの目的で名前の値を確認する場合は、IEqualityComparerを使用します。あなたがいる場合、常に著者のオブジェクトは、名前の値に基づいて比較することにしたい、そして、GetHashCodeメソッドをオーバーライドして、等しい、またはIEquatableを実装します。
レックスM

3
Linqのこれらのメソッドでは、実装IEquatable(およびオーバーライドEquals/ GetHashCode)しましたが、ブレークポイントが1つも起動していDistinctません。
PeterX

2
@PeterX私もこれに気づきました。私は中にブレークポイントを持っていたGetHashCodeし、Equals彼らは、foreachループの間にヒットしました、。これは、がをvar temp = books.SelectMany(book => book.Authors).Distinct();返すためIEnumerableです。つまり、リクエストはすぐには実行されず、データが使用されたときにのみ実行されます。この発砲の例をすぐに確認したい場合は、の後に追加.ToList()する.Distinct()と、EqualsとのGetHashCode前のブレークポイントが表示されます。
JabberwockyDecompiler 2015年

70

このDistinct()メソッドは、参照型の参照の等価性をチェックします。つまり、同じ値を含む別のオブジェクトではなく、複製された文字通り同じオブジェクトを探しています。

あり、過負荷取りされたIEqualityComparerは、あなたが指定されたオブジェクトが別のに等しいかどうかを決定するための別のロジックを指定することができます。

Authorを通常のオブジェクトのように動作させたい(つまり、参照の等価性のみ)が、名前の値による等価性を区別するためには、IEqualityComparerを使用します。常にAuthorオブジェクトを名前の値に基づいて比較する場合は、GetHashCodeおよびEqualsをオーバーライドするか、IEquatableを実装します

IEqualityComparerインターフェースの2つのメンバーはEqualsおよびGetHashCodeです。2つのAuthorオブジェクトが等しいかどうかを判断するためのロジックは、姓と名の文字列が同じ場合に表示されます。

public class AuthorEquals : IEqualityComparer<Author>
{
    public bool Equals(Author left, Author right)
    {
        if((object)left == null && (object)right == null)
        {
            return true;
        }
        if((object)left == null || (object)right == null)
        {
            return false;
        }
        return left.FirstName == right.FirstName && left.LastName == right.LastName;
    }

    public int GetHashCode(Author author)
    {
        return (author.FirstName + author.LastName).GetHashCode();
    }
}

1
ありがとうございました!あなたのGetHashCode()実装は、私がまだ足りないものを示しました。私は{比較に使用されているプロパティ} .GetHashCode()ではなく、{渡されたオブジェクト} .GetHashCode()を返していました。それが違いを生み、鉱山がまだ失敗している理由を説明しています。2つの異なる参照には2つの異なるハッシュコードがあります。
pelazem 2015

44

別の実施ない溶液IEquatableEquals及びGetHashCodeLINQs使用するGroupBy方法およびIGroupingから最初の項目を選択します。

var temp = books.SelectMany(book => book.Authors)
                .GroupBy (y => y.FirstName + y.LastName )
                .Select (y => y.First ());

foreach (var author in temp){
  Console.WriteLine(author.FirstName + " " + author.LastName);
}

1
上記の方法を考慮すると、パフォーマンスを検討するだけで、これは同じ速度で実行できますか?
Biswajeet 2015年

実装方法を複雑にするよりもはるかに優れており、EFを使用すると、SQLサーバーに作業が委任されます。
Zapnologica 2015

この方法は機能するかもしれませんが、グループ化されているものの数が原因でパフォーマンスの問題が発生します
Bellash

@Bellash機能させてから高速化します。もちろん、このグループ化により、さらに多くの作業が必要になる可能性があります。しかし、必要以上に実装するのが面倒な場合もあります。
Jehof 2016年

2
私はGROUPBYに「新しい」オブジェクトを使用して、このソリューションを好むが、: .GroupBy(y => new { y.FirstName, y.LastName })
デイブ・デ・ヨング

32

ユーザー定義のデータ型のリストから個別の値を取得する方法がもう1つあります。

YourList.GroupBy(i => i.Id).Select(i => i.FirstOrDefault()).ToList();

確かに、それは明確なデータセットを提供します


21

Distinct()列挙可能なオブジェクトのデフォルトの等価比較を実行します。Equals()andをオーバーライドしていない場合GetHashCode()は、のデフォルトの実装が使用されobject、参照が比較されます。

簡単な解決策は、追加することです正しいの実装Equals()GetHashCode()あなたが(すなわちブックと著者)を比較しているオブジェクトグラフに参加するすべてのクラスにします。

IEqualityComparerインターフェイスは、あなたが実装することができ便利ですEquals()し、GetHashCode()別のクラスにあなたが比較の異なる方法を使用している場合は、比較する必要がある、またはクラスの内部へのアクセスを持っていないとき。


参加しているオブジェクトに関するこの輝かしいコメントをありがとうございました。
suhyura

11

Equals()をオーバーライドしましたが、GetHashCode()もオーバーライドしていることを確認してください


GetHashCode()を強調するための+1。基本的なHashCode実装を次のように追加しないでください<custom>^base.GetHashCode()
ダニ

8

上記の答えは間違っています!!! MSDNに明記されているように、デフォルトのEquatorを返します。デフォルトプロパティは、T型がSystem.IEquatableインターフェイスを実装しているかどうかをチェックし、実装している場合は、その実装を使用するEqualityComparerを返します。それ以外の場合は、Tによって提供されるObject.EqualsおよびObject.GetHashCodeのオーバーライドを使用するEqualityComparerを返します。

つまり、Equalsをオーバーライドする限り、問題ありません。

コードが機能していない理由は、firstname == lastnameを確認するためです。

https://msdn.microsoft.com/library/bb348436(v=vs.100).aspxおよびhttps://msdn.microsoft.com/en-us/library/ms224763(v=vs.100).aspxを参照してください


0

計算されたハッシュに基づいて一意性をチェックするリストの拡張メソッドを使用できます。IEnumerableをサポートするように拡張メソッドを変更することもできます。

例:

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

List<Employee> employees = new List<Employee>();
employees.Add(new Employee{Name="XYZ", Age=30});
employees.Add(new Employee{Name="XYZ", Age=30});

employees = employees.Unique(); //Gives list which contains unique objects. 

拡張方法:

    public static class LinqExtension
        {
            public static List<T> Unique<T>(this List<T> input)
            {
                HashSet<string> uniqueHashes = new HashSet<string>();
                List<T> uniqueItems = new List<T>();

                input.ForEach(x =>
                {
                    string hashCode = ComputeHash(x);

                    if (uniqueHashes.Contains(hashCode))
                    {
                        return;
                    }

                    uniqueHashes.Add(hashCode);
                    uniqueItems.Add(x);
                });

                return uniqueItems;
            }

            private static string ComputeHash<T>(T entity)
            {
                System.Security.Cryptography.SHA1CryptoServiceProvider sh = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                string input = JsonConvert.SerializeObject(entity);

                byte[] originalBytes = ASCIIEncoding.Default.GetBytes(input);
                byte[] encodedBytes = sh.ComputeHash(originalBytes);

                return BitConverter.ToString(encodedBytes).Replace("-", "");
            }

-1

これは、次の2つの方法で実現できます。

1. Equarable.Distinct メソッドのようにIEquatableインターフェイスを実装するか、この投稿で@skalbの回答を確認できます

2.オブジェクトに一意のキーがない場合は、GroupByメソッドを使用して個別のオブジェクトリストを取得できます。そのためには、オブジェクトのすべてのプロパティをグループ化し、最初のオブジェクトを選択する必要があります。

たとえば、以下のようにして私のために働いています:

var distinctList= list.GroupBy(x => new {
                            Name= x.Name,
                            Phone= x.Phone,
                            Email= x.Email,
                            Country= x.Country
                        }, y=> y)
                       .Select(x => x.First())
                       .ToList()

MyObjectクラスは次のようになります。

public class MyClass{
       public string Name{get;set;}
       public string Phone{get;set;}
       public string Email{get;set;}
       public string Country{get;set;}
}

3.オブジェクトに一意のキーがある場合、それはグループでのみ使用できます。

たとえば、オブジェクトの一意のキーはIdです。

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.