11

I need to use a Dictionary, where TKey is a pair of ints.

I thought of using KeyValuePair for the type of my keys and I was wondering if this was the best way around.

I'm also curious to know if the Dictionary will create separate entries for two different KeyValuePair objects with the same ints and why.

For instance:

var myDictionary = new Dictionary<KeyValuePair<int,int>, string>();
myDictionary.Add(new KeyValuePair<int,int>(3, 3), "FirstItem");
myDictionary.Add(new KeyValuePair<int,int>(3, 3), "SecondItem");
// does the dictionary allow this?
  • 1
    @Cuong Le: Because I want to know why more than the resulting behavior. –  Sep 24 '12 at 18:11
  • @asmo: I don't think `KeyValuePair` would be a good design choice here. For one thing, the `Equals` implementation isn't what you probably expect it to be. How about using a `Tuple`? – code4life Sep 24 '12 at 18:17
  • Maybe it is getting voted down because your example has duplicate keys. It is clear you have done no real testing. – paparazzo Sep 24 '12 at 18:20
  • I chose duplicates on purpose to describe the behavior of the dictionary that I want to avoid (ie: this should produce a runtime error). If you look at the edit history, there were no duplicates at first. –  Sep 24 '12 at 18:22
  • 1
    -1 If you know it will produce a run time error then why are you asking "curious to know if the Dictionary will create separate entries for two different KeyValuePair objects with the same ints" – paparazzo Sep 24 '12 at 18:28
  • 3
    @code4life: `KeyValuePair` does not implement `Equals` so it uses the default `struct` `Equals`, which works as expected in this case. – Guvante Sep 24 '12 at 18:57
  • @Guvante: hmm... looking at Reflector, it clearly does not try to compare the key-value combinations. Are you sure about what you said, or am I looking at the wrong part of code...? – code4life Sep 24 '12 at 19:33
  • 1
    @code4life: `ValueType` has a useful [default implementation](http://msdn.microsoft.com/en-us/library/2dts52z7.aspx) of `Equals`, so all simple `struct` values have built in `Equals`. – Guvante Sep 24 '12 at 20:43
  • @code4life Though it works as expected, it *need not be* still a good design choice if performance matters. `KeyValuePair` doesnt implement `IEquatable` which basically means there is some sort of reflection going behind `Equals` and `GetHashCode` of the struct. [`Tuple`s too suffer somewhat](http://stackoverflow.com/questions/21084412/net-tuple-and-equals-performance). – nawfal May 19 '14 at 14:53

6 Answers6

29

Maybe you should consider using a Tuple

var myDictionary = new Dictionary<Tuple<int,int>, List<string>>(); 
myDictionary.Add(new Tuple<int,int>(3, 3), "FirstItem"); 
myDictionary.Add(new Tuple<int,int>(5, 5), "SecondItem"); 

According to MSDN documentation, a Tuple objects Equals method will use the values of the two Tuple objects. This would result in one entry per Tuple in the outer dictionary and allow you to store a listing of the values per key.

Mike Perrenoud
  • 63,395
  • 23
  • 143
  • 222
  • Sorry I meant two times the same (3,3) couple in my question, otherwise it loses its meaning. You should update your answer too. –  Sep 24 '12 at 18:13
  • What is different that using KeyValuePair? You would exactly get the same behaviour. – L.B Sep 24 '12 at 18:14
  • What about using Dictionary ? Our project is .NET 2 so Tuple is not available. It seems to work as expected. –  Sep 24 '12 at 18:28
  • @L.B: `Tuple` is semantically more appropriate. With `KeyValuePair` you just want a `Pair`, so you end up using a misnamed type. Not a big deal but if you have `Tuple` why do that? – Jon Sep 24 '12 at 18:30
  • @asmo: So write your own Tuple. 5 minutes tops. – Jon Sep 24 '12 at 18:30
  • @Jon Also you can build Tuple objects using the Create factory method without having to specify the generics types, which makes the code simpler. var tuple2 = Tuple.Create("New York", 32.68); instead of : var tuple2 = new Tuple("New York", 32.68); –  Sep 24 '12 at 18:32
  • @asmo: Sure, but you can write your own (extension) method family like that for any type. – Jon Sep 24 '12 at 18:35
  • @Jon .NET 2 doesn't support extension methods. And I always prefer built-in standard code over custom code. –  Sep 24 '12 at 18:35
  • @asmo: Which is why I put that in parens :) – Jon Sep 24 '12 at 18:40
  • @asmo: is there a ___really___ good reason you can't upgrade the project? Though the answer you selected will work it seems it may be a lot easier and much more maintainable for you to continue down the path using new constrcuts. Just a thought friend. – Mike Perrenoud Sep 24 '12 at 18:52
  • @Mike We have clients which use .NET 2 and I don't have any control over that. I agree that Tuple is probably the best generic solution but I chose System.Drawing.Point instead since it better represents my case. I'd use Point even if my project was .NET 4 anyway. In any case, I won't be using KeyValuePair and I only marked Cuong's reply as the accepted answer because his explanation of the behind-the-scenes mechanisms is more complete. Thanks for your help BTW! –  Sep 24 '12 at 19:33
  • @asmo, fair enough. Good luck friend! – Mike Perrenoud Sep 24 '12 at 19:53
  • 4
    Tested Tuple, KVP, and Int64 (my answer). On a grid of 1000, 1000. Loaded the dictionary and the did a lookup on each. And the winner is Tuple! Followed by Int64 and KVP dead last. The ratio was about 1:5:15. Most machines don't have enough memory for Int32 X Int32. – paparazzo Sep 24 '12 at 20:31
  • 1
    @Mike Did not make sense to me that Tuple would be faster so dug deeper. Had a bug in my program. My pack is faster in my test. And in my test Tuple suffers from a high GetHashCode collisions. – paparazzo Sep 30 '12 at 19:36
  • @Blam, I don't fully understand this `My pack is faster in my test.`, but the high `GetHashCode` conversions for the `Tuple` don't matter because the values are actually compared. – Mike Perrenoud Oct 01 '12 at 00:21
  • See "pack". Exactly the values are compared (on a hash collision). With a 97% hash collision values are compare 97% of the time. That is 0(n). With a perfect hash values are never compared O(1). At a million O(1) is one million times faster than O(n). – paparazzo Oct 01 '12 at 01:52
  • 1
    @Blam, so in the OP's situation the `Tuple` is good because the dictionary isn't a ridiculous size, but as the dictionary grows `Tuple` becomes increasingly slower because of how it's equality comparer works? – Mike Perrenoud Oct 01 '12 at 10:59
  • 1
    @Mike Tuple is not a good key because it generates duplicate hash not because of it's equality comparer. First equality on the object will check hash and if they are not equal then it never has to check the more expensive equals. If there is a 97% has collision it has to perform the more expensive equals comparison almost every time. Dictionary and HashTable use GetHashCode for hash buckets. Bucket is O(1) but searching in the bucket is O(n). So if all the objects are in one bucket the Dictionary has been degraded to O(n). – paparazzo Oct 01 '12 at 12:40
  • @Blam, that's what the OP wanted. – Mike Perrenoud Oct 01 '12 at 12:52
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/17397/discussion-between-blam-and-mike) – paparazzo Oct 01 '12 at 13:12
  • @Blam you might want to see this too: http://www.dotnetperls.com/tuple-keyvaluepair – nawfal Jun 04 '13 at 19:29
6

Simply use a long as key and combine the two int keys

public class IntIntDict<T> : Dictionary<long, T>
{
    public void Add(int key1, int key2, T value)
    {
        Add((((long)key1) << 32) + key2, value);
    }

    //TODO: Overload other methods
}

UPDATE

C# 7 introduces the new ValueTuple Struct together with a simplified tuple syntax. These tuples come in handy for compound keys. You can declare your dictionary and add entries like this:

var myDictionary = new Dictionary<(int, int), string>();
myDictionary.Add((3, 3), "FirstItem"); 
myDictionary.Add((5, 5), "SecondItem");

and look up values like this

string result = myDictionary[(5, 5)];

or

if (myDictionary.TryGetValue((5, 7), out string result)) {
    //TODO: use result
}
Olivier Jacot-Descombes
  • 86,431
  • 10
  • 121
  • 160
  • 1
    I upvoted your answer yesterday, but seeing you updated it (after nearly 7 years!) just goes to show your level of dedication and enthusiasm. Also, this should be now the accepted answer. Thank you! – Piotr Justyna May 17 '19 at 07:23
5

For performance Dictionary requires a key that generates unique GetHashValue.

KeyValuePair is a value type and not recommended for a key.

ValueType.GetHashCode

If you call the derived type's GetHashCode method, the return value is not likely to be suitable for use as a key in a hash table. Additionally, if the value of one or more of those fields changes, the return value might become unsuitable for use as a key in a hash table. In either case, consider writing your own implementation of the GetHashCode method that more closely represents the concept of a hash code for the type.

Point is also a value value type and also not recommended for a key.
Tuple also generates a lot of duplicate GetHashCode and is not a good key.

The optimal key is one that generates unique keys.

Consider UInt16 i and UInt j as the two keys.
How can they be combined and generate unique hash?
Easy combine them into and UInt32.
UInt32 natively generates a perfect hash.

The alogorithm for packing two UInt16 into UInt32 is

(i * (UInt16.MaxValue + 1)) + j;

but it is even faster with

(UInt32)i << 16 | j;


myDictionary = new Dictionary<UInt32, string>();

With a perfect hash the Dictionary is O(1).
With a poor hash the Dictionary becomes O(n).

Liam
  • 22,818
  • 25
  • 93
  • 157
paparazzo
  • 42,665
  • 20
  • 93
  • 158
  • I read that documentation, but how do you conclude *KeyValuePair/Point are value types and not recommended for a key.*? – nawfal Jun 04 '13 at 19:40
  • @Nawfal The quote in the answer directly answers that question – bacar Aug 05 '13 at 23:36
  • "If you call the derived type's GetHashCode method, the return value is not likely to be suitable for use as a key in a hash table". I think the wording is fuzzy - I think it does mean that the derived type itself is not suitable as a key. – bacar Aug 05 '13 at 23:48
  • Using a value-type as a key is perfectly appropriate if the value type overrides `GetHashCode` and `Equals`, and if it implements `IEquatable`. A custom integer-pair value type could outperform a `Tuple` class type in the vast majority of usage scenarios, sometimes by a large margin. – supercat Dec 10 '13 at 18:02
  • @supercat But then you are not using ValueType.GetHashCode you are using a custom GetHashCode. So a Tuple can be outperformed - do I recommend Tuple? – paparazzo Dec 10 '13 at 18:16
  • @Blam: Sorry--I misread your statement as "Because it's a value type, ...", and don't know why I didn't see the last comment before mine. – supercat Dec 10 '13 at 18:44
0

UPDATE: Based on your comments to other responders, the code below answers your questions. Yes, duplicates will generate an exception, System.ArgumentException

The code you listed will work, but will not accept duplicate KeyValuePairs. System.ArgumentException or similar will be thrown if you add KeyValuePair that already exists in the dictionary.

For example, this code

using System;
using System.Collections;
using System.Collections.Generic;

namespace test{

    public class App {

        public static void Main(string[] args) {
            var myDictionary = new Dictionary<KeyValuePair<int,int>, string>(); 

            Console.WriteLine("Adding 2 items...");
            myDictionary.Add(new KeyValuePair<int,int>(3, 3), "FirstItem"); 
            myDictionary.Add(new KeyValuePair<int,int>(5, 5), "SecondItem"); 
            Console.WriteLine("Dictionary items: {0}", myDictionary.Count);

            Console.WriteLine("Adding 2 duplicate items...");
            myDictionary.Add(new KeyValuePair<int,int>(3, 3), "FirstItem"); 
            myDictionary.Add(new KeyValuePair<int,int>(5, 5), "SecondItem"); 
            Console.WriteLine("Dictionary items: {0}", myDictionary.Count);
        }
    }
}

gives the following

Microsoft (R) Visual C# Compiler version 4.0.30319.17626 for Microsoft (R) .NET Framework 4.5 Copyright (C) Microsoft Corporation. All rights reserved.

Adding 2 items... Dictionary items: 2 Adding 2 duplicate items...

Unhandled Exception: System.ArgumentException: An item with the same key has already been added. at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add) at test.App.Main(String[] args)

J Burnett
  • 1,429
  • 1
  • 9
  • 5
0

Dictionary requires an equality implementation to determine whether keys are equal. You can specify an implementation of the IEqualityComparer<T> generic interface by using a constructor that accepts a comparer parameter; if you do not specify an implementation, the default generic equality comparer EqualityComparer<T>.Default is used.

So, in your case because you don't specify IEqualityComparer<T>, Default will be used.

The EqualityComparer<T>.Default checks whether type T implements the System.IEquatable<T> interface and, if so, returns an EqualityComparer that uses that implementation. Otherwise, it returns an EqualityComparer<T> that uses the overrides of Object.Equals and Object.GetHashCode provided by T.

T is struct KeyValuePair does not implement System.IEquatable<T>, so it uses Equal and GetHashCode method of struct KeyValuePair. These two methods use both Key and Value to check equal and generate hash code:

public override int GetHashCode()
{
    return Key.GetHashCode() ^ Value.GetHashCode();
}

So, to sum up, in your sample Dictionary does not allow.

cuongle
  • 69,359
  • 26
  • 132
  • 196
-1

Using KeyValuePair as the key for your dictionary:

It will functionally work to use a KeyValuePair as the key in a dictionary; however, conceptually probably not the best choice for your application as it implies the Key-Value relationship between the two ints.

Instead as Mike suggests you should use Tuple for your key.

For the second question:

var myDictionary = new Dictionary<KeyValuePair<int,int>, string>();  
myDictionary.Add(new KeyValuePair<int,int>(3, 3), "FirstItem");  
myDictionary.Add(new KeyValuePair<int,int>(3, 3), "SecondItem");  
// does the dictionary allow this?  

The dictionary will not allow this, dictionaries themselves are sets of key-value pairs where the keys must be unique. If you want to be able to map mulitple values to the same key, one option would be to have the value be another collection:

var myDictionary = new Dictionary<KeyValuePair<int,int>, List<string>>();  

but then you would still not be able to use myDictionary.Add as in your example. instead you would have to provide additional functionality to determine if they key were part of the dictionary and act accordingly:

public static class DictionaryHelper
{

    public static void Add(this Dictionary<Tuple<int,int>, List<string>> dict,Tuple<int,int> key, string value)
    {
        if(dict.ContainsKey(key))
        {
            dict[key].Add(value);
        }
        else
        {
            dict.Add(key, new List<string>{value});
        }
    } 
}
Mr.Mindor
  • 3,869
  • 2
  • 16
  • 25