Tuples (hoặc mảng) làm khóa Từ điển trong C #


109

Tôi đang cố tạo bảng tra cứu Từ điển trong C #. Tôi cần phân giải 3 bộ giá trị thành một chuỗi. Tôi đã thử sử dụng mảng làm khóa, nhưng điều đó không hoạt động và tôi không biết phải làm gì khác. Tại thời điểm này, tôi đang xem xét việc tạo một Từ điển Từ điển của Từ điển, nhưng điều đó có lẽ sẽ không đẹp cho lắm, mặc dù đó là cách tôi làm trong javascript.

Câu trả lời:


114

Nếu bạn đang sử dụng .NET 4.0, hãy sử dụng Tuple:

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

Nếu không, bạn có thể xác định Tuple và sử dụng nó làm khóa. Tuple cần ghi đè GetHashCode, Equals và 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
Cấu trúc này cũng nên triển khai IEquatable <Tuple <T, U, W >>. Bằng cách đó, bạn có thể tránh quyền anh khi Equals () được gọi trong trường hợp xung đột mã băm.
Dustin Campbell

16
@jerryjvl và tất cả những người khác tìm thấy điều này bởi Google như tôi đã làm, Tuple của .NET 4 triển khai bằng để nó có thể được sử dụng trong từ điển.
Scott Chamberlain

30
Việc GetHashCodetriển khai của bạn không tốt lắm. Nó bất biến dưới sự hoán vị của các trường.
CodesInChaos

2
Tuple không nên là một cấu trúc. Trong khuôn khổ, Tuple là một kiểu tham chiếu.
Michael Graczyk

5
@Thoraot - tất nhiên ví dụ của bạn là sai ... nó nên như vậy. Tại sao sẽ new object()bằng nhau new object()? Nó không chỉ cần sử dụng tài liệu tham khảo comarison thẳng ... thử:bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
Mike Marynowski

35

Giữa các phương pháp tiếp cận dựa trên tuple và từ điển lồng nhau, hầu như luôn luôn tốt hơn nên sử dụng dựa trên tuple.

Từ quan điểm về khả năng bảo trì ,

  • nó dễ dàng hơn nhiều để triển khai một chức năng trông giống như:

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

    hơn

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

    từ phía callee. Trong trường hợp thứ hai, mỗi lần bổ sung, tra cứu, loại bỏ, v.v. yêu cầu hành động trên nhiều từ điển.

  • Hơn nữa, nếu khóa tổng hợp của bạn yêu cầu một trường nhiều hơn (hoặc ít hơn) trong tương lai, bạn sẽ cần phải thay đổi mã rất nhiều trong trường hợp thứ hai (từ điển lồng nhau) vì bạn phải thêm các từ điển lồng nhau và các lần kiểm tra tiếp theo.

Từ quan điểm hiệu suất , kết luận tốt nhất bạn có thể đạt được là tự mình đo lường nó. Nhưng có một số hạn chế về mặt lý thuyết mà bạn có thể cân nhắc trước:

  • Trong trường hợp từ điển lồng nhau, có một từ điển bổ sung cho mọi khóa (bên ngoài và bên trong) sẽ có một số chi phí bộ nhớ (nhiều hơn những gì tạo một tuple sẽ có).

  • Trong trường hợp từ điển lồng nhau, mọi hành động cơ bản như thêm, cập nhật, tra cứu, xóa, v.v. cần phải được thực hiện trong hai từ điển. Bây giờ có một trường hợp mà phương pháp tiếp cận từ điển lồng nhau có thể nhanh hơn, tức là khi dữ liệu đang được tra cứu không có, vì các từ điển trung gian có thể bỏ qua tính toán & so sánh mã băm đầy đủ, nhưng sau đó, cần tính thời gian để đảm bảo. Khi có dữ liệu, nó sẽ chậm hơn vì tra cứu phải được thực hiện hai lần (hoặc ba lần tùy thuộc vào lồng ghép).

  • Về phương pháp tiếp cận tuple, các bộ giá trị .NET không phải là hoạt động hiệu quả nhất khi chúng được sử dụng làm khóa trong các tập hợp vì nó EqualsGetHashCodeviệc triển khai gây ra quyền anh cho các loại giá trị .

Tôi sẽ sử dụng từ điển dựa trên tuple, nhưng nếu tôi muốn hiệu suất cao hơn, tôi sẽ sử dụng tuple của riêng mình với việc triển khai tốt hơn.


Một lưu ý nhỏ là, rất ít mỹ phẩm có thể khiến từ điển trở nên thú vị:

  1. Các lệnh gọi kiểu trình lập chỉ mục có thể rõ ràng và trực quan hơn rất nhiều. Ví dụ,

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

    Vì vậy, hãy hiển thị các chỉ mục cần thiết trong lớp từ điển của bạn để xử lý nội bộ việc chèn và tra cứu.

  2. Ngoài ra, hãy triển khai một IEnumerablegiao diện phù hợp và cung cấp một Add(TypeA, TypeB, TypeC, string)phương thức sẽ cung cấp cho bạn cú pháp của trình khởi tạo bộ sưu tập, như:

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

Trong trường hợp các từ điển lồng nhau, cú pháp của trình chỉ mục sẽ không giống như thế này string foo = dict[a][b][c]:?
Steven Rands

@StevenRands vâng, nó sẽ như vậy.
nawfal

1
@nawfal Tôi có thể tra từ điển tuple khi tôi chỉ có một trong các khóa không phải tất cả không? hoặc Tôi có thể làm như này dict [a, b] then dict [a, c] không?
Khan Engineer,

@KhanEngineer Rất nhiều điều đó phụ thuộc vào mục đích sử dụng của từ điển là gì hoặc cách bạn định sử dụng nó. Ví dụ: bạn muốn lấy lại giá trị bằng một phần của khóa a,. Bạn chỉ có thể lặp lại bất kỳ từ điển nào giống như bất kỳ bộ sưu tập thông thường nào và kiểm tra thuộc tính khóa nếu có a. Nếu bạn luôn muốn lấy mục trong thuộc tính dict by first thì tốt hơn bạn có thể thiết kế từ điển dưới dạng từ điển của các từ điển như được hiển thị trong câu trả lời và truy vấn của tôi dict[a], điều này cung cấp cho bạn một từ điển khác.
nawfal,

Nếu "chỉ tìm kiếm bằng một khóa", bạn có nghĩa là lấy lại giá trị bằng bất kỳ khóa nào bạn có, thì tốt hơn bạn nên thiết kế lại từ điển của mình như một loại "từ điển khóa bất kỳ". Ví dụ: nếu bạn muốn nhận giá trị 4cho cả hai khóa ab, thì bạn có thể đặt nó thành một từ điển chuẩn và thêm các giá trị như dict[a] = 4dict[b] = 4. Nó có thể không có ý nghĩa nếu về mặt logic của bạn abphải là một đơn vị. Trong trường hợp như vậy, bạn có thể xác định một tùy chỉnh IEqualityComparertương đương với hai phiên bản khóa là bằng nhau nếu bất kỳ thuộc tính nào của chúng bằng nhau. Tất cả điều này nói chung có thể được thực hiện với tái chế.
nawfal,

30

Nếu bạn đang sử dụng C # 7, bạn nên cân nhắc sử dụng các bộ giá trị làm khóa tổng hợp của mình. Bộ giá trị thường cung cấp hiệu suất tốt hơn bộ Tuple<T1, …>giá trị tham chiếu truyền thống ( ) vì bộ giá trị là kiểu giá trị (cấu trúc), không phải kiểu tham chiếu, vì vậy chúng tránh được chi phí cấp phát bộ nhớ và thu gom rác. Ngoài ra, họ cung cấp cú pháp ngắn gọn và trực quan hơn, cho phép các trường của họ được đặt tên nếu bạn muốn. Họ cũng triển khai IEquatable<T>giao diện cần thiết cho từ điển.

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

Các cách tốt, sạch sẽ, nhanh chóng, dễ dàng và dễ đọc là:

  • tạo thành viên bình đẳng (Equals () và phương thức GetHashCode ()) cho kiểu hiện tại. Các công cụ như ReSharper không chỉ tạo ra các phương thức mà còn tạo mã cần thiết để kiểm tra bình đẳng và / hoặc để tính toán mã băm. Mã được tạo sẽ tối ưu hơn so với hiện thực Tuple.
  • chỉ cần tạo một lớp khóa đơn giản bắt nguồn từ một tuple .

thêm một cái gì đó tương tự như thế này:

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;
}

Vì vậy, bạn có thể sử dụng nó với từ điển:

var myDictinaryData = new Dictionary<myKey, string>()
{
    {new myKey(1, 2, 3), "data123"},
    {new myKey(4, 5, 6), "data456"},
    {new myKey(7, 8, 9), "data789"}
};
  • Bạn cũng có thể sử dụng nó trong các hợp đồng
  • như một khóa để tham gia hoặc nhóm trong linq
  • theo cách này, bạn sẽ không bao giờ gõ nhầm thứ tự của Item1, Item2, Item3 ...
  • bạn không cần phải nhớ hoặc nhìn vào mã để hiểu nơi cần đến để lấy thứ gì đó
  • không cần ghi đè IStructuralEquatable, IStructuralComp so sánh, IComp so sánh được, ITuple tất cả họ đều ở đây

1
Bây giờ bạn có thể sử dụng biểu hiện thân vỏ thành viên sạch thậm chí nó ví dụpublic TypeA DataA => Item1;
andrewtatham

7

Nếu vì lý do nào đó bạn thực sự muốn tránh tạo lớp Tuple của riêng mình hoặc sử dụng trên .NET 4.0 được tích hợp sẵn, thì có một cách tiếp cận khác có thể; bạn có thể kết hợp ba giá trị khóa với nhau thành một giá trị duy nhất.

Ví dụ: nếu ba giá trị là kiểu số nguyên cùng nhau không chiếm quá 64 bit, bạn có thể kết hợp chúng thành a ulong.

Trường hợp xấu nhất, bạn luôn có thể sử dụng một chuỗi, miễn là bạn đảm bảo ba thành phần trong đó được phân tách bằng một số ký tự hoặc chuỗi không xảy ra bên trong các thành phần của khóa, chẳng hạn với ba số, bạn có thể thử:

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

Rõ ràng là có một số chi phí về bố cục trong cách tiếp cận này, nhưng tùy thuộc vào những gì bạn đang sử dụng nó cho điều này có thể đủ tầm thường để không quan tâm đến nó.


6
Tôi muốn nói rằng nó phụ thuộc rất nhiều vào ngữ cảnh; nếu tôi có ba kiểu số nguyên để kết hợp và hiệu suất không quan trọng, thì điều này hoàn toàn tốt với khả năng mắc lỗi tối thiểu. Tất nhiên, tất cả những điều này là hoàn toàn dư thừa kể từ .NET 4, vì Microsoft sẽ cung cấp cho chúng ta (có lẽ là chính xác!) Các loại Tuple sẵn có.
jerryjvl

Bạn thậm chí có thể sử dụng phương thức này kết hợp với một JavaScriptSerializerđể nối một mảng các kiểu chuỗi và / hoặc số nguyên cho bạn. Bằng cách này, bạn không cần phải tự mình đưa ra ký tự phân tách.
binki

3
Điều này có thể nhận được lộn xộn thật nếu có trong những chìa khóa ( key1, key2, key3) là chuỗi chứa deliminator ( "#")
Greg

4

Tôi sẽ ghi đè Tuple của bạn bằng một Mã GetHashCode thích hợp và chỉ sử dụng nó làm khóa.

Miễn là bạn quá tải các phương thức thích hợp, bạn sẽ thấy hiệu suất tốt.


1
Tính năng so sánh được không ảnh hưởng đến cách các khóa được lưu trữ hoặc định vị trong Từ điển <TKey, TValue>. Tất cả được thực hiện thông qua GetHashCode () và IEqualityComparer <T>. Việc triển khai IEquatable <T> sẽ đạt được hiệu suất tốt hơn vì nó làm giảm bớt sự đấm bốc gây ra bởi EqualityComparer mặc định, nằm trong hàm Equals (đối tượng).
Dustin Campbell

Tôi đã định đề cập đến GetHashCode, nhưng tôi nghĩ rằng Từ điển đã sử dụng ICompABLE trong trường hợp các mã HashCodes giống hệt nhau ... đoán tôi đã nhầm.
John Gietzen

3

Đây là tuple .NET để tham khảo:

[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
Lấy mã băm được triển khai dưới dạng ((item1 ^ item2) * 33) ^ item3
Michael Graczyk

2

Nếu mã tiêu thụ của bạn có thể thực hiện với giao diện IDictionary <>, thay vì Từ điển, bản năng của tôi sẽ là sử dụng SortedDictionary <> với trình so sánh mảng tùy chỉnh, tức là:

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;
    }
}

Và tạo do đó (sử dụng int [] chỉ vì lợi ích của ví dụ cụ thể):

var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.