86

I'm basically looking for a way to access a hashtable value using a two-dimensional typed key in c#.

Eventually I would be able to do something like this

HashTable[1][false] = 5;
int a = HashTable[1][false];
//a = 5

This is what I've been trying...hasn't worked

Hashtable test = new Hashtable();
test.Add(new Dictionary<int, bool>() { { 1, true } }, 555);
Dictionary<int, bool> temp = new Dictionary<int, bool>() {{1, true}};
string testz = test[temp].ToString(); 
Scott Schulthess
  • 2,853
  • 2
  • 27
  • 35

16 Answers16

82

I think a better approach is to encapsulate the many fields of your multi-dimensional key into a class / struct. For example

struct Key {
  public readonly int Dimension1;
  public readonly bool Dimension2;
  public Key(int p1, bool p2) {
    Dimension1 = p1;
    Dimension2 = p2;
  }
  // Equals and GetHashCode ommitted
}

Now you can create and use a normal HashTable and use this wrapper as a Key.

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 10
    Don't forget you need to override GetHashCode and Equals to use this in a Hashtable. – David M Mar 27 '09 at 14:28
  • 7
    @David, not in this case. The default implementation of Equals will just perform equality on all of the fields which will work in this case. GetHashcode may not be as effiecient as the user would like but it will also function with the default implementation. – JaredPar Mar 27 '09 at 14:35
  • 6
    @David, that being said, it's usually good practice to actually do so. – JaredPar Mar 27 '09 at 14:51
  • 6
    @JaredPar Doing so would be worthwhile. We recently uncovered a performance issue with the default implementation of `GetHashCode` for `struct` types. Manual implementation completely removed this bottleneck. Also, though a different solution, we find that the "dictionary within a dictionary" approach operates faster on all common actions (`Dictionary>`). However, this doesn't allow for null portions of the composite key, whereas a `struct`/`class` key as above could easily allow for nulls without any extra legwork. – Adam Houldsworth Jan 09 '13 at 15:14
  • This works, but as @Adam mentions it is worth bearing in mind that the performance of struct's default `GetHashCode` _might_ be terrible (or it might be just fine). Either way it will be _correct_ (i.e. consistent with `Equals()`). Some structs, such as `KeyValuePair`, [appear to use no fields from the struct at all](http://stackoverflow.com/a/12659468/37941) and always return the same hash value - such keys would give you O(n) Dictionary lookup. Just be aware that struct isn't a panacea and be aware that you _may_ eventually have to implement your own hashcode. – bacar Aug 06 '13 at 21:39
  • @bacar i didn't feel your comments were unhelpful at all. It is important to understand that this will create hash codes in a predetermined way that may very well be terrible for your application. If profiling showed this was type was causing a lot of bucket collisions that would absolutely be the correct place to go – JaredPar Aug 06 '13 at 21:40
  • Because the default implementations of `GetHashCode` & `Equals` are so slow and prone to hash-collisions, I've implemented [ValueUtils](https://github.com/EamonNerbonne/ValueUtils) which uses runtime code generation to efficiently implement those for you - that makes this approach a lot more palatable (in particular it addresses @bacar's problems). – Eamon Nerbonne Jun 01 '14 at 14:04
76

You can do this in C# 7.0 now with the new tuples:

// Declare
var test = new Dictionary<(int, bool), int>();

// Add
test.Add((1, false), 5);

// Get
int a = test[(1, false)];
Huseyin Yagli
  • 9,896
  • 6
  • 41
  • 50
  • 4
    Note: To use this on an older .NET Target Framework, an [additional nuget package](https://stackoverflow.com/a/38383064/3009574) is needed. – pogosama Jul 12 '18 at 10:34
  • 1
    Note: This also works in VB.NET as of [Visual Basic 2017 (15.x)](https://learn.microsoft.com/en-us/dotnet/visual-basic/getting-started/whats-new#visual-basic-2017) – Keith Stein May 20 '20 at 22:14
24

How about using a regular Dictionary with some kind of Tuple structure as a key?

public class TwoKeyDictionary<K1,K2,V>
{
    private readonly Dictionary<Pair<K1,K2>, V> _dict;

    public V this[K1 k1, K2 k2]
    {
        get { return _dict[new Pair(k1,k2)]; }
    }

    private struct Pair
    {
        public K1 First;
        public K2 Second;

        public override Int32 GetHashCode()
        {
            return First.GetHashCode() ^ Second.GetHashCode();
        }

        // ... Equals, ctor, etc...
    }
}
21

Just in case anyone is here recently, an example of how to do this the quick and dirty way in .Net 4.0, as described by one of the commenters.

class Program
{
  static void Main(string[] args)
  {
     var twoDic = new Dictionary<Tuple<int, bool>, String>();
     twoDic.Add(new Tuple<int, bool>(3, true), "3 and true." );
     twoDic.Add(new Tuple<int, bool>(4, true), "4 and true." );
     twoDic.Add(new Tuple<int, bool>(3, false), "3 and false.");

     // Will throw exception. Item with the same key already exists.
     // twoDic.Add(new Tuple<int, bool>(3, true), "3 and true." );

     Console.WriteLine(twoDic[new Tuple<int, bool>(3,false)]);
     Console.WriteLine(twoDic[new Tuple<int, bool>(4,true)]);
     // Outputs "3 and false." and "4 and true."
  }
}
Parrhesia Joe
  • 1,327
  • 12
  • 12
21

I think this might be closer to what you're looking for...

var data = new Dictionary<int, Dictionary<bool, int>>();
Restore the Data Dumps
  • 38,967
  • 12
  • 96
  • 122
  • 1
    This has some advantages. It is clearer in use ```citiesByState["wa"]["seattle"]``` is beautiful code. Note, though, it has to do two hash compares and isEquals per lookup. A single dictionary with a composite (tuple) key may be significantly faster, when performance is critical. I generally prefer the form in this answer because of the elegance, but 7.0 makes tuples a lot less ugly. – Parrhesia Joe Jan 09 '19 at 01:41
6

I'd suggest a slight variation on jachymko's solution which will allow you to avoid creating a class for key pairs. Instead, wrap a private dictionary of dictionaries, as so:

public class MultiDictionary<K1, K2, V>
{
    private Dictionary<K1, Dictionary<K2, V>> dict = 
        new Dictionary<K1, Dictionary<K2, V>>();

    public V this[K1 key1, K2 key2]
    {
        get
        {
            return dict[key1][key2];
        }

        set
        {
            if (!dict.ContainsKey(key1))
            {
                dict[key1] = new Dictionary<K2, V>();
            }
            dict[key1][key2] = value;
        }
    }
}
  • 3
    Why? I thought about this for a second, but it seems to me worse because it has greater overhead (more GC pressure, worse locality) and there's no benefit to it - searching in a hash table has constant time-efficiency in average, so there's no point in doing it twice. –  Mar 27 '09 at 22:01
  • 4
    Not for any efficiency reasons, but because I think the code is simpler (look at what you had to elide). If efficiency is a prime concern, then I agree that your original solution is better. I dislike having types like Pair around, especially when .NET 4.0 will include tuples out-of-the-box. –  Apr 01 '09 at 23:44
  • 2
    @jachymko If efficiency is defined as speed as opposed to memory usage, then the straight dictionary within a dictionary solution using simpler types actually tends to perform faster. Using `struct` or other type-created compound keys causes a performance drop for reasons I've yet to determine. – Adam Houldsworth Jan 09 '13 at 15:21
  • This is exactly what i was looking for with my 5 dimensions array. thank you. Sadly, can anyone just explain me how to use it when i want to SET values and GET them too?... – user3916429 Aug 25 '15 at 02:17
  • Incase it helps someone else.... **initialization like:** `MultiDictionary arrayName = new MultiDictionary();` **set:** `arrayName[key1,key2] = value;` **get:** `string value = arrayName[key1,key2];` – user3916429 Aug 25 '15 at 13:31
  • 1
    This method also allows the `MultiDictionary` class to return all keys in the 2nd dimension easily. Say you had a dictionary of states->cities->people, and you wanted to use the one data structure to be able to get back both all cites in a given state, and all people in a given state/city pair. – wizard07KSU Mar 16 '17 at 19:18
4

You need a key class for the Dictonary that implements GetHashCode correctly. And you can extend Dictonary to let you access it in a friendly way.

The KeyPair class:

public class KeyPair<Tkey1, Tkey2>
{
    public KeyPair(Tkey1 key1, Tkey2 key2)
    {
        Key1 = key1;
        Key2 = key2;
    }

    public Tkey1 Key1 { get; set; }
    public Tkey2 Key2 { get; set; }

    public override int GetHashCode()
    {
        return Key1.GetHashCode() ^ Key2.GetHashCode();
    }
    public override bool Equals(object obj)
    {
        KeyPair<Tkey1, Tkey2> o = obj as KeyPair<Tkey1, Tkey2>;
        if (o == null)
            return false;
        else
            return Key1.Equals(o.Key1) && Key2.Equals(o.Key2);
    }
}

Extend Dictonary<>:

public class KeyPairDictonary<Tkey1, Tkey2, Tvalue> 
    : Dictionary<KeyPair<Tkey1, Tkey2>, Tvalue>
{
    public Tvalue this[Tkey1 key1, Tkey2 key2]
    {
        get
        {
            return this[new KeyPair<Tkey1, Tkey2>(key1, key2)];
        }
        set
        {
            this[new KeyPair<Tkey1, Tkey2>(key1, key2)] = value;
        }
    }
}

You can use it like this:

KeyPairDictonary<int, bool, string> dict = 
    new KeyPairDictonary<int, bool, string>();

dict[1, false] = "test";
string test = dict[1, false];
Eliahu Aaron
  • 4,103
  • 5
  • 27
  • 37
AndreasN
  • 2,881
  • 3
  • 24
  • 29
1

Essentially you need to use an embedded hashtable. If you think about your question, a hashtable with two keys is a function with two independent variables, and f(x,y) is 2-dimensional by definition.

But you want to use it like it were one hashtable, and not embedded hashes. So what you need to do is create an object that wraps around that embedded hashtable idea and operates like a single hash.

A couple of snags:

  • You want to iterate over it, so you need to overwrite the GetEnumerator() method. And you need your own Iterator that will iterate correctly in 2 dimensions.
  • You need to do more checking to be sure that there are no duplicates.

I have included my code to do it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Windows.Forms;

namespace YourProjectNameHere
{
    public class Hashtable2D
    {
        /// <summary>
        /// This is a hashtable of hashtables
        /// The X dim is the root key, and the y is the internal hashes key
        /// </summary>
        /// 
        private Hashtable root = new Hashtable();
        public bool overwriteDuplicates = false;
        public bool alertOnDuplicates = true;

        public void Add(object key_x, object key_y, object toStore)
        {
            if(root[key_x]!=null)//If key_x has already been entered 
            {
                Hashtable tempHT = (Hashtable)root[key_x];//IF the hash table does not exist then focus will skip to the catch statement
                if (tempHT[key_y] == null)  tempHT.Add(key_y, toStore);
                else handleDuplicate(tempHT, key_y, toStore);
            }else{//Making a new hashtable 
                Hashtable tempHT = new Hashtable();
                tempHT.Add(key_y, toStore);
                root.Add(key_x, tempHT);
            }

        }

        public void Remove(object key_x, object key_y)
        {
            try{
                ((Hashtable)root[key_x]).Remove(key_y);
            }catch(Exception e){
                MessageBox.Show("That item does not exist");
            }

        }

        public void handleDuplicate (Hashtable tempHT, object key_y, object toStore)
        {
            if (alertOnDuplicates) MessageBox.Show("This Item already Exists in the collection");

            if (overwriteDuplicates)
            {
                tempHT.Remove(key_y);
                tempHT.Add(key_y,toStore);
            }
        }

        public object getItem(object key_x, object key_y)
        {
            Hashtable tempHT = (Hashtable)root[key_x];
            return tempHT[key_y];
        }

        public ClassEnumerator GetEnumerator()
        {
            return new ClassEnumerator(root);
        }

        public class ClassEnumerator : IEnumerator
        {
            private Hashtable ht;
            private IEnumerator iEnumRoot;
            private Hashtable innerHt;
            private IEnumerator iEnumInner;

            public ClassEnumerator(Hashtable _ht)
            {
                ht = _ht;
                iEnumRoot = ht.GetEnumerator();

                iEnumRoot.MoveNext();//THIS ASSUMES THAT THERE IS AT LEAST ONE ITEM

                innerHt = (Hashtable)((DictionaryEntry)iEnumRoot.Current).Value;
                iEnumInner = innerHt.GetEnumerator();
            }

            #region IEnumerator Members

            public void Reset()
            {
                iEnumRoot = ht.GetEnumerator();
            }

            public object Current
            {
                get
                {
                    return iEnumInner.Current; 
                }
            }

            public bool MoveNext()
            {
                if(!iEnumInner.MoveNext())
                {
                    if (!iEnumRoot.MoveNext()) return false;
                    innerHt = (Hashtable)((DictionaryEntry)iEnumRoot.Current).Value;
                    iEnumInner = innerHt.GetEnumerator();
                    iEnumInner.MoveNext();
                }
                return true;
            }

            #endregion
        }

    }
}
Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
Jim
  • 11
  • 1
1

I would suggest that you create a small custom class exposing the bool and int properties, and override its GetHashCode and Equals methods, then use this as the key.

David M
  • 71,481
  • 13
  • 158
  • 186
0

Look, this code works just fine:

    public Form1()
    {
            InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {

        this.Services = new Dictionary<object, Hashtable>();
        this.Services.Add("array1", new Hashtable());

        this.Services["array1"]["qwe"] = "123";
        this.Services["array1"][22] = 223;

        object zz = null;
        zz = this.Services["array1"]["qwe"];
        MessageBox.Show(zz.ToString()); // shows qwe

        zz = this.Services["array1"][22];
        MessageBox.Show(zz.ToString()); // shows 22
    }

Now we just need a wrapper to avoid manually doing this.Services.Add("array1", new Hashtable());

0

I think the easiest way to do it now is to use Tupple.Create and ValueTuple.Create:

> var k1 =  Tuple.Create("test", int.MinValue, DateTime.MinValue, double.MinValue);
> var k2 = Tuple.Create("test", int.MinValue, DateTime.MinValue, double.MinValue);
> var dict = new Dictionary<object, object>();
> dict.Add(k1, "item");
> dict.Add(k2, "item");
An item with the same key has already been added....
> dict[k1] == dict[k2]
true

or use new c#7 tuple syntax to create tuple-keys:

var k = (item1: "value1", item2: 123);
SalientBrain
  • 2,431
  • 16
  • 18
0

This is my nested Dictionary implementation:

public class TwoKeysDictionary<K1, K2, T>:
        Dictionary<K1, Dictionary<K2, T>>
{
    public T this[K1 key1, K2 key2]
    {
        get => base.ContainsKey(key1) && base[key1].ContainsKey(key2) ? base[key1][key2] : default;
        set
        {
            if (ContainsKey(key1) && base[key1].ContainsKey(key2))
                base[key1][key2] = value;
            else
                Add(key1, key2, value);
        }
    }

    public void Add(K1 key1, K2 key2, T value)
    {
        if (ContainsKey(key1))
        {
            if (base[key1].ContainsKey(key2))
                throw new Exception("Couple " + key1 + "/" + key2 + " already exists!");
            base[key1].Add(key2, value);
        }
        else
            Add(key1, new Dictionary<K2, T>() { { key2, value } });
    }

    public bool ContainsKey(K1 key1, K2 key2) => ContainsKey(key1) && base[key1].ContainsKey(key2);
}
tedebus
  • 978
  • 13
  • 20
0

You might be able to "double-nest" your hashtables - in other words, your main Dictionary is of type Dictionary<int, Dictionary<bool, my_return_type>>.

That accomplishes your goal of being able to use the double bracket notation in your first code snippet.

Of course, the management side is a little trickier. Every time you add an entry, you need to test if the main dictionary contains a dictionary for the primary key, and add a new dictionary if not, then add the secondary key and value to the inner Dictionary.

Mike
  • 4,542
  • 1
  • 26
  • 29
0

Could you use a Dictionary<KeyValuePair<int,bool>,int>?

Andrew Hare
  • 344,730
  • 71
  • 640
  • 635
0

Wrap your two-dimensional key in a separate type and use that type as a key. Also consider overriding GetHashCode() and Equals() methods. Preferably use Dictionary<> instead of HashTable since apparently you can use that.

bruno conde
  • 47,767
  • 15
  • 98
  • 117
0

A quick and dirty way would be to create a composite key from the two pieces of information, e.g.

IDictionary<string, int> values = new Dictionary<string, int>();
int i = ...;
bool b = ...;
string key = string.Concat(i, '\0', b);
values[key] = 555;

To encapsulate this a bit better you could wrap the dictionary:

public class MyDict
{
    private readonly IDictionary<string, int> values = new Dictionary<string, int>();

    public int this[int i, bool b]
    {
        get
        {
            string key = BuildKey(i, b);
            return values[key];
        }

        set
        {
            string key = BuildKey(i, b);
            values[key] = value;
        }
    }

    private static string BuildKey(int i, bool b)
    {
        return string.Concat(i, '\0', b);
    }
}

To make this more robust, encapsulate the composite key as a type, e.g. a class that contains the two fields, ensuring you override the Equals() and GetHashCode() methods correctly.

Paul Ruane
  • 37,459
  • 12
  • 63
  • 82
  • The two parts of the key. In the original post, this was the integer 1 and the boolean false, hence I have concatenated these two in the sample code. (The delimiter is not strictly necessary in this example.) – Paul Ruane Mar 27 '09 at 15:36