ทูเปิล (หรืออาร์เรย์) เป็นปุ่มพจนานุกรมใน C #


109

ฉันกำลังพยายามสร้างตารางค้นหาพจนานุกรมใน C # ฉันต้องการแก้ไขค่า 3 ทูเพิลเป็นสตริงเดียว ฉันลองใช้อาร์เรย์เป็นคีย์ แต่ก็ไม่ได้ผลและฉันไม่รู้จะทำอะไรอีก ณ จุดนี้ฉันกำลังพิจารณาทำ Dictionary of Dictionaries of Dictionaries แต่มันอาจจะดูไม่สวยเท่าไหร่นักแม้ว่าฉันจะทำมันในจาวาสคริปต์ก็ตาม

คำตอบ:


113

หากคุณใช้. NET 4.0 ให้ใช้ Tuple:

lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

หากไม่สามารถกำหนดTupleและใช้เป็นคีย์ได้ tuple ที่ต้องการที่จะแทนที่GetHashCode, EqualsและIEquatable:

struct Tuple<T, U, W> : IEquatable<Tuple<T,U,W>>
{
    readonly T first;
    readonly U second;
    readonly W third;

    public Tuple(T first, U second, W third)
    {
        this.first = first;
        this.second = second;
        this.third = third;
    }

    public T First { get { return first; } }
    public U Second { get { return second; } }
    public W Third { get { return third; } }

    public override int GetHashCode()
    {
        return first.GetHashCode() ^ second.GetHashCode() ^ third.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        return Equals((Tuple<T, U, W>)obj);
    }

    public bool Equals(Tuple<T, U, W> other)
    {
        return other.first.Equals(first) && other.second.Equals(second) && other.third.Equals(third);
    }
}

6
โครงสร้างนี้ควรใช้ IEquatable <Tuple <T, U, W >> ด้วยวิธีนี้คุณสามารถหลีกเลี่ยงการชกมวยได้เมื่อมีการเรียกใช้ Equals () ในกรณีของการชนกันของรหัสแฮ
Dustin Campbell

16
@jerryjvl และคนอื่น ๆ ที่ค้นพบสิ่งนี้โดย Google เช่นเดียวกับที่ฉันทำ Tuple ของ. NET 4 ใช้ค่าเท่ากับเพื่อให้สามารถใช้ในพจนานุกรมได้
Scott Chamberlain

30
GetHashCodeการนำไปใช้ของคุณไม่ดีนัก มันไม่แปรผันภายใต้การเปลี่ยนแปลงของฟิลด์
CodesInChaos

2
Tuple ไม่ควรเป็นโครงสร้าง ในกรอบ Tuple เป็นประเภทอ้างอิง
Michael Graczyk

5
@Thoraot - แน่นอนว่าตัวอย่างของคุณเป็นเท็จ ... มันควรจะเป็น จะnew object()เท่ากับอีกnew object()ทำไม มันไม่ได้ใช้แค่การเปรียบเทียบอ้างอิงแบบตรงเท่านั้น ... ลอง:bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
Mike Marynowski

35

ระหว่างวิธีการตามพจนานุกรมทูเพิลและแบบซ้อนการเลือกใช้ทูเพิลจะดีกว่าเสมอ

จากจุดการบำรุงรักษาของมุมมอง ,

  • มันง่ายกว่ามากในการใช้ฟังก์ชันที่ดูเหมือน:

    var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

    กว่า

    var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();

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

  • นอกจากนี้หากคีย์คอมโพสิตของคุณต้องการมากกว่าหนึ่งฟิลด์ (หรือน้อยกว่า) ในอนาคตคุณจะต้องเปลี่ยนโค้ดจำนวนมากในกรณีที่สอง (พจนานุกรมแบบซ้อน) เนื่องจากคุณต้องเพิ่มพจนานุกรมที่ซ้อนกันเพิ่มเติมและการตรวจสอบในภายหลัง

จากมุมมองด้านประสิทธิภาพข้อสรุปที่ดีที่สุดที่คุณสามารถทำได้คือการวัดผลด้วยตัวเอง แต่มีข้อ จำกัด ทางทฤษฎีบางประการที่คุณสามารถพิจารณาได้ล่วงหน้า:

  • ในกรณีพจนานุกรมที่ซ้อนกันการมีพจนานุกรมเพิ่มเติมสำหรับทุกคีย์ (ด้านนอกและด้านใน) จะมีหน่วยความจำเหนือศีรษะ (มากกว่าที่การสร้างทูเปิลจะมี)

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

  • เกี่ยวกับวิธีการ tuple, tuples .NET ไม่ได้เป็น performant มากที่สุดเมื่อพวกเขากำลังหมายถึงการใช้เป็นคีย์ในชุดตั้งแต่EqualsและGetHashCodeสาเหตุการดำเนินมวยสำหรับประเภทค่า

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


หมายเหตุด้านข้างเครื่องสำอางไม่กี่ชิ้นสามารถทำให้พจนานุกรมดูดีได้:

  1. การโทรสไตล์ Indexer สามารถทำความสะอาดและใช้งานง่ายมาก สำหรับเช่น

    string foo = dict[a, b, c]; //lookup
    dict[a, b, c] = ""; //update/insertion

    ดังนั้นแสดงดัชนีที่จำเป็นในชั้นเรียนพจนานุกรมของคุณซึ่งจะจัดการการแทรกและการค้นหาภายใน

  2. นอกจากนี้ให้ใช้IEnumerableอินเทอร์เฟซที่เหมาะสมและจัดเตรียมAdd(TypeA, TypeB, TypeC, string)วิธีการที่จะให้ไวยากรณ์ของตัวเริ่มต้นคอลเลคชันเช่น:

    new MultiKeyDictionary<TypeA, TypeB, TypeC, string> 
    { 
        { a, b, c, null }, 
        ...
    };

ในกรณีของพจนานุกรมที่ซ้อนกันไวยากรณ์ตัวทำดัชนีจะไม่เป็นแบบนี้อีกต่อไป: string foo = dict[a][b][c]?
Steven Rands

@StevenRands ใช่มันจะเป็น
nawfal

1
@nawfal ฉันสามารถค้นหาพจนานุกรมทูเปิลได้หรือไม่เมื่อฉันมีเพียงคีย์เดียวไม่ใช่ทั้งหมด หรือฉันจะทำเหมือน dict [a, b] แล้วก็ dict [a, c] ได้ไหม
Khan Engineer

@KhanEngineer ส่วนใหญ่ขึ้นอยู่กับวัตถุประสงค์ของพจนานุกรมหรือวิธีที่คุณต้องการใช้ aสำหรับเช่นคุณต้องการที่จะได้รับค่ากลับมาเป็นส่วนหนึ่งของคีย์ที่ คุณก็สามารถย้ำพจนานุกรมใด ๆ เช่นเดียวกับคอลเลกชันปกติใด ๆ aและตรวจสอบคุณสมบัติที่สำคัญถ้ามันเป็น หากคุณต้องการได้รับรายการใน dict ด้วยคุณสมบัติแรกเสมอคุณสามารถออกแบบพจนานุกรมเป็นพจนานุกรมของพจนานุกรมได้ดีขึ้นดังที่แสดงในคำตอบและข้อความค้นหาของฉันdict[a]ซึ่งจะให้พจนานุกรมอื่นแก่คุณ
nawfal

หากคุณต้องการ "ค้นหาด้วยคีย์เดียว" โดย "ค้นหาด้วยคีย์ใดคีย์หนึ่ง" ที่คุณมีคุณควรออกแบบพจนานุกรมของคุณใหม่เป็น "พจนานุกรมคีย์ใดก็ได้" เพื่อยกตัวอย่างเช่นถ้าคุณต้องการที่จะได้รับค่า4สำหรับคีย์ทั้งสองaและbแล้วคุณสามารถทำให้มันเป็นพจนานุกรมมาตรฐานและเพิ่มค่าเหมือนและdict[a] = 4 dict[b] = 4อาจไม่สมเหตุสมผลหากเหตุผลของคุณaและbควรเป็นหน่วยเดียว ในกรณีเช่นนี้คุณสามารถกำหนดแบบกำหนดเองIEqualityComparerซึ่งถือเอาสองอินสแตนซ์คีย์เท่ากันหากคุณสมบัติใด ๆ เท่ากัน ทั้งหมดนี้สามารถทำได้โดยทั่วไปด้วย refelction
nawfal

30

หากคุณใช้ C # 7 คุณควรพิจารณาใช้สิ่งที่มีค่าเป็นคีย์ผสมของคุณ โดยทั่วไปค่า tuples จะให้ประสิทธิภาพที่ดีกว่า tuples อ้างอิงแบบเดิม ( Tuple<T1, …>) เนื่องจากค่า tuples เป็นชนิดของค่า (โครงสร้าง) ไม่ใช่ชนิดอ้างอิงดังนั้นจึงหลีกเลี่ยงการจัดสรรหน่วยความจำและค่าใช้จ่ายในการรวบรวมขยะ นอกจากนี้ยังมีไวยากรณ์ที่กระชับและใช้งานง่ายมากขึ้นทำให้สามารถตั้งชื่อฟิลด์ได้หากคุณต้องการ พวกเขายังใช้IEquatable<T>อินเทอร์เฟซที่จำเป็นสำหรับพจนานุกรม

var dict = new Dictionary<(int PersonId, int LocationId, int SubjectId), string>();
dict.Add((3, 6, 9), "ABC");
dict.Add((PersonId: 4, LocationId: 9, SubjectId: 10), "XYZ");
var personIds = dict.Keys.Select(k => k.PersonId).Distinct().ToList();

13

วิธีที่ดีสะอาดรวดเร็วง่ายและอ่านได้คือ:

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

เพิ่มสิ่งที่คล้ายกันดังนี้:

public sealed class myKey : Tuple<TypeA, TypeB, TypeC>
{
    public myKey(TypeA dataA, TypeB dataB, TypeC dataC) : base (dataA, dataB, dataC) { }

    public TypeA DataA => Item1; 

    public TypeB DataB => Item2;

    public TypeC DataC => Item3;
}

ดังนั้นคุณสามารถใช้กับพจนานุกรม:

var myDictinaryData = new Dictionary<myKey, string>()
{
    {new myKey(1, 2, 3), "data123"},
    {new myKey(4, 5, 6), "data456"},
    {new myKey(7, 8, 9), "data789"}
};
  • คุณยังสามารถใช้ในสัญญา
  • เป็นกุญแจสำคัญสำหรับการเข้าร่วมหรือการจัดกลุ่มใน linq
  • ไปแบบนี้คุณจะไม่เคยพิมพ์คำสั่งของ Item1, Item2, Item3 ...
  • คุณไม่จำเป็นต้องจำหรือพิจารณารหัสเพื่อทำความเข้าใจว่าจะไปหาอะไรได้ที่ไหน
  • ไม่จำเป็นต้องแทนที่ IStructuralEquatable, IStructuralComparable, IComparable, ITuple พวกเขาทั้งหมดกล่าวหาที่นี่

1
ตอนนี้คุณสามารถใช้การแสดงออกของสมาชิกที่มีร่างกายที่สะอาดยิ่งขึ้นเช่นpublic TypeA DataA => Item1;
andrewtatham

7

หากด้วยเหตุผลบางประการคุณต้องการหลีกเลี่ยงการสร้างคลาส Tuple ของคุณเองหรือใช้ใน. NET 4.0 มีวิธีอื่นที่เป็นไปได้ คุณสามารถรวมค่าคีย์ทั้งสามเข้าด้วยกันเป็นค่าเดียว

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

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

string.Format("{0}#{1}#{2}", key1, key2, key3)

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


6
ฉันจะบอกว่ามันขึ้นอยู่กับบริบทอย่างยิ่ง ถ้าฉันมีจำนวนเต็มสามประเภทที่จะรวมกันและประสิทธิภาพไม่สำคัญสิ่งนี้จะทำงานได้ดีอย่างสมบูรณ์โดยมีโอกาสน้อยที่จะทำผิดพลาด แน่นอนว่าทั้งหมดนี้ซ้ำซ้อนอย่างสมบูรณ์ในขณะที่. NET 4 เนื่องจาก Microsoft จะให้เรา (น่าจะถูกต้อง!) ประเภท Tuple นอกกรอบ
jerryjvl

คุณสามารถใช้วิธีนี้ร่วมกับ a JavaScriptSerializerเพื่อต่ออาร์เรย์ของสตริงและ / หรือประเภทจำนวนเต็มให้คุณได้ ด้วยวิธีนี้คุณไม่จำเป็นต้องสร้างตัวคั่นด้วยตัวเอง
binki

3
ซึ่งอาจได้รับยุ่งจริงถ้าใด ๆ ของคีย์ ( key1, key2, key3) เป็นสายที่มี deliminator ( "#")
เกร็ก

4

ฉันจะแทนที่ Tuple ของคุณด้วย GetHashCode ที่เหมาะสมและใช้มันเป็นคีย์

ตราบเท่าที่คุณใช้วิธีการที่เหมาะสมมากเกินไปคุณจะเห็นประสิทธิภาพที่ดี


1
IComparable ไม่มีผลต่อการจัดเก็บคีย์หรือตำแหน่งในพจนานุกรม <TKey, TValue> ทุกอย่างทำได้ผ่าน GetHashCode () และ IEqualityComparer <T> การใช้ IEquatable <T> จะทำให้ได้ประสิทธิภาพที่ดีขึ้นเนื่องจากช่วยลดการชกมวยที่เกิดจาก EqualityComparer เริ่มต้นซึ่งกลับมาอยู่ที่ฟังก์ชัน Equals (object)
Dustin Campbell

ฉันจะพูดถึง GetHashCode แต่ฉันคิดว่า Dictionary ใช้ IComparable ในกรณีที่ HashCodes เหมือนกัน ... เดาว่าฉันคิดผิด
John Gietzen

3

นี่คือ. NET tuple สำหรับการอ้างอิง:

[Serializable] 
public class Tuple<T1, T2, T3> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple {

    private readonly T1 m_Item1; 
    private readonly T2 m_Item2;
    private readonly T3 m_Item3; 

    public T1 Item1 { get { return m_Item1; } }
    public T2 Item2 { get { return m_Item2; } }
    public T3 Item3 { get { return m_Item3; } } 

    public Tuple(T1 item1, T2 item2, T3 item3) { 
        m_Item1 = item1; 
        m_Item2 = item2;
        m_Item3 = item3; 
    }

    public override Boolean Equals(Object obj) {
        return ((IStructuralEquatable) this).Equals(obj, EqualityComparer<Object>.Default);; 
    }

    Boolean IStructuralEquatable.Equals(Object other, IEqualityComparer comparer) { 
        if (other == null) return false;

        Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;

        if (objTuple == null) {
            return false; 
        }

        return comparer.Equals(m_Item1, objTuple.m_Item1) && comparer.Equals(m_Item2, objTuple.m_Item2) && comparer.Equals(m_Item3, objTuple.m_Item3); 
    }

    Int32 IComparable.CompareTo(Object obj) {
        return ((IStructuralComparable) this).CompareTo(obj, Comparer<Object>.Default);
    }

    Int32 IStructuralComparable.CompareTo(Object other, IComparer comparer) {
        if (other == null) return 1; 

        Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;

        if (objTuple == null) {
            throw new ArgumentException(Environment.GetResourceString("ArgumentException_TupleIncorrectType", this.GetType().ToString()), "other");
        }

        int c = 0;

        c = comparer.Compare(m_Item1, objTuple.m_Item1); 

        if (c != 0) return c; 

        c = comparer.Compare(m_Item2, objTuple.m_Item2);

        if (c != 0) return c; 

        return comparer.Compare(m_Item3, objTuple.m_Item3); 
    } 

    public override int GetHashCode() { 
        return ((IStructuralEquatable) this).GetHashCode(EqualityComparer<Object>.Default);
    }

    Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer) { 
        return Tuple.CombineHashCodes(comparer.GetHashCode(m_Item1), comparer.GetHashCode(m_Item2), comparer.GetHashCode(m_Item3));
    } 

    Int32 ITuple.GetHashCode(IEqualityComparer comparer) {
        return ((IStructuralEquatable) this).GetHashCode(comparer); 
    }
    public override string ToString() {
        StringBuilder sb = new StringBuilder();
        sb.Append("("); 
        return ((ITuple)this).ToString(sb);
    } 

    string ITuple.ToString(StringBuilder sb) {
        sb.Append(m_Item1); 
        sb.Append(", ");
        sb.Append(m_Item2);
        sb.Append(", ");
        sb.Append(m_Item3); 
        sb.Append(")");
        return sb.ToString(); 
    } 

    int ITuple.Size { 
        get {
            return 3;
        }
    } 
}

3
รับรหัสแฮชใช้งานเป็น ((item1 ^ item2) * 33) ^ item3
Michael Graczyk

2

หากโค้ดที่คุณใช้สามารถทำกับอินเทอร์เฟซ IDictionary <> แทนที่จะเป็นพจนานุกรมสัญชาตญาณของฉันก็คือการใช้ SortedDictionary <> กับตัวเปรียบเทียบอาร์เรย์แบบกำหนดเองเช่น:

class ArrayComparer<T> : IComparer<IList<T>>
    where T : IComparable<T>
{
    public int Compare(IList<T> x, IList<T> y)
    {
        int compare = 0;
        for (int n = 0; n < x.Count && n < y.Count; ++n)
        {
            compare = x[n].CompareTo(y[n]);
        }
        return compare;
    }
}

และสร้างสิ่งนี้ (ใช้ int [] เพื่อประโยชน์ของตัวอย่างที่เป็นรูปธรรม):

var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.