ความแตกต่างที่ไม่ทำงานกับ LINQ กับ 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 สองครั้ง ทำไม? ฉันได้ลองลบล้างเมธอด Equals ในคลาส Author แล้ว ความโดดเด่นยังดูเหมือนจะไม่ทำงาน ฉันขาดอะไรไป?

แก้ไข: ฉันได้เพิ่ม == และ! = ตัวดำเนินการมากเกินไปด้วย ยังไม่มีความช่วยเหลือ

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

คำตอบ:


159

LINQ Distinct ไม่ฉลาดเท่าไหร่เมื่อพูดถึงออบเจ็กต์แบบกำหนดเอง

เพียงแค่ดูรายการของคุณและดูว่ามีวัตถุสองชิ้นที่แตกต่างกัน (ไม่สนใจว่าจะมีค่าเดียวกันสำหรับช่องสมาชิก)

หนึ่งวิธีแก้ปัญหาคือการใช้อินเตอร์เฟซ 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 implemement () และ Object.GetHashCode () ด้วยกัน IEquatable <T> .Equals ไม่ได้แทนที่ Object.Equals ดังนั้นสิ่งนี้จะล้มเหลวเมื่อทำการเปรียบเทียบที่ไม่ได้พิมพ์มากเกินไปซึ่งมักเกิดขึ้นในเฟรมเวิร์กและเสมอในคอลเล็กชันที่ไม่ใช่แบบทั่วไป
AndyM

ดังนั้นจะดีกว่าไหมหากใช้การแทนที่ความแตกต่างที่ใช้ IEqualityComparer <T> ตามที่ Rex M แนะนำ ฉันหมายถึงสิ่งที่ฉันควรจะทำถ้าฉันไม่อยากตกหลุมพราง
Tanmoy

3
@ Tanmoy มันขึ้นอยู่กับ. หากคุณต้องการให้ผู้เขียนทำงานเหมือนวัตถุทั่วไป (กล่าวคือความเท่าเทียมกันในการอ้างอิงเท่านั้น) แต่ตรวจสอบค่าชื่อเพื่อวัตถุประสงค์ในการแตกต่างให้ใช้ IEqualityComparer หากคุณมักจะต้องการผู้เขียนวัตถุที่จะเปรียบเทียบตามค่าชื่อแล้วแทนที่ GetHashCode และเท่ากับหรือใช้ IEquatable
Rex M

3
ฉันใช้งานIEquatable(และ overrode Equals/ GetHashCode) แต่ไม่มีเบรกพอยต์ของฉันที่ยิงด้วยวิธีการเหล่านี้บน Linq Distinct?
PeterX

2
@PeterX ฉันก็สังเกตเห็นสิ่งนี้เช่นกัน ฉันมีจุดพักในGetHashCodeและEqualsพวกเขาถูกชนระหว่างห่วงหน้า เนื่องจากการvar temp = books.SelectMany(book => book.Authors).Distinct();ส่งคืน an IEnumerableซึ่งหมายความว่าคำขอจะไม่ถูกดำเนินการทันทีจะดำเนินการเมื่อมีการใช้ข้อมูลเท่านั้น หากคุณต้องการตัวอย่างของการยิงนี้ทันทีให้เพิ่ม.ToList()หลัง.Distinct()และคุณจะเห็นจุดพักในEqualsและGetHashCodeก่อนหน้า
JabberwockyDecompiler

70

Distinct()ความเสมอภาคการตรวจสอบวิธีการอ้างอิงสำหรับประเภทของการอ้างอิง ซึ่งหมายความว่ามันกำลังมองหาอ็อบเจ็กต์เดียวกันที่ซ้ำกันอย่างแท้จริงไม่ใช่อ็อบเจ็กต์อื่นที่มีค่าเหมือนกัน

มีการโอเวอร์โหลดซึ่งใช้IEqualityComparerดังนั้นคุณสามารถระบุตรรกะที่แตกต่างกันเพื่อพิจารณาว่าวัตถุที่กำหนดเท่ากับวัตถุอื่นหรือไม่

หากคุณต้องการที่จะเขียนตามปกติทำตัวเหมือนวัตถุปกติ (IE เท่านั้นอ้างอิงความเท่าเทียมกัน) แต่สำหรับวัตถุประสงค์ของการตรวจสอบความเท่าเทียมกันที่แตกต่างด้วยค่าชื่อที่ใช้IEqualityComparer หากคุณต้องการเสมอผู้เขียนวัตถุที่จะเปรียบเทียบตามค่าชื่อแล้วแทนที่ GetHashCode และเท่ากับหรือใช้ IEquatable

สมาชิกสองคนบนIEqualityComparerอินเทอร์เฟซคือEqualsและGetHashCode. ตรรกะของคุณในการพิจารณาว่า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 () ของคุณแสดงให้ฉันเห็นว่าฉันยังขาดอะไรอยู่ ฉันส่งคืน {pass-in object} .GetHashCode () ไม่ใช่ {คุณสมบัติที่ใช้สำหรับการเปรียบเทียบ} .GetHashCode () สิ่งนี้สร้างความแตกต่างและอธิบายว่าเหตุใดของฉันจึงยังล้มเหลวการอ้างอิงที่แตกต่างกันสองรายการจะมีรหัสแฮชที่แตกต่างกันสองรหัส
pelazem

44

วิธีอื่นได้โดยไม่ต้องดำเนินการIEquatable, EqualsและGetHashCodeคือการใช้ LINQs 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

ดีกว่าการซับซ้อนด้วยวิธีการใช้งานและหากใช้ EF จะมอบหมายงานไปยังเซิร์ฟเวอร์ sql
Zapnologica

แม้ว่าวิธีนี้อาจใช้ได้ผล แต่จะมีปัญหาด้านประสิทธิภาพเนื่องจากจำนวนสิ่งที่ถูกจัดกลุ่ม
Bellash

@Bellash ทำให้มันทำงานแล้วทำให้เร็ว แน่นอนว่าการจัดกลุ่มนี้อาจทำให้ต้องทำงานให้เสร็จมากขึ้น แต่บางครั้งก็ยุ่งยากที่จะใช้งานมากกว่าที่คุณต้องการ
Jehof

2
ฉันชอบวิธีแก้ปัญหานี้ แต่โดยใช้วัตถุ "ใหม่" ใน groupby: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong

32

มีอีกวิธีหนึ่งในการรับค่าที่แตกต่างจากรายการประเภทข้อมูลที่ผู้ใช้กำหนด:

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

แน่นอนมันจะให้ชุดข้อมูลที่แตกต่างกัน


21

Distinct()ทำการเปรียบเทียบค่าเริ่มต้นของความเท่าเทียมกันบนวัตถุในตัวนับ หากคุณไม่ได้ลบล้างEquals()และGetHashCode()จะใช้การใช้งานเริ่มต้นบนobjectซึ่งเปรียบเทียบการอ้างอิง

วิธีแก้ปัญหาง่ายๆคือเพิ่มการใช้งานที่ถูกต้องEquals()และGetHashCode()ทุกชั้นเรียนที่มีส่วนร่วมในกราฟวัตถุที่คุณกำลังเปรียบเทียบ (เช่นหนังสือและผู้แต่ง)

IEqualityComparerอินเตอร์เฟซที่มีความสะดวกสบายที่ช่วยให้คุณในการดำเนินการEquals()และGetHashCode()ในชั้นเรียนที่แยกจากกันเมื่อคุณไม่ได้มีการเข้าถึง internals ของการเรียนที่คุณต้องการที่จะเปรียบเทียบหรือถ้าคุณกำลังใช้วิธีที่แตกต่างของการเปรียบเทียบ


ขอบคุณมากสำหรับคำแนะนำที่ยอดเยี่ยมเกี่ยวกับวัตถุที่เข้าร่วมนี้
suhyura

11

คุณได้ลบล้าง Equals () แต่ต้องแน่ใจว่าคุณได้ลบล้าง GetHashCode () ด้วย


+1 สำหรับเน้น GetHashCode () อย่าเพิ่มการใช้งาน HashCode พื้นฐานใน<custom>^base.GetHashCode()
Dani

8

คำตอบข้างบนผิด !!! ความแตกต่างตามที่ระบุไว้ใน MSDN จะส่งกลับค่า Equator เริ่มต้นซึ่งตามที่ระบุไว้คุณสมบัติ Default จะตรวจสอบว่าชนิด T ใช้อินเทอร์เฟซ System.IEquatable หรือไม่และถ้าเป็นเช่นนั้นจะส่งคืน EqualityComparer ที่ใช้การนำไปใช้ มิฉะนั้นจะส่งคืน EqualityComparer ที่ใช้การแทนที่ของ Object.Equals และ Object.GetHashCode ที่จัดทำโดย T

ซึ่งหมายความว่าตราบใดที่คุณเอาชนะเท่ากับคุณสบายดี

สาเหตุที่รหัสของคุณไม่ทำงานเนื่องจากคุณตรวจสอบ 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

คุณสามารถใช้วิธีการขยายในรายการซึ่งตรวจสอบความไม่ซ้ำกันตาม Hash ที่คำนวณได้ คุณยังสามารถเปลี่ยนวิธีการขยายเพื่อรองรับ 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

คุณสามารถบรรลุเป้าหมายนี้ได้ด้วยสองวิธี:

1. คุณสามารถใช้อินเทอร์เฟซ 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.