15

I was wondering if there was a built in type in C# that was like 'Dictionary' but where both TKey and TValue had to be unique.

For example::

d.Add(1, "1");
d.Add(2, "1"); // This would not be OK because "1" has already been used as a value.

I know this is kind of exotic, but it seems that since there are about a billion collection types in the BCL it might exist. Any ideas?

A.R.
  • 15,405
  • 19
  • 77
  • 123
  • 3
    Make the value part of the key. – Robert Harvey Feb 24 '12 at 21:09
  • 3
    There is no such class in the .NET Framework. But you can easily construct one out of a Dictionary and a HashSet, or two Dictionaries. – dtb Feb 24 '12 at 21:10
  • 1
    @Robert Harvey: if he does that he can't do `d[1]` any longer, which defeats the purpose of Dictionary. Might as well use HashSet<> – THX-1138 Feb 24 '12 at 21:10
  • @RobertHarvey: If you have only one half of the key, how would you retrieve the other half? – dtb Feb 24 '12 at 21:11
  • See http://stackoverflow.com/a/5853223/59303 - you have to store an extra `char` but a `Tuple` might work for you. Though this does suffer from the same problem as mentioned in the other comments. – ChrisF Feb 24 '12 at 21:11
  • @dtb Sorry, we're just thinking the same things – Oleg Dok Feb 24 '12 at 21:13
  • @OlegDok: I'd expect more from an answer than just "use two dictionaries", so I posted only a comment. – dtb Feb 24 '12 at 21:15
  • btw, this data structure is called bidirectional map – Display Name Apr 12 '14 at 13:49
  • if you need only fast "already there?" test, you can use `HashSet>`, where T1 and T2 are your key and value types otherwise search the web for library implementations – Display Name Apr 12 '14 at 13:51

5 Answers5

18

How about having Dictionary and HashSet/secondary reverse Dictionary - it will solve the issue and will perform better than checks on single Dictionary.

Something like this, wrapped as class:

HashSet<string> secondary = new HashSet<string>(/*StringComparer.InvariantCultureIgnoreCase*/);
Dictionary<int, string>dictionary = new Dictionary<int, string>();
object syncer = new object();

public override void Add(int key, string value)
{
  lock(syncer)
  {
    if(dictionary.ContainsKey(key))
    {
      throw new Exception("Key already exists");
    }

    if(secondary.Add(value)
    {
      throw new Exception("Value already exists");
    }
    dictionary.Add(key, value);
  }
}
Oleg Dok
  • 21,109
  • 4
  • 45
  • 54
1

I solved this issue by storing the data as Dictionary<TKey, HashSet<TValue>>. You can replace the HashSet by another Dictionary if you want a value that has 2 primary keys.

Dictionary<int, HashSet<int>> _myUniquePairOfIntegerKeys;
// OR
Dictionary<string, Dictionary<string, bool>> _myUniquePairOfStringKeysWithABooleanValue;
1

For internal puposes I wrote a BiDictionary. It isn't bullet-proof by I don't expose it to the user so it works fine for me. It allows to me get either key as I ofter need to.

The KeyPair<,> is necessary to be able to implement the IEnumerable<,> and thus the Add method so that we can use the object initializer.

internal class KeyPair<TKey1, TKey2>
{
    public TKey1 Key1 { get; set; }
    public TKey2 Key2 { get; set; }
}

This is the main class as a dynamic object so that we can use key names on it when retrieving values:

internal class BiDictionary<TKey1, TKey2> : DynamicObject, IEnumerable<KeyPair<TKey1, TKey2>>
{
    private readonly Dictionary<TKey1, TKey2> _K1K2 = new Dictionary<TKey1, TKey2>();
    private readonly Dictionary<TKey2, TKey1> _K2K1 = new Dictionary<TKey2, TKey1>();

    private readonly string _key1Name;
    private readonly string _key2Name;

    public BiDictionary(string key1Name, string key2Name)
    {
        _key1Name = key1Name;
        _key2Name = key2Name;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (binder.Name == _key1Name)
        {
            result = _K1K2;
            return true;
        }

        if (binder.Name == _key2Name)
        {
            result = _K2K1;
            return true;
        }

        result = null;
        return false;
    }

    public void Add(TKey1 key1, TKey2 key2)
    { 
        _K1K2.Add(key1, key2);
        _K2K1.Add(key2, key1);
    }

    public IEnumerator<KeyPair<TKey1, TKey2>> GetEnumerator()
    {
        return _K1K2.Zip(_K2K1, (d1, d2) => new KeyPair<TKey1, TKey2>
        {
            Key1 = d1.Key,
            Key2 = d2.Key
        }).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Example:

dynamic bidic = new BiDictionary<string, string>("Key1", "Key2") 
{ 
    { "foo", "bar" }, 
    { "baz", "qux" } 
};
var bar = bidic.Key1["foo"];
var foo = bidic.Key2["bar"];

They may go out of sync if you modify any of the dictionaries outside. For this purpose I use ObservableDictionary so that I can update the other one if one changes but for the sake of simplicity I removed this part of the code to just demostrate the main logic.

t3chb0t
  • 16,340
  • 13
  • 78
  • 118
0

There is a project located here that has a type like this. It is called PairDictionary and it works pretty well. Not the best answer, but for anyone who needs that custom class.

A.R.
  • 15,405
  • 19
  • 77
  • 123
  • The `PairDictionary` is a terrible solution. Internally it works with lists and each operation is a O(n) operation unlike the O(1) of the _real_ dictionary --- [source](http://curations.codeplex.com/SourceControl/latest#Curations/PairDictionary.cs) – t3chb0t Feb 13 '16 at 15:27
  • @t3chb0t You are focusing on the implementation and not the API. Yeah, it's not the best, buts its usage is what is important. Since that is part of some open source code, you should submit a patch that improves the functionality :) – A.R. Feb 15 '16 at 13:36
0

You could implement an extension method that skips adding to the dictionary if a given value already exists.

static class Extensions
{
    public static void AddSafe(this Dictionary<int, string> dictionary, int key, string value)
    {
        if (!dictionary.ContainsValue(value))
            dictionary.Add(key, value);
    }
}

Call it like a normal function

d.AddSafe(1, "1");
d.AddSafe(2, "1"); // This would not add anything
live627
  • 397
  • 4
  • 18