使用两个键

时间:2018-05-09 01:40:24

标签: c# collections

想象一下这种情况:我需要操作(添加,搜索和删除)Book类型对象列表中的项目。

class Book{
 int Id {get; set;}
 string Title {get; set;}
 string Author {get; set;}
 int Year {get; set;}
 // more properties
}

Constriants:

  • IdBooks
  • 的集合中应该是唯一的
  • TitleBooks
  • 的集合中应该是唯一的

到目前为止,Dictionary<int, Book>Id为关键字,Book为值。但在这种情况下,如果我想在字典中添加新书,我必须遍历所有值以检查Title是否重复。

我开始考虑仅为标题创建HashSet,或者使用以Dictionary<string, Book>作为关键字的第二个字典Title

任何建议如何处理这种情况?

修改

正如@David所说,我忘了告诉我这里主要关注的是表现。我希望以最快的方式通过Id和Title查找对象(O(1))。

3 个答案:

答案 0 :(得分:2)

您可以使用元组作为键:

var collection = new Dictionary<Tuple<int, string>, Book> (...);
var key = new Tuple<int, string>(1, "David");  // <<-----------
if(!collection.ContainsKey(key))
    collection [key] = new Book(...);

请注意,Tuple内置Equals()以使您的生活更轻松。

更新

@AustinWBryan提到使用ValueTuples(C#7.0功能)来取代Tuple,强烈推荐。有关ValueTuples的更多信息,请参阅this link

答案 1 :(得分:1)

为确保复合键的两侧也是唯一的,元组不会将其剪切。而是在等号检查器中创建自己的密钥来检查它。

public struct CompositeKey<T1, T2> : IEquatable<CompositeKey<T1, T2>>
{
    private static readonly EqualityComparer<T1> t1Comparer = EqualityComparer<T1>.Default;
    private static readonly EqualityComparer<T2> t2Comparer = EqualityComparer<T2>.Default;

    public T1 Key1;
    public T2 Key2;

    public CompositeKey(T1 key1, T2 key2)
    {
        Key1 = key1;
        Key2 = key2;
    }

    public override bool Equals(object obj) => obj is CompositeKey<T1, T2> && Equals((CompositeKey<T1, T2>)obj);

    public bool Equals(CompositeKey<T1, T2> other)
    {
        return t1Comparer.Equals(Key1, other.Key1)
            && t2Comparer.Equals(Key2, other.Key2);
    }

    public override int GetHashCode() => Key1.GetHashCode();
}

因此字典适用于存储桶。它根据GetHashCode()生成的哈希码将所有密钥放入桶中。然后,它使用Equals()上的for循环搜索该存储桶。这个想法是桶应该尽可能小(理想情况下是一个项目)。

因此,我们可以通过控制哈希码来控制密钥何时匹配,以及有多少桶/项。如果我们返回一个像0这样的常量哈希码,那么所有内容都在同一个桶中,并且它是用于比较每个项目的相等方法。

此比较器仅返回第一个关键项的哈希值。假设第一个关键项应该是唯一的,这就足够了。每个桶应该仍然是一个项目,并且在进行查找(使用完全等于方法)时,也检查第二个键以确保类型是相同的值。

如果您想使用ValueTuple作为密钥类型,您可以将自定义比较器传递给字典以达到相同的效果。

public class CompositeValueTupleComparer<T1, T2> : IEqualityComparer<(T1, T2)>
{
    private static readonly EqualityComparer<T1> t1Comparer = EqualityComparer<T1>.Default;
    private static readonly EqualityComparer<T2> t2Comparer = EqualityComparer<T2>.Default;

    public bool Equals((T1, T2) x, (T1, T2) y) => 
        t1Comparer.Equals(x.Item1, y.Item1) && t2Comparer.Equals(x.Item2, y.Item2);

    public int GetHashCode((T1, T2) obj) => obj.Item1.GetHashCode();
}

new Dictionary<(int, string), Book>(new CompositeValueTupleComparer<int, string>());

答案 2 :(得分:0)

It seems like both the ID and Name are going to be unique, as in, you shouldn't be able to use the same ID twice, regardless if the name has been used already. Otherwise, we'd end up with dict[3] referring to two different values.

Tuples or structs can't give that behavior, and still require you to loop. What you should instead do, is use a class similar to the one I've created:

public class TwoKeyDictionary<TKey1, TKey2, TValue>
{
    public readonly List<TKey1> firstKeys  = new List<TKey1>();
    public readonly List<TKey2> secondKeys = new List<TKey2>();
    public readonly List<TValue> values    = new List<TValue>();


    public void Add(TKey1 key1, TKey2 key2, TValue value)
    {
        if (firstKeys.Contains(key1))  throw new ArgumentException();
        if (secondKeys.Contains(key2)) throw new ArgumentException();

        firstKeys.Add(key1);
        secondKeys.Add(key2);
        values.Add(value);
    }

    public void Remove(TKey1 key) => RemoveAll(firstKeys.IndexOf(key));
    public void Remove(TKey2 key) => RemoveAll(secondKeys.IndexOf(key));
    private void RemoveAll(int index)
    {
        if (index < 1) return;

        firstKeys.RemoveAt(index);
        secondKeys.RemoveAt(index);
        values.RemoveAt(index);
    }


    public TValue this[TKey1 key1]
    {
        get
        {
            int index = firstKeys.IndexOf(key1);
            if (index < 0) throw new IndexOutOfRangeException();

            return values[firstKeys.IndexOf(key1)];
        }
    }

    public TValue this[TKey2 key2]
    {
        get
        {
            int index = secondKeys.IndexOf(key2);
            if (index < 0) throw new IndexOutOfRangeException();

            return values[secondKeys.IndexOf(key2)];
        }
    }
}

And then you can use it like this:

var twoDict = new TwoKeyDictionary<int, string, float>();
twoDict.Add(0, "a", 0.5f);
twoDict.Add(2, "b", 0.25f);

Console.WriteLine(twoDict[0]);     // Prints "0.5"
Console.WriteLine(twoDict[2]);     // Prints "0.25"
Console.WriteLine(twoDict["a"]);   // Prints "0.5"
Console.WriteLine(twoDict["b"]);   // Prints "0.25"

twoDict.Add(0, "d", 2);            // Throws exception: 0 has already been added, even though "d" hasn't
twoDict.Add(1, "a", 5);            // Throws exception: "a" has already been added, even though "1" hasn't

The TwoKeyDictionary would need to implement ICollection, IEnumerable, etc., to do the full behavior stuff