HashSet เปรียบเทียบองค์ประกอบเพื่อความเท่าเทียมกันอย่างไร


128

ฉันมีคลาสที่IComparable:

public class a : IComparable
{
    public int Id { get; set; }
    public string Name { get; set; }

    public a(int id)
    {
        this.Id = id;
    }

    public int CompareTo(object obj)
    {
        return this.Id.CompareTo(((a)obj).Id);
    }
}

เมื่อฉันเพิ่มรายการวัตถุของคลาสนี้ในชุดแฮช:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(a1);

ทุกอย่างดีและha.countเป็น2แต่:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(new a(1));

ตอนนี้ha.countคือ3.

  1. ทำไมไม่HashSetเคารพa's CompareToวิธี
  2. เป็นHashSetวิธีที่ดีที่สุดที่จะมีรายชื่อของวัตถุที่ไม่ซ้ำกันหรือไม่?

เพิ่มการดำเนินงานของในตัวสร้างหรือใช้มันในชั้นเรียนIEqualityComparer<T> msdn.microsoft.com/en-us/library/bb301504(v=vs.110).aspxa
Jaider

คำตอบ:


138

ใช้IEqualityComparer<T>( EqualityComparer<T>.Defaultเว้นแต่คุณจะระบุสิ่งที่แตกต่างในการก่อสร้าง)

เมื่อคุณเพิ่มองค์ประกอบลงในชุดจะพบรหัสแฮชโดยใช้IEqualityComparer<T>.GetHashCodeและจัดเก็บทั้งรหัสแฮชและองค์ประกอบ (หลังจากตรวจสอบว่าองค์ประกอบนั้นอยู่ในชุดแล้วหรือไม่)

ในการค้นหาองค์ประกอบขั้นแรกจะใช้IEqualityComparer<T>.GetHashCodeเพื่อค้นหารหัสแฮชจากนั้นสำหรับองค์ประกอบทั้งหมดที่มีรหัสแฮชเดียวกันจะใช้IEqualityComparer<T>.Equalsเพื่อเปรียบเทียบเพื่อความเท่าเทียมที่แท้จริง

นั่นหมายความว่าคุณมีสองทางเลือก:

  • ส่งผ่านแบบกำหนดเองIEqualityComparer<T>ไปยังตัวสร้าง นี่เป็นตัวเลือกที่ดีที่สุดหากคุณไม่สามารถแก้ไขTตัวเองได้หรือหากคุณต้องการความสัมพันธ์แบบไม่เป็นค่าเริ่มต้น (เช่น "ผู้ใช้ทั้งหมดที่มี ID ผู้ใช้ติดลบจะถือว่าเท่ากัน") สิ่งนี้แทบจะไม่เคยใช้กับประเภทตัวเองเลย (เช่นFooไม่ได้ใช้งานIEqualityComparer<Foo>) แต่เป็นประเภทแยกต่างหากซึ่งใช้สำหรับการเปรียบเทียบเท่านั้น
  • ใช้ความเท่าเทียมกันในรูปแบบของตัวเองโดยการเอาชนะและGetHashCode Equals(object)ตามIEquatable<T>หลักการแล้วให้ใช้งานในประเภทด้วยโดยเฉพาะอย่างยิ่งถ้าเป็นประเภทค่า วิธีการเหล่านี้จะถูกเรียกโดยตัวเปรียบเทียบค่าเริ่มต้นของความเท่าเทียมกัน

สังเกตว่าสิ่งนี้ไม่ได้อยู่ในแง่ของการเปรียบเทียบแบบเรียงลำดับ - ซึ่งสมเหตุสมผลเนื่องจากมีสถานการณ์ที่คุณสามารถระบุความเท่าเทียมกันได้อย่างง่ายดาย แต่ไม่ใช่การสั่งซื้อทั้งหมด ทั้งหมดนี้เหมือนกับDictionary<TKey, TValue>โดยทั่วไป

หากคุณต้องการชุดที่ใช้การสั่งซื้อแทนการเปรียบเทียบความเท่าเทียมกันคุณควรใช้SortedSet<T>จาก. NET 4 - ซึ่งช่วยให้คุณระบุ a IComparer<T>แทนIEqualityComparer<T>ไฟล์. นี้จะใช้IComparer<T>.Compare- ซึ่งจะมอบหมายให้IComparable<T>.CompareToหรือถ้าคุณกำลังใช้IComparable.CompareToComparer<T>.Default


7
+1 โปรดสังเกตคำตอบของ @ tyriker (IMO นั้นควรเป็นความคิดเห็นที่นี่) ซึ่งชี้ให้เห็นว่าวิธีที่ง่ายที่สุดในการใช้ประโยชน์ดังกล่าวIEqualityComparer<T>.GetHashCode/Equals()คือการนำไปใช้EqualsและGetHashCodeในTตัวมันเอง (และในขณะที่คุณทำเช่นนั้นคุณจะต้องใช้คู่พิมพ์ที่พิมพ์มากเกินไป : - bool IEquatable<T>.Equals(T other))
Ruben Bartelink

5
แม้ว่าคำตอบที่ถูกต้องมากอาจทำให้สับสนโดยเฉพาะอย่างยิ่งสำหรับผู้ใช้ใหม่เนื่องจากไม่ได้ระบุอย่างชัดเจนว่าสำหรับกรณีที่ง่ายที่สุดที่จะแทนที่EqualsและGetHashCodeเพียงพอ - ดังที่กล่าวไว้ในคำตอบของ @ tyriker
BartoszKP

Imo เมื่อคุณนำไปใช้IComparable(หรือIComparerสำหรับเรื่องนั้น) คุณไม่ควรถูกขอให้ใช้ความเท่าเทียมกันแยกกัน (แต่เพียงแค่GetHashCode) ในแง่หนึ่งอินเทอร์เฟซการเปรียบเทียบควรสืบทอดมาจากอินเทอร์เฟซความเท่าเทียมกัน ฉันเข้าใจถึงประโยชน์ด้านประสิทธิภาพของการมีฟังก์ชันสองฟังก์ชันแยกกัน (ซึ่งคุณสามารถปรับความเท่าเทียมกันให้เหมาะสมโดยแยกจากกันเพียงแค่บอกว่าบางอย่างเท่ากันหรือไม่) แต่ก็ยัง .. สับสนมากเมื่อคุณระบุเมื่ออินสแตนซ์เท่ากันในCompareToฟังก์ชันและเฟรมเวิร์กจะไม่พิจารณา ที่.
nawfal

@nawfal ไม่ใช่ทุกอย่างที่มีเหตุผล ถ้าคุณกำลังเปรียบเทียบสองสิ่งที่มีคุณสมบัติบูลมันเป็นเพียงธรรมดาอันยิ่งใหญ่ที่จะต้องมีการเขียนสิ่งที่ต้องการa.boolProp == b.boolProp ? 1 : 0หรือมันควรจะเป็นหรือa.boolProp == b.boolProp ? 0 : -1 a.boolProp == b.boolProp ? 1 : -1จุ๊บ!
Simon_Weaver

1
@Simon_Weaver มันคือ ฉันต้องการหลีกเลี่ยงมันในคุณสมบัติสมมุติของฉันที่ฉันเสนอ
nawfal

77

นี่คือคำชี้แจงในส่วนหนึ่งของคำตอบที่ถูกทิ้งไว้โดยไม่ได้กล่าว: ประเภทวัตถุของคุณHashSet<T>ไม่จำเป็นต้องใช้งานIEqualityComparer<T>แต่ต้องแทนที่Object.GetHashCode()Object.Equals(Object obj)และ

แทนสิ่งนี้:

public class a : IEqualityComparer<a>
{
  public int GetHashCode(a obj) { /* Implementation */ }
  public bool Equals(a obj1, a obj2) { /* Implementation */ }
}

คุณทำสิ่งนี้:

public class a
{
  public override int GetHashCode() { /* Implementation */ }
  public override bool Equals(object obj) { /* Implementation */ }
}

เป็นเรื่องละเอียดอ่อน แต่สิ่งนี้ทำให้ฉันสะดุดในส่วนที่ดีขึ้นของวันที่พยายามทำให้ HashSet ทำงานตามที่ตั้งใจไว้ และเช่นเดียวกับคนอื่น ๆ กล่าวว่าHashSet<a>จะจบลงด้วยการโทรa.GetHashCode()และa.Equals(obj)ตามความจำเป็นเมื่อทำงานกับชุด


2
จุดดี. BTW ตามที่กล่าวไว้ในความคิดเห็นของฉันเกี่ยวกับคำตอบของ @ JonSkeet คุณควรใช้bool IEquatable<T>.Equals(T other)เพื่อเพิ่มประสิทธิภาพเล็กน้อย แต่ที่สำคัญกว่าคือประโยชน์ที่ชัดเจน ด้วยเหตุผล obv นอกเหนือจากความจำเป็นในการใช้งานGetHashCodeควบคู่ไปIEquatable<T>ด้วยแล้วเอกสารสำหรับ IEquatable <T> กล่าวว่าเพื่อความสอดคล้องกันคุณควรลบล้างความobject.Equalsสอดคล้องด้วย
Ruben Bartelink

ฉันลองใช้สิ่งนี้แล้ว ใช้ovveride getHashcodeงานoverride bool equalsได้แต่ได้รับข้อผิดพลาด: ไม่พบวิธีการที่จะแทนที่ ความคิดใด
Stefanvds

ในที่สุดข้อมูลที่ฉันกำลังมองหา ขอบคุณ.
Mauro Sampietro

จากความคิดเห็นของฉันเกี่ยวกับคำตอบข้างต้น - ในกรณี "แทนที่จะเป็น" ของคุณคุณอาจมีpublic class a : IEqualityComparer<a> {แล้วnew HashSet<a>(a)ก็ได้
HankCa

แต่ดูความคิดเห็นของ Jon Skeets ด้านบน
HankCa

9

HashSetใช้EqualsและGetHashCode().

CompareTo สำหรับชุดที่สั่งซื้อ

หากคุณต้องการออบเจ็กต์ที่ไม่ซ้ำใคร แต่คุณไม่สนใจลำดับการทำซ้ำHashSet<T>มักเป็นทางเลือกที่ดีที่สุด


5

ตัวสร้าง HashSet รับวัตถุที่ใช้ IEqualityComparer สำหรับการเพิ่มวัตถุใหม่ หากคุณไม่ใช้วิธีการใน HashSet คุณจะต้องลบล้าง Equals, GetHashCode

namespace HashSet
{
    public class Employe
    {
        public Employe() {
        }

        public string Name { get; set; }

        public override string ToString()  {
            return Name;
        }

        public override bool Equals(object obj) {
            return this.Name.Equals(((Employe)obj).Name);
        }

        public override int GetHashCode() {
            return this.Name.GetHashCode();
        }
    }

    class EmployeComparer : IEqualityComparer<Employe>
    {
        public bool Equals(Employe x, Employe y)
        {
            return x.Name.Trim().ToLower().Equals(y.Name.Trim().ToLower());
        }

        public int GetHashCode(Employe obj)
        {
            return obj.Name.GetHashCode();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            HashSet<Employe> hashSet = new HashSet<Employe>(new EmployeComparer());
            hashSet.Add(new Employe() { Name = "Nik" });
            hashSet.Add(new Employe() { Name = "Rob" });
            hashSet.Add(new Employe() { Name = "Joe" });
            Display(hashSet);
            hashSet.Add(new Employe() { Name = "Rob" });
            Display(hashSet);

            HashSet<Employe> hashSetB = new HashSet<Employe>(new EmployeComparer());
            hashSetB.Add(new Employe() { Name = "Max" });
            hashSetB.Add(new Employe() { Name = "Solomon" });
            hashSetB.Add(new Employe() { Name = "Werter" });
            hashSetB.Add(new Employe() { Name = "Rob" });
            Display(hashSetB);

            var union = hashSet.Union<Employe>(hashSetB).ToList();
            Display(union);
            var inter = hashSet.Intersect<Employe>(hashSetB).ToList();
            Display(inter);
            var except = hashSet.Except<Employe>(hashSetB).ToList();
            Display(except);

            Console.ReadKey();
        }

        static void Display(HashSet<Employe> hashSet)
        {
            if (hashSet.Count == 0)
            {
                Console.Write("Collection is Empty");
                return;
            }
            foreach (var item in hashSet)
            {
                Console.Write("{0}, ", item);
            }
            Console.Write("\n");
        }

        static void Display(List<Employe> list)
        {
            if (list.Count == 0)
            {
                Console.WriteLine("Collection is Empty");
                return;
            }
            foreach (var item in list)
            {
                Console.Write("{0}, ", item);
            }
            Console.Write("\n");
        }
    }
}

จะเกิดอะไรขึ้นถ้าชื่อเป็นโมฆะ? ค่าแฮชของ null คืออะไร?
joe
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.