การใช้งานเริ่มต้นสำหรับ Object.GetHashCode ()


162

การใช้งานเริ่มต้นGetHashCode()ทำงานอย่างไร และมันจัดการโครงสร้างคลาสอาเรย์และอื่น ๆ อย่างมีประสิทธิภาพและเพียงพอหรือไม่

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


ลองดูความคิดเห็นที่ฉันทิ้งไว้ในบทความ: stackoverflow.com/questions/763731/gethashcode-extension-method
Paul Westcott


34
นอกเหนือ: คุณสามารถรับแฮชโค้ดที่เป็นค่าเริ่มต้น (แม้ว่าGetHashCode()จะถูกแทนที่) โดยใช้System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
Marc Gravell

@MarcGravell ขอบคุณสำหรับการมีส่วนร่วมฉันได้ค้นหาคำตอบนี้อย่างแน่นอน
Andrew Savinykh

@MarcGravell แต่ฉันจะทำเช่นนี้กับวิธีอื่นได้อย่างไร
Tomáš Zato - Reinstate Monica

คำตอบ:


86
namespace System {
    public class Object {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern int InternalGetHashCode(object obj);

        public virtual int GetHashCode() {
            return InternalGetHashCode(this);
        }
    }
}

InternalGetHashCodeถูกแมปไปยังฟังก์ชันObjectNative :: GetHashCodeใน CLR ซึ่งมีลักษณะดังนี้:

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {  
    CONTRACTL  
    {  
        THROWS;  
        DISABLED(GC_NOTRIGGER);  
        INJECT_FAULT(FCThrow(kOutOfMemoryException););  
        MODE_COOPERATIVE;  
        SO_TOLERANT;  
    }  
    CONTRACTL_END;  

    VALIDATEOBJECTREF(obj);  

    DWORD idx = 0;  

    if (obj == 0)  
        return 0;  

    OBJECTREF objRef(obj);  

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame  

    idx = GetHashCodeEx(OBJECTREFToObject(objRef));  

    HELPER_METHOD_FRAME_END();  

    return idx;  
}  
FCIMPLEND

การดำเนินการของGetHashCodeExมีขนาดใหญ่พอสมควรดังนั้นจึงเป็นเรื่องง่ายที่จะเชื่อมโยงเพียงเพื่อซอร์สโค้ด C ++


5
เอกสารอ้างอิงนั้นต้องมาจากเวอร์ชันแรกสุด มันไม่ได้ถูกเขียนในบทความ MSDN ปัจจุบันอีกต่อไปอาจเป็นเพราะมันค่อนข้างผิด
Hans Passant

4
พวกเขาเปลี่ยนถ้อยคำใช่ แต่ก็ยังพูดโดยทั่วไปว่า: "ดังนั้นการใช้งานเริ่มต้นของวิธีนี้จะต้องไม่ถูกใช้เป็นตัวระบุวัตถุที่ไม่ซ้ำกันเพื่อวัตถุประสงค์ในการแฮช"
เดวิดบราวน์

7
ทำไมเอกสารอ้างว่าการนำไปปฏิบัติไม่ได้มีประโยชน์อย่างยิ่งสำหรับการแปลงแป้นพิมพ์? หากวัตถุมีค่าเท่ากับตัวเองและไม่มีอะไรอื่นวิธีการแฮชโค้ดใด ๆ ที่มักจะส่งคืนค่าเดียวกันสำหรับอินสแตนซ์ของวัตถุที่กำหนดและโดยทั่วไปจะคืนค่าที่แตกต่างกันสำหรับอินสแตนซ์ที่แตกต่างกัน
supercat

3
@ ta.speot.is: หากสิ่งที่คุณต้องการคือการตรวจสอบว่ามีการเพิ่มอินสแตนซ์เฉพาะลงในพจนานุกรมหรือไม่การอ้างอิงความเท่าเทียมกันนั้นสมบูรณ์แบบ ตามที่คุณทราบสตริงมักจะสนใจว่าสตริงที่มีลำดับอักขระเหมือนกันได้ถูกเพิ่มเข้าไปแล้ว นั่นเป็นเหตุผลที่การแทนที่string GetHashCodeในทางกลับกันสมมติว่าคุณต้องการเก็บจำนวนการควบคุมPaintเหตุการณ์ที่ประมวลผลเหตุการณ์ต่าง ๆ คุณสามารถใช้Dictionary<Object, int[]>(ทุกรายการที่int[]เก็บไว้จะมีหนึ่งรายการ)
supercat

6
@ It'sNotALie จากนั้นขอบคุณArchive.orgสำหรับการคัดลอก ;-)
RobIII

88

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

เมื่อเอาชนะความเท่าเทียมกันคุณควรมีการจับคู่เสมอEquals()และGetHashCode()(เช่นสำหรับสองค่าหากEquals()คืนจริงพวกเขาจะต้องส่งคืนรหัสแฮชเดียวกัน แต่ไม่จำเป็นต้องมีการสนทนา) - และเป็นเรื่องปกติที่จะมีผู้ให้บริการ==/ !=และบ่อยครั้ง ใช้งานIEquatable<T>เกินไป

สำหรับการสร้างรหัสแฮชเป็นเรื่องปกติที่จะใช้ผลรวมที่แยกออกมาเนื่องจากเป็นการหลีกเลี่ยงการชนกันของค่าที่จับคู่ตัวอย่างเช่นสำหรับแฮชฟิลด์ 2 พื้นฐาน:

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
    int hash = 27;
    hash = (13 * hash) + field1.GetHashCode();
    hash = (13 * hash) + field2.GetHashCode();
    return hash;
}

นี่คือข้อดีที่:

  • แฮชของ {1,2} ไม่เหมือนกับแฮชของ {2,1}
  • แฮชของ {1,1} ไม่เหมือนกับแฮชของ {2,2}

ฯลฯ - ซึ่งอาจเป็นเรื่องธรรมดาหากใช้เพียงผลรวมที่ไม่ได้กระจายหรือ xor ( ^) ฯลฯ


จุดดีเยี่ยมเกี่ยวกับประโยชน์ของอัลกอริทึมผลรวม สิ่งที่ฉันไม่เคยรู้มาก่อน!
ช่องโหว่ของ

จำนวนเงินแฟคตอริ่ง (ตามที่เขียนไว้ด้านบน) จะไม่ทำให้เกิดข้อยกเว้นล้นในบางครั้งหรือไม่
sinelaw

4
@sinelaw uncheckedใช่ก็ควรจะดำเนินการ โชคดีที่uncheckedเป็นค่าเริ่มต้นใน C # แต่มันจะดีกว่าที่จะทำให้ชัดเจน แก้ไขแล้ว
Marc Gravell

7

เอกสารประกอบสำหรับGetHashCodeวิธีการของObjectบอกว่า"การใช้งานเริ่มต้นของวิธีนี้จะต้องไม่ใช้เป็นตัวระบุวัตถุที่ไม่ซ้ำกันเพื่อการแฮช" และหนึ่งสำหรับValueTypeบอกว่า"ถ้าคุณเรียกใช้เมธอด GetHashCode ของประเภทที่ได้รับค่าส่งคืนนั้นไม่น่าจะเหมาะสำหรับใช้เป็นคีย์ในตารางแฮช" .

ชนิดข้อมูลพื้นฐานชอบbyte, short, int, long, charและstringใช้วิธีการที่ดี GetHashCode คลาสและโครงสร้างอื่น ๆPointเช่นใช้GetHashCodeวิธีการที่เหมาะสมหรือไม่เหมาะสมกับความต้องการของคุณ คุณเพียงแค่ต้องลองดูว่ามันดีพอหรือยัง

เอกสารสำหรับแต่ละชั้นเรียนหรือโครงสร้างสามารถบอกคุณได้ว่ามันแทนที่การใช้งานเริ่มต้นหรือไม่ หากไม่ได้แทนที่คุณควรใช้การดำเนินการของคุณเอง สำหรับคลาสหรือโครงสร้างใด ๆ ที่คุณสร้างเองในที่ที่คุณต้องการใช้GetHashCodeวิธีการนั้นคุณควรสร้างการใช้งานของคุณเองโดยใช้สมาชิกที่เหมาะสมในการคำนวณรหัสแฮช


2
ฉันไม่เห็นด้วยที่คุณควรเพิ่มการใช้งานของคุณเองเป็นประจำ เพียงแค่ส่วนใหญ่ของคลาส (โดยเฉพาะ) จะไม่ถูกทดสอบเพื่อความเท่าเทียมกัน - หรือว่าพวกเขาอยู่ที่ไหนความเท่าเทียมกันของการอ้างอิงแบบ inbuilt นั้นดี ในโอกาส (หายากอยู่แล้ว) ในการเขียน struct มันจะเป็นเรื่องธรรมดามากขึ้นจริง
Marc Gravell

@ Marc Gravel: แน่นอนว่าไม่ใช่สิ่งที่ฉันตั้งใจจะพูด ฉันจะปรับย่อหน้าสุดท้าย :)
Guffa

ชนิดข้อมูลพื้นฐานไม่ได้ใช้วิธีการที่ดีของ GetHashCode อย่างน้อยก็ในกรณีของฉัน ตัวอย่างเช่น GetHashCode สำหรับ int คืนค่าตัวเลขเอง: (123) .GetHashCode () ส่งคืน 123
fdermishin

5
@ user502144 และมีอะไรผิดปกติกับเรื่องนี้? มันเป็นตัวบ่งชี้เฉพาะที่สมบูรณ์แบบที่ง่ายต่อการคำนวณโดยไม่มีการบวกเท็จเกี่ยวกับความเสมอภาค ...
ริชาร์ด Rast

@Richard Rast: มันใช้ได้ยกเว้นปุ่มสามารถกระจายได้ไม่ดีเมื่อใช้ใน Hashtable ลองดูคำตอบนี้: stackoverflow.com/a/1388329/502144
fdermishin

5

เนื่องจากฉันไม่พบคำตอบที่อธิบายว่าทำไมเราจึงควรแทนที่GetHashCodeและEqualsโครงสร้างที่กำหนดเองและทำไมการปรับใช้เริ่มต้น "ไม่น่าจะเหมาะสำหรับใช้เป็นกุญแจในตารางแฮช" ฉันจะทิ้งลิงค์ไปยังบล็อกนี้ โพสต์ซึ่งอธิบายว่าทำไมด้วยตัวอย่างจริงของปัญหาที่เกิดขึ้น

ฉันขอแนะนำให้อ่านโพสต์ทั้งหมด แต่นี่เป็นบทสรุป (เพิ่มการเน้นและการชี้แจง)

เหตุผลที่แฮชเริ่มต้นสำหรับ structs ช้าและไม่ดีมาก:

วิธีที่ CLR ได้รับการออกแบบทุกการโทรไปยังสมาชิกที่กำหนดไว้ในSystem.ValueTypeหรือSystem.Enumประเภท [อาจ] ทำให้เกิดการจัดสรรมวย [... ]

ตัวดำเนินการของฟังก์ชันแฮชเผชิญภาวะที่กลืนไม่เข้าคายไม่ออก: ทำการกระจายที่ดีของฟังก์ชั่นแฮชหรือทำให้มันรวดเร็ว ในบางกรณีก็เป็นไปได้เพื่อให้บรรลุพวกเขาทั้งสอง แต่มันก็เป็นเรื่องยากที่จะทำเช่นนี้โดยทั่วไปValueType.GetHashCodeใน

ฟังก์ชันแฮชแบบบัญญัติของโครงสร้าง "รวม" รหัสแฮชของฟิลด์ทั้งหมด แต่วิธีเดียวที่จะได้รับรหัสกัญชาของสนามในValueTypeวิธีการคือการใช้สะท้อน ดังนั้นผู้เขียน CLR จึงตัดสินใจแลกเปลี่ยนความเร็วในการแจกจ่ายและGetHashCodeรุ่นเริ่มต้นจะส่งคืนรหัสแฮชของเขตข้อมูลที่ไม่ใช่ศูนย์แรกและ "munges" ด้วยรหัสประเภท [... ] นี่เป็นพฤติกรรมที่สมเหตุสมผลเว้นแต่จะไม่ใช่ . ตัวอย่างเช่นหากคุณโชคไม่ดีพอและฟิลด์แรกของ struct ของคุณมีค่าเท่ากันสำหรับอินสแตนซ์ส่วนใหญ่แล้วฟังก์ชันแฮชจะให้ผลลัพธ์เดียวกันตลอดเวลา และอย่างที่คุณอาจจินตนาการว่าสิ่งนี้จะส่งผลกระทบอย่างมากต่อประสิทธิภาพการทำงานหากอินสแตนซ์เหล่านี้ถูกเก็บไว้ในชุดแฮชหรือตารางแฮช

[ ... ] การดำเนินการสะท้อนตามช้า ช้ามาก.

[... ] ทั้งคู่ValueType.EqualsและValueType.GetHashCodeมีการเพิ่มประสิทธิภาพพิเศษ ถ้าเป็นชนิดที่ไม่ได้มี "ชี้" และเต็มไปอย่างถูกต้อง [ ... ] แล้วรุ่นที่เหมาะสมมากขึ้นมีการใช้GetHashCodeiterates กว่าตัวอย่างและ XORs บล็อคของ 4 ไบต์และวิธีการเปรียบเทียบสองกรณีใช้Equals memcmp[... ] แต่การเพิ่มประสิทธิภาพนั้นยุ่งยากมาก อันดับแรกเป็นเรื่องยากที่จะทราบเมื่อเปิดใช้งานการเพิ่มประสิทธิภาพ [... ] ประการที่สองการเปรียบเทียบหน่วยความจำจะไม่จำเป็นต้องให้ผลลัพธ์ที่ถูกต้องแก่คุณ นี่คือตัวอย่างง่ายๆ: [... ] -0.0และ+0.0เท่ากัน แต่มีการแทนเลขฐานสองที่แตกต่างกัน

ปัญหาโลกแห่งความจริงที่อธิบายไว้ในโพสต์:

private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
    // Empty almost all the time
    public string OptionalDescription { get; }
    public string Path { get; }
    public int Position { get; }
}

เราใช้ tuple ที่มีโครงสร้างแบบกำหนดเองพร้อมการนำความเท่าเทียมกันเริ่มต้นมาใช้ และโชคไม่ดีที่ struct มีสนามแรกที่เป็นตัวเลือกที่ได้รับมักจะเท่ากับ [สตริงว่าง] ประสิทธิภาพการทำงานก็โอเคจนกระทั่งจำนวนองค์ประกอบในชุดเพิ่มขึ้นอย่างมากทำให้เกิดปัญหาประสิทธิภาพจริงใช้เวลาไม่กี่นาทีในการเริ่มต้นการรวบรวมด้วยรายการนับหมื่น

ดังนั้นเพื่อตอบคำถาม "ในกรณีใดฉันควรแพ็คของตัวเองและในกรณีใดที่ฉันสามารถพึ่งพาการใช้งานเริ่มต้นได้อย่างปลอดภัย" อย่างน้อยในกรณีของstructsคุณควรลบล้างEqualsและGetHashCodeเมื่อใดก็ตามที่โครงสร้างแบบกำหนดเองของคุณอาจใช้เป็น Dictionaryที่สำคัญในตารางแฮชหรือ
ฉันขอแนะนำให้ใช้งานIEquatable<T>ในกรณีนี้เพื่อหลีกเลี่ยงการชกมวย

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


1

โดยทั่วไปถ้าคุณกำลังเอาชนะเท่ากับคุณต้องการแทนที่ GetHashCode เหตุผลนี้เป็นเพราะทั้งคู่ใช้เพื่อเปรียบเทียบความเท่าเทียมกันของคลาส / โครงสร้างของคุณ

เท่ากับใช้เมื่อตรวจสอบ Foo A, B;

ถ้า (A == B)

เนื่องจากเรารู้ว่าตัวชี้ไม่น่าจะตรงกันเราจึงสามารถเปรียบเทียบสมาชิกภายในได้

Equals(obj o)
{
    if (o == null) return false;
    MyType Foo = o as MyType;
    if (Foo == null) return false;
    if (Foo.Prop1 != this.Prop1) return false;

    return Foo.Prop2 == this.Prop2;
}

GetHashCode โดยทั่วไปจะใช้โดยตารางแฮช hashcode ที่สร้างโดยคลาสของคุณควรเหมือนกันเสมอสำหรับคลาสที่ให้สถานะ

ฉันมักจะทำ

GetHashCode()
{
    int HashCode = this.GetType().ToString().GetHashCode();
    HashCode ^= this.Prop1.GetHashCode();
    etc.

    return HashCode;
}

บางคนจะบอกว่าควรคำนวณ hashcode หนึ่งครั้งต่ออายุการใช้งานของวัตถุ แต่ฉันไม่เห็นด้วย (และฉันอาจผิด)

ใช้การใช้งานเริ่มต้นให้โดยวัตถุเว้นแต่ว่าคุณมีการอ้างอิงเดียวกันกับหนึ่งในชั้นเรียนของคุณพวกเขาจะไม่เท่ากับซึ่งกันและกัน โดยการเอาชนะ Equals และ GetHashCode คุณสามารถรายงานความเท่าเทียมกันตามค่าภายในแทนที่จะเป็นการอ้างอิงวัตถุ


2
วิธี ^ = ไม่ใช่วิธีที่ดีโดยเฉพาะอย่างยิ่งสำหรับการสร้างแฮช - ซึ่งมีแนวโน้มที่จะนำไปสู่การชนทั่วไป / คาดเดาได้มากมาย - ตัวอย่างเช่นถ้า Prop1 = Prop2 = 3
Marc Gravell

หากค่าเหมือนกันฉันไม่เห็นปัญหาการชนเนื่องจากวัตถุมีค่าเท่ากัน The 13 * Hash + NewHash นั้นน่าสนใจ
Bennett Dill

2
เบ็น: ลอง Obj1 {Prop1 = 12, Prop2 = 12} และ Obj2 {Prop1 = 13, Prop2 = 13}
Tomáš Kafka

0

หากคุณกำลังติดต่อกับ POCO คุณสามารถใช้ยูทิลิตี้นี้เพื่อทำให้ชีวิตของคุณง่ายขึ้น:

var hash = HashCodeUtil.GetHashCode(
           poco.Field1,
           poco.Field2,
           ...,
           poco.FieldN);

...

public static class HashCodeUtil
{
    public static int GetHashCode(params object[] objects)
    {
        int hash = 13;

        foreach (var obj in objects)
        {
            hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
        }

        return hash;
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.