อัลกอริทึมที่ดีที่สุดสำหรับการเอาชนะ GetHashCode คืออะไร


1448

ใน. NET GetHashCodeวิธีนี้ถูกใช้ในหลาย ๆ ที่ทั่วทั้งไลบรารีคลาสฐาน. NET การใช้อย่างถูกต้องเป็นสิ่งสำคัญโดยเฉพาะอย่างยิ่งในการค้นหารายการอย่างรวดเร็วในคอลเลกชันหรือเมื่อพิจารณาความเท่าเทียมกัน

มีอัลกอริทึมมาตรฐานหรือวิธีปฏิบัติที่ดีที่สุดเกี่ยวกับวิธีการใช้GetHashCodeสำหรับคลาสที่กำหนดเองของฉันดังนั้นฉันจึงไม่ลดประสิทธิภาพการทำงานหรือไม่


38
GetHashCodeหลังจากที่ได้อ่านคำถามนี้และบทความดังต่อไปนี้ฉันสามารถใช้แทนที่ ฉันหวังว่ามันจะเป็นประโยชน์สำหรับคนอื่น ๆ คำแนะนำและกฎสำหรับ GetHashCode เขียนโดย Eric Lippert
rene

4
"หรือเพื่อกำหนดความเท่าเทียมกัน": ไม่! วัตถุสองชิ้นที่มีแฮชโค้ดเดียวกันนั้นไม่จำเป็นต้องเท่ากัน
Thomas Levesque

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

3
@ bitbonk โดยปกติทั้งคู่GetHashCode()และEquals()จำเป็นต้องดูทุกฟิลด์ของวัตถุทั้งสอง (เท่ากับต้องทำเช่นนี้หากแฮชโค้ดมีค่าเท่ากันหรือไม่ถูกตรวจสอบ) ด้วยเหตุนี้การโทรGetHashCode()ภายในEquals()จึงมักจะซ้ำซ้อนและอาจลดประสิทธิภาพลง Equals()อาจลัดวงจรทำให้เร็วขึ้น - ในบางกรณีแฮชโค้ดอาจถูกแคชทำให้การGetHashCode()ตรวจสอบเร็วขึ้นและคุ้มค่ามาก ดูคำถามนี้เพิ่มเติม
NotEnoughData

ข้อมูลล่าสุด ม.ค. 2020: บล็อกของ Eric Lippert อยู่ที่: docs.microsoft.com/en-us/archive/blogs/ericlippert/…
Rick Davin

คำตอบ:


1603

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

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        // Suitable nullity checks etc, of course :)
        hash = hash * 23 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

ดังที่ระบุไว้ในความคิดเห็นคุณอาจพบว่าเป็นการดีที่จะเลือกนายกใหญ่ที่จะคูณด้วย เห็นได้ชัดว่า 486187739 นั้นดี ... และถึงแม้ว่าตัวอย่างส่วนใหญ่ที่ฉันเห็นด้วยจำนวนน้อยมักจะใช้เฉพาะช่วงเวลา แต่ก็มีอัลกอริทึมที่คล้ายกันอย่างน้อยซึ่งมักใช้ตัวเลขที่ไม่สำคัญ ในFNV ไม่มากนักตัวอย่างต่อมาตัวอย่างเช่นผมเคยใช้ตัวเลขที่เห็นได้ชัดว่าทำงานได้ดี - แต่ค่าเริ่มต้นไม่ได้เป็นนายก (ค่าคงที่การคูณเป็นค่าที่สำคัญ แต่ฉันไม่รู้ว่ามันสำคัญขนาดไหน)

สิ่งนี้ดีกว่าการใช้XORแฮชโค้ดทั่วไปสองประการด้วยกัน สมมติว่าเรามีประเภทที่มีสองintฟิลด์:

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

โดยวิธีการอัลกอริทึมก่อนหน้านี้เป็นหนึ่งในปัจจุบันที่ใช้โดยคอมไพเลอร์ C # สำหรับประเภทที่ไม่ระบุชื่อ

หน้านี้มีตัวเลือกค่อนข้างน้อย ฉันคิดว่าในกรณีส่วนใหญ่ข้างต้นคือ "ดีพอ" และเป็นเรื่องง่ายที่จะจดจำและทำให้ถูกต้องอย่างไม่น่าเชื่อ FNVทางเลือกที่เป็นเรื่องง่ายเหมือนกัน แต่ใช้ค่าคงที่แตกต่างกันและXORแทนที่จะADDเป็นงานรวม มันดูบางสิ่งบางอย่างเช่นรหัสข้างล่างนี้ แต่อัลกอริทึม FNV ปกติดำเนินการในแต่ละไบต์ดังนั้นนี้จะต้องมีการปรับเปลี่ยนการดำเนินการอย่างใดอย่างหนึ่งต่อย้ำไบต์แทนการต่อค่าแฮ 32 บิต FNV ยังได้รับการออกแบบมาสำหรับความยาวของตัวแปรข้อมูลในขณะที่วิธีการที่เราใช้งานที่นี่นั้นมีค่าฟิลด์เท่ากัน ความคิดเห็นเกี่ยวกับคำตอบนี้ชี้ให้เห็นว่ารหัสที่นี่ใช้งานไม่ได้เช่นกัน (ในตัวอย่างกรณีทดสอบ) เป็นวิธีการเพิ่มเติมข้างต้น

// Note: Not quite FNV!
public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = (int) 2166136261;
        // Suitable nullity checks etc, of course :)
        hash = (hash * 16777619) ^ field1.GetHashCode();
        hash = (hash * 16777619) ^ field2.GetHashCode();
        hash = (hash * 16777619) ^ field3.GetHashCode();
        return hash;
    }
}

โปรดทราบว่าสิ่งหนึ่งที่ควรระวังก็คือในอุดมคติคุณควรป้องกันสถานะที่มีความอ่อนไหวต่อความเท่าเทียมกันของคุณ

ตามเอกสาร :

คุณสามารถแทนที่ GetHashCode สำหรับประเภทการอ้างอิงที่ไม่เปลี่ยนรูป โดยทั่วไปสำหรับประเภทการอ้างอิงที่ไม่แน่นอนคุณควรแทนที่ GetHashCode เฉพาะเมื่อ:

  • คุณสามารถคำนวณรหัสแฮชจากฟิลด์ที่ไม่สามารถเปลี่ยนแปลงได้ หรือ
  • คุณสามารถมั่นใจได้ว่ารหัสแฮชของวัตถุที่ไม่แน่นอนไม่เปลี่ยนแปลงในขณะที่วัตถุนั้นมีอยู่ในคอลเลกชันที่ต้องอาศัยรหัสแฮช

8
อัลกอริทึมที่อธิบายไว้ในหนังสือที่คุณพูดถึงนั้นมีรายละเอียดเพิ่มเติมเล็กน้อย especailly อธิบายถึงสิ่งที่ต้องทำสำหรับชนิดข้อมูลต่างๆของฟิลด์ เช่น: สำหรับเขตข้อมูลที่มีประเภทการใช้งานที่ยาวนาน (int) (ฟิลด์ ^ f >>> 32) แทนที่จะเรียกใช้ GetHashcode มีความยาว GetHashCodes ดำเนินการในลักษณะนั้นหรือไม่?
bitbonk

13
ใช่ Int64.GetHashCode ทำสิ่งนั้นอย่างแน่นอน ในชวานั้นคงต้องมีมวยแน่นอน นั่นทำให้ฉันนึกถึงเวลาที่จะเพิ่มลิงค์ไปยังหนังสือ ...
Jon Skeet

77
23 ไม่มีทางเลือกที่ดีเนื่องจาก (ณ . net 3.5 SP1) Dictionary<TKey,TValue>ถือว่าการกระจายที่ดี modulo บางช่วงเวลา และ 23 ก็เป็นหนึ่งในนั้น ดังนั้นหากคุณมีพจนานุกรมที่มีความจุ 23 เท่านั้นผลงานล่าสุดที่มีผลGetHashCodeต่อแฮชโค้ดแบบผสม ดังนั้นฉันควรใช้ 29 แทน 23
CodesInChaos

23
@CodeInChaos: เฉพาะผลงานล่าสุดเท่านั้นที่มีผลต่อที่เก็บข้อมูล - ดังนั้นอาจเป็นไปได้ว่าในที่สุดที่เลวร้ายที่สุดนั้นต้องดูรายการทั้ง 23รายการในพจนานุกรม มันจะยังคงตรวจสอบรหัสแฮชจริงของแต่ละรายการซึ่งจะมีราคาถูก หากคุณมีพจนานุกรมที่มีขนาดเล็กมันไม่น่าเป็นเรื่องสำคัญ
Jon Skeet

20
@Vajda: ฉันมักจะใช้ 0 เป็นรหัสแฮชที่มีประสิทธิภาพnullซึ่งไม่เหมือนกับเขตข้อมูลที่ละเว้น
Jon Skeet

431

ประเภทไม่ระบุชื่อ

Microsoft มีตัวสร้าง HashCode ทั่วไปที่ดีอยู่แล้ว: เพียงคัดลอกค่าคุณสมบัติ / ฟิลด์ของคุณไปเป็นประเภทที่ไม่ระบุชื่อและแฮชมัน:

new { PropA, PropB, PropC, PropD }.GetHashCode();

สิ่งนี้จะทำงานกับคุณสมบัติจำนวนเท่าใดก็ได้ มันไม่ได้ใช้มวย มันใช้อัลกอริธึมที่นำมาใช้ในกรอบงานสำหรับประเภทที่ไม่ระบุตัวตน

ValueTuple - อัปเดตสำหรับ C # 7

ในฐานะ @cactuaroid ที่กล่าวถึงในความคิดเห็นสามารถใช้ tuple ค่าได้ วิธีนี้ช่วยประหยัดการกดแป้นไม่กี่ครั้งและที่สำคัญดำเนินการอย่างหมดจดบนสแต็ก (ไม่มีขยะ):

(PropA, PropB, PropC, PropD).GetHashCode();

(หมายเหตุ: เทคนิคดั้งเดิมที่ใช้ชนิดไม่ระบุชื่อดูเหมือนว่าจะสร้างวัตถุบนฮีปเช่นขยะเนื่องจากมีการใช้ประเภทนิรนามเป็นคลาสถึงแม้ว่าสิ่งนี้อาจได้รับการปรับให้เหมาะสมโดยคอมไพเลอร์มันน่าสนใจที่จะสร้างมาตรฐานตัวเลือกเหล่านี้ ตัวเลือก tuple ควรเหนือกว่า)


85
ใช่GetHashCodeการใช้งานแบบไม่ระบุชื่อนั้นมีประสิทธิภาพมาก (BTW เหมือนกันกับคำตอบของ Jon Skeet) แต่ปัญหาเดียวของการแก้ปัญหานี้คือคุณสร้างอินสแตนซ์ใหม่ได้ทุกGetHashCodeเมื่อ มันสามารถเป็นบิตค่าใช้จ่าย-ish โดยเฉพาะอย่างยิ่งในกรณีของการเข้าถึงอย่างเข้มข้นคอลเลกชันแฮชใหญ่ ...
digEmAll

5
@digEmAll จุดดีฉันไม่ได้คิดเกี่ยวกับค่าใช้จ่ายในการสร้างวัตถุใหม่ คำตอบของ Jon Skeet นั้นมีประสิทธิภาพมากที่สุดและไม่ใช้มวย (@Kumba เพื่อแก้ปัญหาไม่ถูกตรวจสอบใน VB, เพียงแค่ใช้ Int64 (ยาว) และตัดมันหลังจากการคำนวณ.)
ริกรัก

42
สามารถพูดได้new { PropA, PropB, PropC, PropD }.GetHashCode()เช่นกัน
sehe

17
VB.NET ต้องใช้คีย์ในการสร้างประเภทที่ไม่ระบุชื่อ: New With {Key PropA}.GetHashCode()มิฉะนั้น GetHashCode จะไม่ส่งคืนรหัสแฮชโค้ดเดียวกันสำหรับวัตถุต่าง ๆ ที่มีคุณสมบัติ 'การระบุ' ที่เหมือนกัน
David Osborne

4
@ Keith ในกรณีนั้นฉันจะพิจารณาการบันทึก IEnumerable เป็นค่ารายการที่ไหนสักแห่งแทนการแจกแจงทุกครั้งที่คำนวณ hashcode การคำนวณ ToList ในแต่ละครั้งภายใน GetHashCode อาจส่งผลกระทบต่อประสิทธิภาพการทำงานในหลาย ๆ สถานการณ์
Rick Love

105

นี่คือผู้ช่วยแฮชโค้ดของฉัน
ข้อดีคือมันใช้อาร์กิวเมนต์ประเภททั่วไปและดังนั้นจะไม่ทำให้เกิดการชกมวย:

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }

นอกจากนี้ยังมีวิธีการขยายเพื่อให้อินเทอร์เฟซที่คล่องแคล่วดังนั้นคุณสามารถใช้สิ่งนี้:

public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}

หรือเช่นนี้

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}

5
ไม่จำเป็นต้องT[]แยกกันเพราะมันมีอยู่แล้วIEnumerable<T>
nawfal

5
คุณสามารถ refactor วิธีการเหล่านั้นและ จำกัด ตรรกะหลักในหนึ่งฟังก์ชัน
nawfal

12
อนึ่ง 31 เป็นกะและลบบน CPU ซึ่งเร็วมาก
Chui Tey

4
@nightcoder คุณสามารถใช้พารามิเตอร์
ตอบของ

6
@ChuiTey นี่คือสิ่งที่Mersenne Primesทุกคนมีเหมือนกัน
Pharap

63

ฉันมีคลาส Hashing ในไลบรารี Helper ที่ฉันใช้เพื่อจุดประสงค์นี้

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}

จากนั้นคุณสามารถใช้เป็น:

public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

ฉันไม่ได้ประเมินประสิทธิภาพของมันดังนั้นจึงยินดีรับข้อเสนอแนะ


26
มันจะทำให้เกิดมวยถ้าฟิลด์เป็นประเภทค่า
nightcoder

5
"สามารถเพิ่มในภายหลังโดยการจับ OverflowException ว่า" จุดรวมของคือการหลีกเลี่ยงข้อยกเว้นในล้นซึ่งเป็นที่ต้องการบนunchecked GetHashCodeดังนั้นจึงไม่ถูกต้องหากค่าเกินintและไม่เจ็บเลย
Tim Schmelter

1
ปัญหาหนึ่งของอัลกอริทึมนี้คืออาร์เรย์ใด ๆ ที่มีค่า Null จะส่งคืน 0 เสมอโดยไม่คำนึงถึงความยาว
Nathan Adams

2
วิธีการช่วยเหลือนี้ยังจัดสรรวัตถุใหม่ []
James Newton-King

1
@NathanAdams กล่าวถึงความจริงที่nullถูกข้ามไปทั้งหมดอาจให้ผลลัพธ์ที่ไม่คาดคิดแก่คุณ แทนการกระโดดข้ามพวกเขาคุณก็ควรจะใช้บางส่วนค่าคงที่แทนinput[i].GetHashCode()เมื่อinput[i]เป็นโมฆะ
David Schwartz

58

นี่คือระดับผู้ช่วยของฉันโดยใช้การดำเนินงานของจอนสกีต

public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

การใช้งาน:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

หากคุณต้องการหลีกเลี่ยงการเขียนวิธีการขยายสำหรับ System.Int32:

public readonly struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}

มันยังคงหลีกเลี่ยงการจัดสรรฮีปใด ๆ และใช้วิธีเดียวกันทุกประการ:

public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)     
        .Hash(_field3);
}

แก้ไข (พฤษภาคม 2018): EqualityComparer<T>.Defaultgetter ตอนนี้เป็น JIT ภายใน - คำขอดึงถูกกล่าวถึงโดยสตีเฟ่น Toub ในโพสต์บล็อกนี้


1
ฉันจะเปลี่ยนสายกับผู้ประกอบการระดับอุดมศึกษาเป็น:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Bill Barry

ฉันเชื่อว่าผู้ประกอบการที่ประกอบไปด้วยobj != nullจะรวบรวมboxคำสั่งที่จะจัดสรรหน่วยความจำหากTเป็นประเภทค่า แต่คุณสามารถใช้obj.Equals(null)ซึ่งจะรวบรวมการโทรเสมือนของEqualsวิธีการ
Martin Liversage

this.hashCode != hเพราะ มันจะไม่ส่งกลับค่าเดียวกัน
ŞafakGür

ขออภัยจัดการเพื่อลบความคิดเห็นของฉันแทนการแก้ไข มันจะมีประโยชน์มากขึ้นในการสร้างโครงสร้างใหม่แล้วเปลี่ยน hashCode เป็นแบบอ่านอย่างเดียวและทำ: "ไม่ถูกตรวจสอบ {this.hashCode ^ = h * 397;} คืนสิ่งนี้;" ตัวอย่างเช่น?
Erik Karlsson

ความไม่สามารถเปลี่ยนแปลงได้มีประโยชน์อย่างไร ( เหตุใดจึงเปลี่ยนแปลงโครงสร้างที่ชั่วร้ายได้ ) เกี่ยวกับประสิทธิภาพสิ่งที่ฉันทำค่อนข้างถูกเพราะไม่ได้จัดสรรพื้นที่ในกอง
ŞafakGür

30

. NET Standard 2.1 ขึ้นไป

หากคุณกำลังใช้ .NET มาตรฐาน 2.1 หรือสูงกว่าคุณสามารถใช้System.HashCode struct มีสองวิธีในการใช้งาน:

HashCode.Combine

Combineวิธีสามารถนำมาใช้ในการสร้างรหัสกัญชาให้ถึงแปดวัตถุ

public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);

HashCode.Add

Addวิธีช่วยให้คุณสามารถจัดการกับคอลเลกชัน:

public override int GetHashCode()
{
    var hashCode = new HashCode();
    hashCode.Add(this.object1);
    foreach (var item in this.collection)
    {
        hashCode.Add(item);
    }
    return hashCode.ToHashCode();
}

GetHashCode ทำง่าย

คุณสามารถอ่านโพสต์บล็อกแบบเต็ม ' GetHashCode Made Easy ' สำหรับรายละเอียดและความคิดเห็นเพิ่มเติม

ตัวอย่างการใช้งาน

public class SuperHero
{
    public int Age { get; set; }
    public string Name { get; set; }
    public List<string> Powers { get; set; }

    public override int GetHashCode() =>
        HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers);
}

การดำเนินงาน

public struct HashCode : IEquatable<HashCode>
{
    private const int EmptyCollectionPrimeNumber = 19;
    private readonly int value;

    private HashCode(int value) => this.value = value;

    public static implicit operator int(HashCode hashCode) => hashCode.value;

    public static bool operator ==(HashCode left, HashCode right) => left.Equals(right);

    public static bool operator !=(HashCode left, HashCode right) => !(left == right);

    public static HashCode Of<T>(T item) => new HashCode(GetHashCode(item));

    public static HashCode OfEach<T>(IEnumerable<T> items) =>
        items == null ? new HashCode(0) : new HashCode(GetHashCode(items, 0));

    public HashCode And<T>(T item) => 
        new HashCode(CombineHashCodes(this.value, GetHashCode(item)));

    public HashCode AndEach<T>(IEnumerable<T> items)
    {
        if (items == null)
        {
            return new HashCode(this.value);
        }

        return new HashCode(GetHashCode(items, this.value));
    }

    public bool Equals(HashCode other) => this.value.Equals(other.value);

    public override bool Equals(object obj)
    {
        if (obj is HashCode)
        {
            return this.Equals((HashCode)obj);
        }

        return false;
    }

    public override int GetHashCode() => this.value.GetHashCode();

    private static int CombineHashCodes(int h1, int h2)
    {
        unchecked
        {
            // Code copied from System.Tuple a good way to combine hashes.
            return ((h1 << 5) + h1) ^ h2;
        }
    }

    private static int GetHashCode<T>(T item) => item?.GetHashCode() ?? 0;

    private static int GetHashCode<T>(IEnumerable<T> items, int startHashCode)
    {
        var temp = startHashCode;

        var enumerator = items.GetEnumerator();
        if (enumerator.MoveNext())
        {
            temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));

            while (enumerator.MoveNext())
            {
                temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
            }
        }
        else
        {
            temp = CombineHashCodes(temp, EmptyCollectionPrimeNumber);
        }

        return temp;
    }
}

อัลกอริทึมที่ดีคืออะไร

ความเร็ว

อัลกอริทึมที่คำนวณรหัสแฮชต้องรวดเร็ว อัลกอริทึมแบบง่ายมักจะเป็นวิธีที่เร็วกว่า

กำหนด

อัลกอริทึมการแปลงแป้นพิมพ์ต้องกำหนดค่าได้เช่นกันเนื่องจากอินพุตเดียวกันนั้นจะต้องสร้างเอาต์พุตเดียวกัน

ลดการชน

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

ฟังก์ชันแฮชที่ดีควรแมปอินพุตที่คาดหวังอย่างสม่ำเสมอเท่าที่จะเป็นไปได้ในช่วงเอาต์พุต มันควรจะมีความสม่ำเสมอ

ป้องกันของ DoS

ใน. NET Core ทุกครั้งที่คุณเริ่มแอปพลิเคชันใหม่คุณจะได้รับรหัสแฮชที่แตกต่างกัน นี่คือคุณลักษณะด้านความปลอดภัยเพื่อป้องกันการโจมตีแบบปฏิเสธการให้บริการ (DoS) สำหรับ. NET Framework คุณควรเปิดใช้งานคุณสมบัตินี้โดยเพิ่มไฟล์ App.config ต่อไปนี้:

<?xml version ="1.0"?>  
<configuration>  
   <runtime>  
      <UseRandomizedStringHashAlgorithm enabled="1" />  
   </runtime>  
</configuration>

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

อ่านข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ที่นี่

ปลอดภัยด้วยการเข้ารหัส?

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

  • เป็นไปไม่ได้ที่จะสร้างข้อความที่ให้ค่าแฮชที่กำหนด
  • ไม่สามารถค้นหาข้อความที่แตกต่างกันสองข้อความที่มีค่าแฮชเดียวกัน
  • การเปลี่ยนแปลงเล็กน้อยกับข้อความควรเปลี่ยนค่าแฮชอย่างกว้างขวางเพื่อให้ค่าแฮชใหม่ปรากฏขึ้นโดยไม่เกี่ยวข้องกับค่าแฮชเก่า (เอฟเฟกต์หิมะถล่ม)

29

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

การยกของหนักควรเป็นส่วนหนึ่งของวิธี Equals () แฮควรเป็นการดำเนินการที่ถูกมากเพื่อเปิดใช้งานการโทรเท่ากับ () ในรายการน้อยที่สุด

และเป็นหนึ่งในปลายสุดท้าย: ไม่ต้องพึ่งพา GetHashCode () เป็นมีเสถียรภาพมากกว่า aplication หลายวิ่ง ประเภท. Net หลายประเภทไม่รับประกันรหัสแฮชที่จะคงเดิมหลังจากรีสตาร์ทดังนั้นคุณควรใช้ค่าของ GetHashCode () สำหรับโครงสร้างข้อมูลหน่วยความจำเท่านั้น


10
"ในกรณีส่วนใหญ่ที่ Equals () เปรียบเทียบหลายฟิลด์มันไม่สำคัญว่า GetHash ของคุณจะแฮชบนหนึ่งหรือหลาย ๆ อัน" นี่คือคำแนะนำที่เป็นอันตรายเนื่องจากสำหรับวัตถุที่แตกต่างกันในฟิลด์ที่ไม่ได้ถูกแฮชคุณจะได้รับการชนแฮช หากสิ่งนี้เกิดขึ้นบ่อยครั้งประสิทธิภาพของคอลเลกชันที่ใช้แฮช (HashMap, HashSet เป็นต้น) จะลดประสิทธิภาพลง (มากถึง O (n) ในกรณีที่แย่ที่สุด)
sleske

10
สิ่งนี้เกิดขึ้นจริงใน Java: ในรุ่นแรก ๆ ของ JDK String.hashCode () พิจารณาเฉพาะจุดเริ่มต้นของสตริงเท่านั้น สิ่งนี้นำไปสู่ปัญหาด้านประสิทธิภาพหากคุณใช้ Strings เป็นกุญแจใน HashMaps ซึ่งแตกต่างกันในตอนท้ายเท่านั้น (ซึ่งเป็นเรื่องปกติเช่นสำหรับ URL) ดังนั้นอัลกอริทึมจึงถูกเปลี่ยน (ใน JDK 1.2 หรือ 1.3 ฉันเชื่อว่า)
sleske

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

ฉันไม่คิดว่าจะมีปัญหาในการGetHashCodeทำการจัดสรรหน่วยความจำโดยที่ทำได้เพียงครั้งแรกเท่านั้นที่ใช้งาน (ด้วยการเรียกใช้งานครั้งต่อไปจะแสดงผลลัพธ์ที่แคชไว้) สิ่งสำคัญไม่ใช่ว่าเราควรพยายามอย่างสุดความสามารถเพื่อหลีกเลี่ยงการชน แต่ควรหลีกเลี่ยงการชนกันอย่างเป็นระบบ หากประเภทมีสองintฟิลด์oldXและnewXบ่อยครั้งที่แตกต่างกันหนึ่งค่าแฮชของoldX^newXจะกำหนด 90% ของค่าแฮชของเร็กคอร์ดดังกล่าวเป็น 1, 2, 4 หรือ 8 การใช้oldX+newX[ไม่ จำกัด ทางคณิตศาสตร์] อาจทำให้เกิดการชนมากขึ้น ...
supercat

1
... มากกว่าฟังก์ชั่นที่ซับซ้อนกว่านี้ แต่ชุดของ 1,000,000 สิ่งที่มี 500,000 ค่าแฮชที่แตกต่างกันจะดีมากถ้าค่าแฮชแต่ละรายการมีสองสิ่งที่เชื่อมโยงกันและแย่มากถ้าหนึ่งค่าแฮชมี 500,001 รายการและอื่น ๆ
supercat

23

จนกระทั่งเมื่อไม่นานมานี้คำตอบของฉันจะใกล้เคียงกับ Jon Skeet มากที่นี่ อย่างไรก็ตามเมื่อเร็ว ๆ นี้ฉันเริ่มโครงการที่ใช้ตารางแฮชแบบ power-of-two นั่นคือตารางแฮชที่ขนาดของตารางภายในคือ 8, 16, 32 และอื่น ๆ มีเหตุผลที่ดีสำหรับการเลือกขนาดหมายเลขเฉพาะ แต่มี ข้อดีบางประการสำหรับขนาดกำลังของสองด้วย

และมันก็ดูดมาก ดังนั้นหลังจากการทดลองและการวิจัยเล็กน้อยฉันเริ่ม hash ของฉันใหม่อีกครั้งด้วยสิ่งต่อไปนี้:

public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}

แล้วตารางแฮชพลังของฉันไม่ได้ดูดอีกต่อไป

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

การผสมแฮชโค้ดอีกครั้งไม่สามารถปรับปรุงแฮชโค้ดที่ยอดเยี่ยมได้เนื่องจากเอฟเฟกต์ที่เป็นไปได้เพียงอย่างเดียวคือเราจะเพิ่มการชนกันเล็กน้อย

การผสมรหัสแฮชใหม่ไม่สามารถปรับปรุงรหัสแฮชที่แย่มากได้เนื่องจากเอฟเฟกต์ที่เป็นไปได้เพียงอย่างเดียวคือเราเปลี่ยนเช่นการชนกันจำนวนมากในค่า 53 เป็นจำนวนมาก 18,3487,291

การผสมรหัสแฮชอีกครั้งจะสามารถปรับปรุงรหัสแฮชที่ทำได้ดีพอสมควรในการหลีกเลี่ยงการชนที่แน่นอนตลอดช่วง (2 32ค่าที่เป็นไปได้) แต่การหลีกเลี่ยงการชนเมื่อโมดูโลลดลงสำหรับการใช้งานจริงในตารางแฮช ในขณะที่โมดูโล่แบบเรียบง่ายของตาราง power-of-two ทำให้สิ่งนี้ชัดเจนยิ่งขึ้น แต่ก็มีผลกระทบเชิงลบกับตารางหมายเลขเฉพาะที่พบได้ทั่วไปซึ่งไม่ชัดเจนเท่าที่ควร แต่ประโยชน์ยังคงอยู่ที่นั่น)

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

และก็เป็นการรบกวนว่าการstring.GetHashCode()ใช้งานใน. NET (หรือการศึกษาที่นี่ ) สามารถปรับปรุงได้ด้วยวิธีนี้ (ตามลำดับของการทดสอบที่รันเร็วกว่าประมาณ 20-30 เท่าเนื่องจากการชนน้อยลง) และรบกวนรหัสแฮชของตัวเองมากขึ้น สามารถปรับปรุงได้ (ยิ่งไปกว่านั้น)

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

ดังนั้นฉันจึงวางโครงการนั้นไว้ด้านหนึ่ง (มันเป็นโครงการสำหรับสัตว์เลี้ยงอยู่แล้ว) และเริ่มดูว่าจะสร้างรหัสแฮชที่ดีและกระจายได้ดีใน. NET อย่างรวดเร็ว

ในที่สุดฉันก็ตัดสินใจย้ายSpookyHashไปยัง. NET อันที่จริงรหัสข้างต้นเป็นเวอร์ชั่นที่รวดเร็วในการใช้ SpookyHash เพื่อสร้างเอาต์พุต 32- บิตจากอินพุต 32- บิต

ตอนนี้ SpookyHash ไม่ใช่โค้ดที่ดีในการจดจำอย่างรวดเร็ว พอร์ทของฉันมันน้อยกว่านี้มากเพราะฉันอินไลน์มันบ่อยมากเพื่อความเร็วที่ดีขึ้น * แต่นั่นคือสิ่งที่ใช้รหัสซ้ำ

จากนั้นฉันก็วางโครงการนั้นไว้ที่ด้านหนึ่งเพราะเช่นเดียวกับที่โครงการต้นฉบับสร้างคำถามว่าจะสร้างรหัสแฮชที่ดีขึ้นได้อย่างไรดังนั้นโครงการนั้นจึงสร้างคำถามว่าจะผลิต. NET memcpy ที่ดีขึ้นได้อย่างไร

จากนั้นฉันกลับมาอีกครั้งและผลิตโอเวอร์โหลดจำนวนมากเพื่อป้อนฟีเจอร์ดั้งเดิมทั้งหมด (ยกเว้นdecimal†) ลงในแฮชโค้ดได้อย่างง่ายดาย

มันเร็วซึ่ง Bob Jenkins สมควรได้รับเครดิตส่วนใหญ่เพราะรหัสดั้งเดิมของเขาที่ฉันส่งไปนั้นยังเร็วกว่าโดยเฉพาะในเครื่อง 64 บิตซึ่งอัลกอริธึมได้รับการปรับให้เหมาะสมสำหรับ‡

รหัสเต็มสามารถดูได้ที่https://bitbucket.org/JonHanna/spookilysharp/srcแต่พิจารณาว่ารหัสข้างต้นเป็นเวอร์ชั่นที่ง่ายขึ้น

อย่างไรก็ตามเนื่องจากตอนนี้เขียนเสร็จแล้วจึงสามารถใช้งานได้ง่ายขึ้น:

public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

นอกจากนี้ยังใช้ค่าเมล็ดดังนั้นหากคุณต้องการจัดการกับอินพุตที่ไม่น่าเชื่อถือและต้องการป้องกันการโจมตี Hash DoS คุณสามารถตั้งค่าเมล็ดตามสถานะการออนไลน์หรือคล้ายกันและทำให้ผลลัพธ์ที่ผู้โจมตีคาดเดาไม่ได้:

private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

* ความประหลาดใจที่ยิ่งใหญ่ในเรื่องนี้คือวิธีการหมุนด้วยมือที่คืน(x << n) | (x >> -n)สิ่งที่ได้รับการปรับปรุงให้ดีขึ้น ฉันจะมั่นใจได้ว่ากระวนกระวายใจจะมีการ inline สำหรับฉัน แต่โปรไฟล์แสดงให้เห็นเป็นอย่างอื่น

decimalไม่ได้มาจากเปอร์สเปคทีฟของ. NET แม้ว่าจะมาจาก C # ปัญหาที่เกิดขึ้นกับตัวมันคือGetHashCode()ความแม่นยำของตัวมันเองEquals()นั้นสำคัญมาก ทั้งสองเป็นตัวเลือกที่ถูกต้อง แต่ไม่ผสมกันเช่นนั้น ในการใช้เวอร์ชันของคุณเองคุณต้องเลือกที่จะทำอย่างใดอย่างหนึ่ง แต่อย่างอื่น แต่ฉันไม่รู้ว่าคุณต้องการอะไร

‡โดยวิธีการเปรียบเทียบ หากใช้กับสตริง SpookyHash บน 64 บิตจะเร็วกว่าstring.GetHashCode()32 บิตซึ่งเร็วกว่าstring.GetHashCode()64 บิตเล็กน้อยซึ่งเร็วกว่า SpookyHash ใน 32 บิตแม้ว่าจะเร็วพอที่จะเป็นตัวเลือกที่สมเหตุสมผลก็ตาม


เมื่อรวมค่าแฮชเป็นหนึ่งในหลายผมมักจะใช้longค่าสำหรับผลกลางแล้ว Munge intผลสุดท้ายลงไป ดูเหมือนว่าเป็นความคิดที่ดีหรือไม่? ความกังวลของฉันคือผู้ใช้เช่น hash = (hash * 31) + nextField จากนั้นค่าการจับคู่คู่จะมีผลกับ 27 บิตส่วนบนของแฮช การให้การคำนวณขยายไปถึงlongสิ่งที่ห่อหุ้มด้วยจะช่วยลดอันตรายนั้นได้
supercat

@supercat มันขึ้นอยู่กับการกระจายของ munging สุดท้ายของคุณ SpookilySharp ไลบรารี่จะทำให้แน่ใจว่าการกระจายนั้นดีเลิศ (เพราะมันไม่จำเป็นต้องสร้างวัตถุ) โดยการส่งพอยน์เตอร์ไปยังประเภทที่สามารถ blittable หรือผ่านหนึ่งใน enumerables ที่มันจัดการโดยตรง แต่ถ้าคุณยังไม่มี blittable ข้อมูลหรือการแจงนับที่เหมาะสมจากนั้นเรียก.Update()ด้วยค่าหลายค่าตามคำตอบข้างต้นจะทำเคล็ดลับ
Jon Hanna

@ จอนฮันนาคุณต้องการที่จะมีความแม่นยำมากขึ้นกับพฤติกรรมที่เป็นปัญหาที่คุณพบหรือไม่? ฉันพยายามที่จะใช้ห้องสมุดที่ทำให้การใช้งานค่าวัตถุเป็นเรื่องเล็กน้อย ( ValueUtils ) และฉันชอบชุดทดสอบที่แสดงให้เห็นถึงการเข้ากันไม่ได้ของแฮชที่ไม่ดีในแฮชเทเบิ้ลที่มีกำลังสอง
Eamon Nerbonne

@EamonNerbonne ฉันไม่ได้มีอะไรที่แม่นยำมากไปกว่า "เวลาโดยรวมก็ช้าลง" เมื่อฉันเพิ่มในการแก้ไขความจริงที่ว่าฉันใช้การเปิดที่อยู่อาจมีความสำคัญมากกว่าปัจจัยอำนาจของสอง ฉันวางแผนที่จะทำกรณีทดสอบบางอย่างในโครงการเฉพาะที่ฉันจะเปรียบเทียบวิธีการที่แตกต่างกันสองสามข้อดังนั้นฉันอาจมีคำตอบที่ดีกว่าสำหรับคุณหลังจากนั้นถึงแม้ว่ามันจะไม่ใช่ลำดับความสำคัญสูง ดังนั้นฉันจะไปหามันเมื่อฉันไปถึง ... )
Jon Hanna

@ จอนฮัน: ใช่ฉันรู้ว่าตารางโครงการส่วนบุคคลไป - โชคดี! ไม่ว่าในกรณีใดฉันเห็นว่าฉันไม่ได้ใช้คำพูดที่แสดงความคิดเห็นได้ดีฉันหมายถึงขอข้อมูลที่มีปัญหาและไม่จำเป็นต้องมีรายละเอียดของปัญหาที่เกิดขึ้น ฉันชอบที่จะใช้ชุดทดสอบ (หรือเป็นแรงบันดาลใจสำหรับชุดทดสอบ) ในกรณีใด ๆ - ขอให้โชคดีกับโครงการสัตว์เลี้ยงของคุณ :-)
Eamon Nerbonne

13

นี่เป็นสิ่งที่ดี:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

และนี่คือวิธีการใช้งาน:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}

1
กุญแจมีการกำหนดอย่างไร? GetHashCode () ไม่รับพารามิเตอร์ใด ๆ ดังนั้นจึงจำเป็นต้องเรียกใช้สิ่งนี้ด้วยสองปุ่มที่ต้องได้รับการพิจารณา ขออภัยไม่มีคำอธิบายเพิ่มเติมนี้ดูฉลาด แต่ไม่ดี
Michael Stum

และทำไมคุณถึงต้องการโอเวอร์โหลดทั่วไป? ประเภทไม่สำคัญ (และไม่ได้ใช้ในรหัสของคุณ) เนื่องจากวัตถุทั้งหมดมีGetHashCode()วิธีการดังนั้นคุณจึงสามารถใช้วิธีนี้กับparamsพารามิเตอร์อาร์เรย์ หรือฉันกำลังพลาดบางสิ่งที่นี่
gehho

4
เมื่อคุณใช้วัตถุแทนข้อมูลทั่วไปคุณจะได้รับการจัดสรรมวยและหน่วยความจำซึ่งคุณไม่ต้องการใน GetHashCode ดังนั้นยาชื่อสามัญเป็นวิธีที่จะไป
CodesInChaos

1
ขั้นตอนการกะ / xor ต่อท้าย ( h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);มีรหัส: พวกเขาไม่ได้ขึ้นอยู่กับการป้อนข้อมูลใด ๆ และดูซ้ำซ้อนกับฉันอย่างมาก
sehe

1
@ Magnus ใช่แล้วฉันจะลบความคิดเห็นเดิมของฉัน ขอให้ทราบเพียงเล็กน้อยว่านี่อาจไม่เร็วเท่ากับโซลูชันอื่น ๆ ที่นี่ แต่อย่างที่คุณพูดไม่ควรสำคัญ การกระจายนั้นยอดเยี่ยมดีกว่าโซลูชันส่วนใหญ่ที่นี่ดังนั้น +1 จากฉัน! :)
nawfal

11

ตั้งแต่https://github.com/dotnet/coreclr/pull/14863มีวิธีใหม่ในการสร้างรหัสแฮชที่ง่ายสุด ๆ ! แค่เขียน

public override int GetHashCode()
    => HashCode.Combine(field1, field2, field3);

สิ่งนี้จะสร้างรหัสแฮชที่มีคุณภาพโดยที่คุณไม่ต้องกังวลกับรายละเอียดการใช้งาน


ดูเหมือนว่าจะเป็นการเพิ่มเติมที่หวาน ... มีวิธีใดบ้างที่จะรู้ว่า. NET Core รุ่นใดที่จะจัดส่ง?
Dan J

1
@DanJ ช่างเป็นเรื่องบังเอิญที่มีความสุขการHashCodeเปลี่ยนแปลงของ corefx ได้ถูกรวมเข้าด้วยกันเพียงไม่กี่ชั่วโมงก่อนที่ความคิดเห็นของคุณจะเป็นประเภทที่มีกำหนดจะจัดส่งใน. NET Core 2.1
James Ko

นั่นเป็นสิ่งที่ยอดเยี่ยม - และเป็นช่วงเวลาที่ค่อนข้างจะพลิกผัน upvoted :)
Dan J

@DanJ ข่าวที่ดียิ่งขึ้น - ตอนนี้ควรมีวางจำหน่ายในรุ่นต่อไปของ CoreFX ที่โฮสต์ในฟีด dotnet-core MyGet
James Ko

หวาน - ที่ไม่ช่วยฉันในการทำงานเนื่องจากเราไม่ได้ค่อนข้างที่มีเลือดออกที่ทันสมัย แต่ดีที่จะรู้ ไชโย!
ด่านเจ

9

นี่คือการใช้อัลกอริทึมอื่นที่โพสต์ไว้ด้านบนโดย Jon Skeetแต่ไม่รวมถึงการจัดสรรหรือการดำเนินการชกมวย:

public static class Hash
{
    public const int Base = 17;

    public static int HashObject(this int hash, object obj)
    {
        unchecked { return hash * 23 + (obj == null ? 0 : obj.GetHashCode()); }
    }

    public static int HashValue<T>(this int hash, T value)
        where T : struct
    {
        unchecked { return hash * 23 + value.GetHashCode(); }
    }
}

การใช้งาน:

public class MyType<T>
{
    public string Name { get; set; }

    public string Description { get; set; }

    public int Value { get; set; }

    public IEnumerable<T> Children { get; set; }

    public override int GetHashCode()
    {
        return Hash.Base
            .HashObject(this.Name)
            .HashObject(this.Description)
            .HashValue(this.Value)
            .HashObject(this.Children);
    }
}

คอมไพเลอร์จะตรวจสอบให้แน่ใจHashValueว่าไม่ได้ถูกเรียกพร้อมกับคลาสเนื่องจากข้อ จำกัด ประเภททั่วไป แต่ไม่มีคอมไพเลอร์สนับสนุนHashObjectตั้งแต่เพิ่มอาร์กิวเมนต์ทั่วไปยังเพิ่มการดำเนินการมวย


8

นี่เป็นวิธีง่าย ๆ ของฉัน ฉันใช้รูปแบบการสร้างแบบคลาสสิคสำหรับสิ่งนี้ มันเป็น typesafe (ไม่มีมวย / unboxing) และยังเข้ากันได้กับ. NET 2.0 (ไม่มีวิธีการขยาย ฯลฯ )

มันถูกใช้อย่างนี้:

public override int GetHashCode()
{
    HashBuilder b = new HashBuilder();
    b.AddItems(this.member1, this.member2, this.member3);
    return b.Result;
} 

และนี่คือคลาสผู้สร้างแบบเฉียบพลัน:

internal class HashBuilder
{
    private const int Prime1 = 17;
    private const int Prime2 = 23;
    private int result = Prime1;

    public HashBuilder()
    {
    }

    public HashBuilder(int startHash)
    {
        this.result = startHash;
    }

    public int Result
    {
        get
        {
            return this.result;
        }
    }

    public void AddItem<T>(T item)
    {
        unchecked
        {
            this.result = this.result * Prime2 + item.GetHashCode();
        }
    }

    public void AddItems<T1, T2>(T1 item1, T2 item2)
    {
        this.AddItem(item1);
        this.AddItem(item2);
    }

    public void AddItems<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
    }

    public void AddItems<T1, T2, T3, T4>(T1 item1, T2 item2, T3 item3, 
        T4 item4)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
    }

    public void AddItems<T1, T2, T3, T4, T5>(T1 item1, T2 item2, T3 item3, 
        T4 item4, T5 item5)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
        this.AddItem(item5);
    }        

    public void AddItems<T>(params T[] items)
    {
        foreach (T item in items)
        {
            this.AddItem(item);
        }
    }
}

คุณสามารถหลีกเลี่ยงการสร้างวัตถุภายในฟังก์ชั่น gethashcode ในคำตอบของ Mangus เพียงแค่เรียกใช้ฟังก์ชันแฮชแบบคงที่แช่ง (ใครจะสนใจเกี่ยวกับแฮชเริ่มต้น) นอกจากนี้คุณสามารถใช้AddItems<T>(params T[] items)วิธีการบ่อยในคลาสตัวช่วย (มากกว่าการโทรAddItem(T)แต่ละครั้ง)
nawfal

และคุณได้ประโยชน์อะไรthis.result * Prime2 * item.GetHashCode()เมื่อใช้บ่อย ๆ คือthis.result * Prime2 + item.GetHashCode()อะไร?
nawfal

ฉันไม่สามารถใช้AddItems<T>(params T[] items)มักจะเพราะมากขึ้นtypeof(T1) != typeof(T2)ฯลฯ
bitbonk

โอ้ใช่ฉันพลาดไป
nawfal

5

ผู้ใช้ReSharperสามารถสร้าง GetHashCode, Equals และอื่น ๆReSharper -> Edit -> Generate Code -> Equality Membersได้

// ReSharper's GetHashCode looks like this
public override int GetHashCode() {
    unchecked {
        int hashCode = Id;
        hashCode = (hashCode * 397) ^ IntMember;
        hashCode = (hashCode * 397) ^ OtherIntMember;
        hashCode = (hashCode * 397) ^ (RefMember != null ? RefMember.GetHashCode() : 0);
        // ...
        return hashCode;
    }
}

4

หากเรามีที่พักไม่เกิน 8 แห่ง (หวังว่า) นี่คืออีกทางเลือกหนึ่ง

ValueTuple เป็น struct และดูเหมือนจะมีของแข็ง GetHashCodeใช้งานที่

นั่นหมายความว่าเราสามารถทำได้:

// Yay, no allocations and no custom implementations!
public override int GetHashCode() => (this.PropA, this.PropB).GetHashCode();

ลองมาดูที่การดำเนินการปัจจุบัน .NET หลักของสำหรับ'sValueTupleGetHashCode

นี่คือจากValueTuple:

    internal static int CombineHashCodes(int h1, int h2)
    {
        return HashHelpers.Combine(HashHelpers.Combine(HashHelpers.RandomSeed, h1), h2);
    }

    internal static int CombineHashCodes(int h1, int h2, int h3)
    {
        return HashHelpers.Combine(CombineHashCodes(h1, h2), h3);
    }

และนี่คือจากHashHelper:

    public static readonly int RandomSeed = Guid.NewGuid().GetHashCode();

    public static int Combine(int h1, int h2)
    {
        unchecked
        {
            // RyuJIT optimizes this to use the ROL instruction
            // Related GitHub pull request: dotnet/coreclr#1830
            uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
            return ((int)rol5 + h1) ^ h2;
        }
    }

เป็นภาษาอังกฤษ:

  • หมุนไปทางซ้าย (shift shift) h1 5 ตำแหน่ง
  • เพิ่มผลลัพธ์และ h1 เข้าด้วยกัน
  • แฮคเกอร์ผลลัพธ์ด้วย h2
  • เริ่มต้นด้วยการดำเนินการข้างต้นใน {เมล็ดสุ่มแบบคงที่, h1}
  • สำหรับแต่ละรายการเพิ่มเติมให้ดำเนินการกับผลลัพธ์ก่อนหน้าและรายการถัดไป (เช่น h2)

เราน่าจะทราบเพิ่มเติมเกี่ยวกับคุณสมบัติของอัลกอริทึมแฮชโค้ด ROL-5 นี้

น่าเสียดายการชะลอตัวเพื่อValueTupleตัวเราเองGetHashCodeอาจไม่เร็วอย่างที่เราต้องการและคาดหวัง ความคิดเห็นในการสนทนาที่เกี่ยวข้องนี้แสดงให้เห็นว่าการโทรโดยตรงHashHelpers.Combineนั้นมีประสิทธิภาพมากกว่า อีกด้านหนึ่งคือภายในดังนั้นเราต้องคัดลอกรหัสเพื่อเสียสละสิ่งที่เราได้รับที่นี่ นอกจากนี้เราจะต้องรับผิดชอบในการจดจำก่อนCombineด้วยการสุ่มเมล็ด ฉันไม่รู้ว่าผลที่ตามมาคืออะไรถ้าเราข้ามขั้นตอนนั้นไป


สมมติว่าh1 >> 27เป็น 0 ถึงไม่สนใจมันh1 << 5เท่ากับจึงเป็นเช่นเดียวกับh1 * 32 h1 * 33 ^ h2ตามหน้านี้เรียกว่า "Modified Bernstein"
cactuaroid

3

งานส่วนใหญ่ของฉันเสร็จสิ้นด้วยการเชื่อมต่อฐานข้อมูลซึ่งหมายความว่าคลาสของฉันทั้งหมดมีตัวระบุเฉพาะจากฐานข้อมูล ฉันมักจะใช้ ID จากฐานข้อมูลเพื่อสร้าง hashcode

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}

ซึ่งหมายความว่าหากคุณมีวัตถุบุคคลและบัญชีและทั้งคู่มีและ ID = 1 พวกเขาจะมีรหัสแฮชเดียวกัน และนั่นก็ไม่เป็นไร
pero

15
จริงๆแล้วความคิดเห็นข้างต้นไม่ถูกต้อง จะมีความเป็นไปได้ที่จะเกิดการชนกันของรหัสแฮช (รหัสแฮชจะอยู่ที่ที่ฝากข้อมูลเท่านั้นไม่ใช่แต่ละวัตถุ) ดังนั้นการนำไปใช้ - สำหรับแฮชโค้ดที่มีวัตถุผสม - จะนำไปสู่การชนจำนวนมากซึ่งไม่เป็นที่พึงปรารถนา แต่จะดีมากถ้าคุณเคยมีวัตถุประเภทเดียวในแฮชเทเบิลของคุณ นอกจากนี้มันยังไม่กระจายเท่า ๆ กัน แต่ก็ไม่มีการติดตั้งพื้นฐานใน system.object ดังนั้นฉันจึงไม่ต้องกังวลกับมันมากเกินไป ...
piers7

2
รหัสแฮชสามารถเป็น ID ได้เนื่องจาก id เป็นจำนวนเต็ม ไม่จำเป็นต้องโทร GetHashCode ในจำนวนเต็มไม่ได้ (มันเป็นฟังก์ชั่นตัวตน)
Darrel ลี

2
@DarrelLee แต่โทโมะ _id ของเขาอาจเป็น Guid เป็นการฝึกเขียนโค้ดที่ดีที่ต้องทำ_id.GetHashCodeตามเจตนาที่ชัดเจน
nawfal

2
@ 1224 ขึ้นอยู่กับรูปแบบการใช้งานมันน่ากลัวสำหรับเหตุผลที่คุณให้ แต่มันก็เยี่ยมมาก หากคุณมีลำดับของตัวเลขดังกล่าวโดยไม่มีหลุมแสดงว่าคุณได้แฮชสมบูรณ์แบบดีกว่าอัลกอริธึมที่สามารถสร้างได้ หากคุณรู้ว่าเป็นกรณีที่คุณสามารถนับได้และข้ามการตรวจสอบความเท่าเทียมกัน
Jon Hanna

3

ค่อนข้างคล้ายกับวิธีแก้ปัญหา nightcoder ของนอกจากจะง่ายขึ้นที่จะเพิ่มเฉพาะช่วงเวลาถ้าคุณต้องการ

PS: นี่เป็นหนึ่งในช่วงเวลาที่คุณอ้วกเล็ก ๆ น้อย ๆ ในปากของคุณรู้ว่าสิ่งนี้สามารถ refactored เป็นวิธีการหนึ่งที่มีค่าเริ่มต้น 9 แต่มันจะช้าลงดังนั้นคุณเพียงแค่ปิดตาและพยายามที่จะลืมมัน

/// <summary>
/// Try not to look at the source code. It works. Just rely on it.
/// </summary>
public static class HashHelper
{
    private const int PrimeOne = 17;
    private const int PrimeTwo = 23;

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();
            hash = hash * PrimeTwo + arg10.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();

            return hash;
        }
    }
}

2
ไม่จัดการกับค่า null
JJS

1

ฉันพบปัญหากับการลอยและทศนิยมโดยใช้การดำเนินการที่เลือกไว้เป็นคำตอบข้างต้น

การทดสอบนี้ล้มเหลว (ลอย; แฮชเหมือนกันแม้ว่าฉันจะเปลี่ยน 2 ค่าเป็นลบ):

        var obj1 = new { A = 100m, B = 100m, C = 100m, D = 100m};
        var obj2 = new { A = 100m, B = 100m, C = -100m, D = -100m};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

แต่การทดสอบนี้ผ่าน (มี ints):

        var obj1 = new { A = 100m, B = 100m, C = 100, D = 100};
        var obj2 = new { A = 100m, B = 100m, C = -100, D = -100};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

ฉันเปลี่ยนการใช้งานของฉันเพื่อไม่ใช้ GetHashCode สำหรับประเภทดั้งเดิมและดูเหมือนว่าจะทำงานได้ดีขึ้น

    private static int InternalComputeHash(params object[] obj)
    {
        unchecked
        {
            var result = (int)SEED_VALUE_PRIME;
            for (uint i = 0; i < obj.Length; i++)
            {
                var currval = result;
                var nextval = DetermineNextValue(obj[i]);
                result = (result * MULTIPLIER_VALUE_PRIME) + nextval;

            }
            return result;
        }
    }



    private static int DetermineNextValue(object value)
    {
        unchecked
        {

                int hashCode;
                if (value is short
                    || value is int
                    || value is byte
                    || value is sbyte
                    || value is uint
                    || value is ushort
                    || value is ulong
                    || value is long
                    || value is float
                    || value is double
                    || value is decimal)
                {
                    return Convert.ToInt32(value);
                }
                else
                {
                    return value != null ? value.GetHashCode() : 0;
                }
        }
    }

1
ในกรณีที่คุณตั้งใจเป็นอย่างอื่นuncheckedไม่ได้ส่งผลกระทบต่อConvert.ToInt32: uint, long, float, doubleและdecimalสามารถล้นทั้งหมดที่นี่
Mark Hurd

1

Microsoft เป็นผู้นำในการ hashing หลายวิธี ...

//for classes that contain a single int value
return this.value;

//for classes that contain multiple int value
return x ^ y;

//for classes that contain single number bigger than int    
return ((int)value ^ (int)(value >> 32)); 

//for classes that contain class instance fields which inherit from object
return obj1.GetHashCode();

//for classes that contain multiple class instance fields which inherit from object
return obj1.GetHashCode() ^ obj2.GetHashCode() ^ obj3.GetHashCode(); 

ฉันสามารถเดาได้ว่าสำหรับ int ขนาดใหญ่หลายตัวคุณสามารถใช้สิ่งนี้:

int a=((int)value1 ^ (int)(value1 >> 32));
int b=((int)value2 ^ (int)(value2 >> 32));
int c=((int)value3 ^ (int)(value3 >> 32));
return a ^ b ^ c;

เช่นเดียวกับหลายประเภท: ทั้งหมดที่ถูกแปลงเป็นคนแรกที่intใช้GetHashCode() แล้วค่า int จะเป็น xor'ed และผลลัพธ์คือแฮชของคุณ

สำหรับผู้ที่ใช้แฮชเป็น ID (ฉันหมายถึงค่าที่ไม่ซ้ำกัน) แฮช จำกัด ตามหลักตัวเลขโดยธรรมชาติฉันคิดว่ามันเป็น 5 ไบต์สำหรับอัลกอริทึมการแฮชอย่างน้อย MD5

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


7
จำนวนเต็ม Xoring ในการสร้างแฮชโค้ดเป็นแอนตี้รูปแบบที่รู้จักกันดีซึ่งมีแนวโน้มที่จะส่งผลให้เกิดการชนจำนวนมากโดยเฉพาะอย่างยิ่งที่มีค่าในโลกแห่งความจริง
Jon Hanna

ทุกคนที่นี่ใช้จำนวนเต็มและไม่เคยมีการรับประกันใด ๆ สำหรับการแฮชเหมือนกันมันแค่พยายามที่จะแตกต่างกันมากเนื่องจากมีการชนกันเล็กน้อยที่จะเกิดขึ้น
deadManN

ใช่ แต่ข้อที่สองและข้อที่ห้าอย่าพยายามหลีกเลี่ยงการชน
Jon Hanna

1
ใช่พลั่วนั้นเป็นเรื่องธรรมดา
Jon Hanna

2
มีความสมดุลในการเข้าถึง ใช้รหัสแฮชที่ดีมากเช่น Spookyhash และคุณจะได้รับการหลีกเลี่ยงการชนที่ดีกว่ามาก แต่มันจะมีเวลาในการคำนวณมากกว่าเวลาใด ๆ (แต่เมื่อมันมาถึงการ hashing ข้อมูลจำนวนมาก Spookyhash นั้นเร็วมาก) การเลื่อนค่าอย่างใดอย่างหนึ่งอย่างใดอย่างหนึ่งก่อนที่จะ xoring เป็นค่าใช้จ่ายเพิ่มเติมเพียงเล็กน้อยสำหรับการลดการชนที่ดี การทวีคูณจำนวนเฉพาะเพิ่มทั้งเวลาและคุณภาพอีกครั้ง ซึ่งดีกว่าระหว่างกะหรือ mult จึงเป็นที่ถกเถียงกัน xor ธรรมดาแม้ว่าบ่อยครั้งมากจะมีการชนกันของข้อมูลจริงและหลีกเลี่ยงได้ดีที่สุด
Jon Hanna

1

นี่คือคลาสผู้ช่วยแบบสแตติกที่ใช้ Josh Bloch และจัดเตรียมการโอเวอร์โหลดอย่างชัดเจนเพื่อ "ป้องกัน" การชกมวยและเพื่อใช้แฮชโดยเฉพาะสำหรับการใช้งานแบบดั้งเดิม

คุณสามารถผ่านการเปรียบเทียบสตริงที่ตรงกับการใช้งานเท่ากับของคุณ

เนื่องจากเอาต์พุตแฮชจะเป็น int เสมอคุณจึงสามารถโยงการเรียกแฮชได้

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace Sc.Util.System
{
    /// <summary>
    /// Static methods that allow easy implementation of hashCode. Example usage:
    /// <code>
    /// public override int GetHashCode()
    ///     => HashCodeHelper.Seed
    ///         .Hash(primitiveField)
    ///         .Hsh(objectField)
    ///         .Hash(iEnumerableField);
    /// </code>
    /// </summary>
    public static class HashCodeHelper
    {
        /// <summary>
        /// An initial value for a hashCode, to which is added contributions from fields.
        /// Using a non-zero value decreases collisions of hashCode values.
        /// </summary>
        public const int Seed = 23;

        private const int oddPrimeNumber = 37;


        /// <summary>
        /// Rotates the seed against a prime number.
        /// </summary>
        /// <param name="aSeed">The hash's first term.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int rotateFirstTerm(int aSeed)
        {
            unchecked {
                return HashCodeHelper.oddPrimeNumber * aSeed;
            }
        }


        /// <summary>
        /// Contributes a boolean to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aBoolean">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, bool aBoolean)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (aBoolean
                                ? 1
                                : 0);
            }
        }

        /// <summary>
        /// Contributes a char to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aChar">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, char aChar)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aChar;
            }
        }

        /// <summary>
        /// Contributes an int to the developing HashCode seed.
        /// Note that byte and short are handled by this method, through implicit conversion.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aInt">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, int aInt)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aInt;
            }
        }

        /// <summary>
        /// Contributes a long to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aLong">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, long aLong)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (int)(aLong ^ (aLong >> 32));
            }
        }

        /// <summary>
        /// Contributes a float to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aFloat">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, float aFloat)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + Convert.ToInt32(aFloat);
            }
        }

        /// <summary>
        /// Contributes a double to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aDouble">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, double aDouble)
            => aSeed.Hash(Convert.ToInt64(aDouble));

        /// <summary>
        /// Contributes a string to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aString">The value to contribute.</param>
        /// <param name="stringComparison">Optional comparison that creates the hash.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(
                this int aSeed,
                string aString,
                StringComparison stringComparison = StringComparison.Ordinal)
        {
            if (aString == null)
                return aSeed.Hash(0);
            switch (stringComparison) {
                case StringComparison.CurrentCulture :
                    return StringComparer.CurrentCulture.GetHashCode(aString);
                case StringComparison.CurrentCultureIgnoreCase :
                    return StringComparer.CurrentCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.InvariantCulture :
                    return StringComparer.InvariantCulture.GetHashCode(aString);
                case StringComparison.InvariantCultureIgnoreCase :
                    return StringComparer.InvariantCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.OrdinalIgnoreCase :
                    return StringComparer.OrdinalIgnoreCase.GetHashCode(aString);
                default :
                    return StringComparer.Ordinal.GetHashCode(aString);
            }
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// Each element may be a primitive, a reference, or a possibly-null array.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, IEnumerable aArray)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (object item in aArray) {
                ++countPlusOne;
                if (item is IEnumerable arrayItem) {
                    if (!object.ReferenceEquals(aArray, arrayItem))
                        aSeed = aSeed.Hash(arrayItem); // recursive call!
                } else
                    aSeed = aSeed.Hash(item);
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// You must provide the hash function for each element.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <param name="hashElement">Required: yields the hash for each element
        /// in <paramref name="aArray"/>.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash<T>(this int aSeed, IEnumerable<T> aArray, Func<T, int> hashElement)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (T item in aArray) {
                ++countPlusOne;
                aSeed = aSeed.Hash(hashElement(item));
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null object to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, object aObject)
        {
            switch (aObject) {
                case null :
                    return aSeed.Hash(0);
                case bool b :
                    return aSeed.Hash(b);
                case char c :
                    return aSeed.Hash(c);
                case int i :
                    return aSeed.Hash(i);
                case long l :
                    return aSeed.Hash(l);
                case float f :
                    return aSeed.Hash(f);
                case double d :
                    return aSeed.Hash(d);
                case string s :
                    return aSeed.Hash(s);
                case IEnumerable iEnumerable :
                    return aSeed.Hash(iEnumerable);
            }
            return aSeed.Hash(aObject.GetHashCode());
        }


        /// <summary>
        /// This utility method uses reflection to iterate all specified properties that are readable
        /// on the given object, excluding any property names given in the params arguments, and
        /// generates a hashcode.
        /// </summary>
        /// <param name="aSeed">The developing hash code, or the seed: if you have no seed, use
        /// the <see cref="Seed"/>.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <param name="propertySelector"><see cref="BindingFlags"/> to select the properties to hash.</param>
        /// <param name="ignorePropertyNames">Optional.</param>
        /// <returns>A hash from the properties contributed to <c>aSeed</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashAllProperties(
                this int aSeed,
                object aObject,
                BindingFlags propertySelector
                        = BindingFlags.Instance
                        | BindingFlags.Public
                        | BindingFlags.GetProperty,
                params string[] ignorePropertyNames)
        {
            if (aObject == null)
                return aSeed.Hash(0);
            if ((ignorePropertyNames != null)
                    && (ignorePropertyNames.Length != 0)) {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (!propertyInfo.CanRead
                            || (Array.IndexOf(ignorePropertyNames, propertyInfo.Name) >= 0))
                        continue;
                    aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            } else {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (propertyInfo.CanRead)
                        aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            }
            return aSeed;
        }


        /// <summary>
        /// NOTICE: this method is provided to contribute a <see cref="KeyValuePair{TKey,TValue}"/> to
        /// the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on the Key or Value here if that itself is a KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePair">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeyAndValue<TKey, TValue>(this int aSeed, KeyValuePair<TKey, TValue> keyValuePair)
            => aSeed.Hash(keyValuePair.Key)
                    .Hash(keyValuePair.Value);

        /// <summary>
        /// NOTICE: this method is provided to contribute a collection of <see cref="KeyValuePair{TKey,TValue}"/>
        /// to the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on a Key or Value here if that itself is a KeyValuePair or an Enumerable of
        /// KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePairs">The values to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeysAndValues<TKey, TValue>(
                this int aSeed,
                IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs)
        {
            if (keyValuePairs == null)
                return aSeed.Hash(null);
            foreach (KeyValuePair<TKey, TValue> keyValuePair in keyValuePairs) {
                aSeed = aSeed.HashKeyAndValue(keyValuePair);
            }
            return aSeed;
        }
    }
}

Yipes: ฉันพบข้อผิดพลาด! HashKeysAndValuesวิธีการได้รับการแก้ไข: HashKeyAndValueมันจะเรียก
Steven Coco

0

ในกรณีที่คุณต้องการที่จะ polyfill HashCodeจากnetstandard2.1

public static class HashCode
{
    public static int Combine(params object[] instances)
    {
        int hash = 17;

        foreach (var i in instances)
        {
            hash = unchecked((hash * 31) + (i?.GetHashCode() ?? 0));
        }

        return hash;
    }
}

หมายเหตุ: หากใช้กับstructมันจะจัดสรรหน่วยความจำเนื่องจากการชกมวย

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