4
\$\begingroup\$

Inspired by this question and its answers, I've made my own version.

 /// <summary>
 /// A one-one relation bidirectional map.
 /// <para>
 /// A one-one relation means that each entry of type <typeparamref name="TFirst"/> can correspond to exactly one 
 /// entry of type <typeparamref name="TSecond"/> and visa versa.
 /// </para>
 /// The map doesn't support null objects because each element is both key and value in its relation and keys can't be null.
 /// </summary>
 /// <typeparam name="TFirst">Any type</typeparam>
 /// <typeparam name="TSecond">Any type</typeparam>
 public class BidirectionalMap<TFirst, TSecond> : IEnumerable<KeyValuePair<TFirst, TSecond>>
 {
 private readonly Dictionary<TFirst, TSecond> primary;
 private readonly Dictionary<TSecond, TFirst> secondary;
 public BidirectionalMap()
 {
 primary = new Dictionary<TFirst, TSecond>();
 secondary = new Dictionary<TSecond, TFirst>();
 }
 /// <summary>
 /// Creates a BidirectionalMap initialized with the specified <paramref name="capacity"/>.
 /// </summary>
 /// <param name="capacity">The desired capacity for the map.</param>
 /// <exception cref="ArgumentOutOfRangeException">If capacity is out of range (&lt; 0)</exception>
 public BidirectionalMap(int capacity)
 {
 primary = new Dictionary<TFirst, TSecond>(capacity);
 secondary = new Dictionary<TSecond, TFirst>(capacity);
 }
 /// <summary>
 /// Creates a BidirectionalMap with the specified equality comparers.
 /// </summary>
 /// <param name="firstComparer">Equality comparer for <typeparamref name="TFirst"/>. If null, the default comparer is used.</param>
 /// <param name="secondComparer">Equality comparer for <typeparamref name="TSecond"/>. If null, the default comparer is used.</param>
 public BidirectionalMap(IEqualityComparer<TFirst> firstComparer, IEqualityComparer<TSecond> secondComparer)
 {
 primary = new Dictionary<TFirst, TSecond>(firstComparer);
 secondary = new Dictionary<TSecond, TFirst>(secondComparer);
 }
 /// <summary>
 /// Creates a BidirectionalMap from the <paramref name="source"/> dictionary.
 /// </summary>
 /// <param name="source">The source dictionary from which to create a one-one relation map</param>
 /// <exception cref="ArgumentException">If <paramref name="source"/> contains doublets in values</exception>
 /// <exception cref="ArgumentNullException">If <paramref name="source"/> contains null values</exception>
 public BidirectionalMap(IDictionary<TFirst, TSecond> source)
 {
 primary = new Dictionary<TFirst, TSecond>(source);
 secondary = source.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
 }
 /// <summary>
 /// Creates a BidirectionalMap from the <paramref name="inverseSource"/> dictionary.
 /// </summary>
 /// <param name="inverseSource">The source dictionary from which to create a one-one relation map</param>
 /// <exception cref="ArgumentException">If <paramref name="inverseSource"/> contains doublets in values</exception>
 /// <exception cref="ArgumentNullException">If <paramref name="inverseSource"/> contains null values</exception>
 public BidirectionalMap(IDictionary<TSecond, TFirst> inverseSource)
 {
 primary = inverseSource.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
 secondary = new Dictionary<TSecond, TFirst>(inverseSource);
 }
 public int Count => primary.Count;
 public ICollection<TFirst> PrimaryKeys => primary.Keys;
 public ICollection<TSecond> SecondaryKeys => secondary.Keys;
 // This should be useful only for enumeration by TSecond as key
 public IReadOnlyDictionary<TSecond, TFirst> Inverse => secondary;
 public TSecond this[TFirst first]
 {
 get { return primary[first]; }
 set { Set(first, value); }
 }
 public TFirst this[TSecond second]
 {
 get { return secondary[second]; }
 set { Set(value, second); }
 }
 private void Set(TFirst first, TSecond second)
 {
 // Remove both the entries related to first and second if any
 Remove(first);
 Remove(second);
 // Now it should be safe to add the new relation.
 Add(first, second);
 }
 public void Add(TFirst first, TSecond second)
 {
 try
 {
 primary.Add(first, second);
 }
 catch (ArgumentNullException)
 {
 // If first is null, we end here and can rethrow with no harm done.
 throw new ArgumentNullException(nameof(first));
 }
 catch (ArgumentException)
 {
 // If the key is present in primary, then we end here and can rethrow with no harm done.
 throw new ArgumentException(nameof(first), $"{first} already present in the dictionary");
 }
 try
 {
 secondary.Add(second, first);
 }
 catch (ArgumentNullException)
 {
 // If second is null, we end here, and primary must be rolled back - because first was added successfully
 primary.Remove(first);
 throw new ArgumentNullException(nameof(second));
 }
 catch (ArgumentException)
 {
 // If second exists in secondary, secondary throws, and primary must be rolled back - because first was added successfully
 primary.Remove(first);
 throw new ArgumentException(nameof(second), $"{second} already present in the dictionary");
 }
 }
 public bool Remove(TFirst first)
 {
 if (primary.TryGetValue(first, out var second))
 {
 secondary.Remove(second);
 primary.Remove(first);
 return true;
 }
 return false;
 }
 public bool Remove(TSecond second)
 {
 if (secondary.TryGetValue(second, out var first))
 {
 primary.Remove(first);
 secondary.Remove(second);
 return true;
 }
 return false;
 }
 public bool TryGetValue(TFirst first, out TSecond second)
 {
 return primary.TryGetValue(first, out second);
 }
 public bool TryGetValue(TSecond second, out TFirst first)
 {
 return secondary.TryGetValue(second, out first);
 }
 public bool Contains(TFirst first)
 {
 return primary.ContainsKey(first);
 }
 public bool Contains(TSecond second)
 {
 return secondary.ContainsKey(second);
 }
 public void Clear()
 {
 primary.Clear();
 secondary.Clear();
 }
 public IEnumerator<KeyValuePair<TFirst, TSecond>> GetEnumerator()
 {
 return primary.GetEnumerator();
 }
 IEnumerator IEnumerable.GetEnumerator()
 {
 return GetEnumerator();
 }
 }

It doesn't implement IDictionary<T, S> for either directions, but has just about the same public interface for both. The directions should therefore be regarded as equal.


Here are a set of unit tests - not complete but covering the most parts:

 [TestClass]
 public class BidirectionalMapTests
 {
 class TestObject<T>
 {
 public TestObject(T value)
 {
 Value = value;
 }
 public T Value { get; }
 public static implicit operator T(TestObject<T> to) => to.Value;
 public static implicit operator TestObject<T>(T value) => new TestObject<T>(value);
 public override string ToString()
 {
 return Value?.ToString() ?? "";
 }
 }
 [TestMethod]
 public void InitializeFromSourceDictionary()
 {
 Dictionary<string, int> source = new Dictionary<string, int>
 {
 { "a", 1 },
 { "b", 2 }
 };
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>(source);
 Assert.AreEqual(1, map["a"]);
 Assert.AreEqual("b", map[2]);
 }
 [TestMethod]
 public void InvalidInitializeFromSourceDictionary()
 {
 TestObject<string> one = new TestObject<string>("1");
 Dictionary<string, TestObject<string>> source = new Dictionary<string, TestObject<string>>
 {
 { "a", one },
 { "b", one }
 };
 BidirectionalMap<string, TestObject<string>> map = null;
 Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));
 Dictionary<TestObject<string>, string> inverseSource = new Dictionary<TestObject<string>, string>
 {
 { "a", "1" },
 { "b", "1" }
 };
 Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(inverseSource));
 source = new Dictionary<string, TestObject<string>>
 {
 { "a", null },
 { "b", "1" }
 };
 Assert.ThrowsException<ArgumentNullException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));
 }
 [TestMethod]
 public void Add()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 Assert.AreEqual(1, map["a"]);
 Assert.AreEqual("b", map[2]);
 Assert.AreEqual(2, map.Count);
 }
 [TestMethod]
 public void InvalidAdd()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 Assert.ThrowsException<ArgumentException>(() => map.Add("a", 2));
 Assert.ThrowsException<ArgumentException>(() => map.Add("b", 1));
 Assert.AreEqual(1, map["a"]);
 }
 [TestMethod]
 public void AddNull()
 {
 BidirectionalMap<string, string> map = new BidirectionalMap<string, string>();
 Assert.ThrowsException<ArgumentNullException>(() => map.Add(null, "a"));
 Assert.ThrowsException<ArgumentNullException>(() => map.Add("a", null));
 Assert.AreEqual(0, map.Count);
 }
 [TestMethod]
 public void Remove()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 Assert.AreEqual(2, map.Count);
 map.Remove("a");
 Assert.AreEqual(1, map.Count);
 map.Remove(2);
 Assert.AreEqual(0, map.Count);
 }
 [TestMethod]
 public void RemoveNonExistingValue()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 Assert.IsFalse(map.Remove("c"));
 Assert.AreEqual(2, map.Count);
 }
 [TestMethod]
 public void Set()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 map["a"] = 3;
 Assert.AreEqual(2, map.Count);
 Assert.IsTrue(map.TryGetValue("a", out int second));
 Assert.AreEqual(3, second);
 Assert.IsTrue(map.TryGetValue(3, out string first));
 Assert.AreEqual("a", first);
 Assert.IsFalse(map.TryGetValue(1, out _));
 }
 [TestMethod]
 public void SetWithExistingSecondValue()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 map["a"] = 2;
 Assert.AreEqual(1, map.Count);
 Assert.IsTrue(map.TryGetValue("a", out int second));
 Assert.AreEqual(2, second);
 Assert.IsTrue(map.TryGetValue(2, out string first));
 Assert.AreEqual("a", first);
 Assert.IsFalse(map.TryGetValue("b", out _));
 }
 [TestMethod]
 public void TryGetValue()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>
 {
 { "a", 1 },
 { "b", 2 }
 };
 Assert.IsTrue(map.TryGetValue("a", out int second));
 Assert.AreEqual(1, second);
 Assert.IsTrue(map.TryGetValue(2, out string first));
 Assert.AreEqual("b", first);
 Assert.IsFalse(map.TryGetValue("c", out _));
 Assert.IsFalse(map.TryGetValue(3, out _));
 }
 }

Any comments are welcome, but the implementation of Add(), Set() and Remove() are the most vulnerable parts.

I think that the naming TFirst, TSecond, primary and secondary could be better in order to reflect their equal status, but I didn't find any better. May be you have any suggestions?

asked Sep 4, 2019 at 16:32
\$\endgroup\$
4
  • 2
    \$\begingroup\$ This solution is an example of how enthusiasm can make one blind about the obvious pitfalls - as mentioned by Peter in his answer - embarrassing. \$\endgroup\$ Commented Sep 5, 2019 at 8:18
  • 2
    \$\begingroup\$ I know this feeling. Code Review is sometimes cruel ;-P \$\endgroup\$ Commented Sep 5, 2019 at 8:32
  • \$\begingroup\$ for the naming: en.wikipedia.org/wiki/Bijection they mention X and Y. But I'm not sure we should use these mathematical names as member names in C#. What do you think? \$\endgroup\$ Commented Sep 5, 2019 at 11:44
  • \$\begingroup\$ @dfhwze: I don't like single char names, but they are at least neutral. I'll consider that. \$\endgroup\$ Commented Sep 5, 2019 at 12:14

3 Answers 3

4
\$\begingroup\$

If TFirst and TSecond are the same, nearly all of the API becomes useless because the compiler can't disambiguate the method calls.

I think a better design would be for Inverse to be a BidirectionalMap<TSecond, TFirst>, so that the methods don't need to be duplicated. Then one obvious unit test would be ReferenceEquals(map, map.Inverse.Inverse). And I think the obstacle to implementing at least IReadOnlyDictionary<TFirst, TSecond> would have been removed.


Add seems overly complicated and slightly fragile. I don't think the tiny performance improvement justifies the complexity over

 public void Add(TFirst first, TSecond second)
 {
 if (first == null) throw new ArgumentNullException(nameof(first));
 if (primary.ContainsKey(first)) throw new ArgumentException(nameof(first), $"{first} already present in the dictionary");
 if (second == null) throw new ArgumentNullException(nameof(second));
 if (secondary.ContainsKey(second)) throw new ArgumentException(nameof(second), $"{second} already present in the dictionary");
 primary.Add(first, second);
 secondary.Add(second, first);
 }
answered Sep 5, 2019 at 7:52
\$\endgroup\$
2
  • \$\begingroup\$ Oh, I just realized that you mentioned the same bug but with different examples hehe. \$\endgroup\$ Commented Sep 5, 2019 at 8:10
  • 1
    \$\begingroup\$ @HenrikHansen then let it be a secret review ;) \$\endgroup\$ Commented Sep 5, 2019 at 8:21
4
\$\begingroup\$

Set and Remove are compact and clean code. Add is convoluted with these exception handlers:

 try
 {
 primary.Add(first, second);
 }
 catch (ArgumentNullException)
 {
 // If first is null, we end here and can rethrow with no harm done.
 throw new ArgumentNullException(nameof(first));
 }
 catch (ArgumentException)
 {
 // If the key is present in primary, then we end here and can rethrow with no harm done.
 throw new ArgumentException(nameof(first), $"{first} already present in the dictionary");
 }

I would opt for calling sand-box methods TryAdd and Remove instead:

public void Add(TFirst first, TSecond second)
{
 if (!primary.TryAdd(first, second) && !secondary.TryAdd(second, first))
 {
 primary.Remove(first);
 throw new InvalidOperationException("The tuple violates the bijective constraint");
 }
}

You can make the body a bit more verbose if you which to notify the caller which of the arguments violates the constraint.

public void Add(TFirst first, TSecond second)
{
 if (!primary.TryAdd(first, second))
 {
 throw new InvalidOperationException("The first arg violates the bijective constraint");
 }
 if (!secondary.TryAdd(second, first))
 {
 primary.Remove(first);
 throw new InvalidOperationException("The second arg violates the bijective constraint");
 }
}

You could even check for a combination if you really need to:

public void Add(TFirst first, TSecond second)
{
 var primaryAdded = primary.TryAdd(first, second);
 var secondaryAdded = secondary.TryAdd(second, first);
 if (primaryAdded && !secondaryAdded)
 {
 primary.Remove(first);
 throw new InvalidOperationException("The second arg violates the bijective constraint");
 }
 if (!primaryAdded && secondaryAdded)
 {
 secondary.Remove(second);
 throw new InvalidOperationException("The first arg violates the bijective constraint");
 }
 if (!primaryAdded && !secondaryAdded)
 {
 throw new InvalidOperationException("Both args violate the bijective constraint");
 }
}
answered Sep 5, 2019 at 11:30
\$\endgroup\$
1
  • 1
    \$\begingroup\$ I didn't know about TryAdd() as it is still in preview for .NET Standard. But I could maybe make an extension. My argument for the convoluted approach was to let the dictionaries handle invalid input for performance reasons. Personally I don't find it that convoluted - the workflow and logic is easy followed IMO. I'll make a self-answer later in the evening that - I think - will build on the idea of an Inverse. \$\endgroup\$ Commented Sep 5, 2019 at 11:58
2
\$\begingroup\$

My question is a disaster, and I don't know where my thoughts were while writing it. Funny enough I managed to write a lot of unittest even with the same type for the two keys without getting any compiler errors. Anyway below is a new version that builds on the Inverse concept, and it seems to do the trick all the way - but I'm sure someone can find something to criticize. I have experimented with the naming, but I'm not sure if they are final.

 /// <summary>
 /// A one-one relation bidirectional map.
 /// <para>
 /// A one-one relation means that each entry of type <typeparamref name="TPrimary"/> can correspond to exactly one 
 /// entry of type <typeparamref name="TSecondary"/> and visa versa.
 /// </para>
 /// The map doesn't support null elements because each element is both key and value in its relation and keys can't be null.
 /// </summary>
 /// <typeparam name="TPrimary">Any type</typeparam>
 /// <typeparam name="TSecondary">Any type</typeparam>
 public sealed class BidirectionalMap<TPrimary, TSecondary> : IEnumerable<KeyValuePair<TPrimary, TSecondary>>
 {
 private readonly Dictionary<TPrimary, TSecondary> map;
 private readonly Dictionary<TSecondary, TPrimary> inverseMap;
 /// <summary>
 /// Creates a BidirectionalMap from the provided dictionaries.
 /// Should be used only to create the Inverse.
 /// </summary>
 /// <param name="map"></param>
 /// <param name="inverseMap"></param>
 private BidirectionalMap(BidirectionalMap<TSecondary, TPrimary> inverse, Dictionary<TPrimary, TSecondary> map, Dictionary<TSecondary, TPrimary> inverseMap)
 {
 this.map = map;
 this.inverseMap = inverseMap;
 Inverse = inverse;
 }
 private BidirectionalMap(int capacity, IEqualityComparer<TPrimary> primaryComparer, IEqualityComparer<TSecondary> secondaryComparer)
 {
 map = new Dictionary<TPrimary, TSecondary>(capacity, primaryComparer);
 inverseMap = new Dictionary<TSecondary, TPrimary>(capacity, secondaryComparer);
 Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
 }
 public BidirectionalMap()
 {
 map = new Dictionary<TPrimary, TSecondary>();
 inverseMap = new Dictionary<TSecondary, TPrimary>();
 Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
 }
 /// <summary>
 /// Creates a BidirectionalMap initialized with the specified <paramref name="capacity"/>.
 /// </summary>
 /// <param name="capacity">The desired capacity for the map.</param>
 /// <exception cref="ArgumentOutOfRangeException">If capacity is out of range (&lt; 0)</exception>
 public BidirectionalMap(int capacity) : this(capacity, null, null)
 {
 }
 /// <summary>
 /// Creates a BidirectionalMap with the specified equality comparers.
 /// </summary>
 /// <param name="mapComparer">Equality comparer for <typeparamref name="TPrimary"/>. If null, the default comparer is used.</param>
 /// <param name="inverseComparer">Equality comparer for <typeparamref name="TSecondary"/>. If null, the default comparer is used.</param>
 public BidirectionalMap(IEqualityComparer<TPrimary> mapComparer, IEqualityComparer<TSecondary> inverseComparer)
 : this(0, mapComparer, inverseComparer)
 {
 }
 /// <summary>
 /// Creates a BidirectionalMap from the <paramref name="source"/> dictionary.
 /// </summary>
 /// <param name="source">The source dictionary from which to create a one-one relation map</param>
 /// <exception cref="ArgumentException">If <paramref name="source"/> contains doublets in values</exception>
 /// <exception cref="ArgumentNullException">If <paramref name="source"/> contains null keys and/or values</exception>
 public BidirectionalMap(IDictionary<TPrimary, TSecondary> source)
 {
 map = new Dictionary<TPrimary, TSecondary>(source);
 inverseMap = new Dictionary<TSecondary, TPrimary>(source.ToDictionary(kvp => kvp.Value, kvp => kvp.Key));
 Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
 }
 /// <summary>
 /// Creates a BidirectionalMap from the <paramref name="inverseSource"/> dictionary.
 /// </summary>
 /// <param name="inverseSource">The source dictionary from which to create a one-one relation map</param>
 /// <exception cref="ArgumentException">If <paramref name="inverseSource"/> contains doublets in values</exception>
 /// <exception cref="ArgumentNullException">If <paramref name="inverseSource"/> contains null keys and/or values</exception>
 public BidirectionalMap(IDictionary<TSecondary, TPrimary> inverseSource)
 {
 map = new Dictionary<TPrimary, TSecondary>(inverseSource.ToDictionary(kvp => kvp.Value, kvp => kvp.Key));
 inverseMap = new Dictionary<TSecondary, TPrimary>(inverseSource);
 Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
 }
 public BidirectionalMap<TSecondary, TPrimary> Inverse { get; }
 public int Count => map.Count;
 public ICollection<TPrimary> Keys => map.Keys;
 public ICollection<TSecondary> InverseKeys => inverseMap.Keys;
 public TSecondary this[TPrimary key]
 {
 get { return map[key]; }
 set { Set(key, value); }
 }
 public void Set(TPrimary primary, TSecondary secondary)
 {
 // Remove both the entries related to primary and secondary if any
 Remove(primary, secondary);
 // Now it should be safe to add the new relation.
 Add(primary, secondary);
 }
 public void Add(TPrimary primary, TSecondary secondary)
 {
 if (primary == null) throw new ArgumentNullException(nameof(primary));
 if (map.ContainsKey(primary)) throw new ArgumentException(nameof(primary), $"{primary} already present in the dictionary");
 if (secondary == null) throw new ArgumentNullException(nameof(secondary));
 if (inverseMap.ContainsKey(secondary)) throw new ArgumentException(nameof(secondary), $"{secondary} already present in the dictionary");
 map.Add(primary, secondary);
 inverseMap.Add(secondary, primary);
 }
 private bool Remove(TPrimary primary, TSecondary secondary)
 {
 bool result = false;
 if (map.TryGetValue(primary, out var primaryValue))
 {
 inverseMap.Remove(primaryValue);
 map.Remove(primary);
 result = true;
 }
 if (inverseMap.TryGetValue(secondary, out var secondaryValue))
 {
 map.Remove(secondaryValue);
 inverseMap.Remove(secondary);
 result = true;
 }
 return result;
 }
 public bool Remove(TPrimary primary)
 {
 if (map.TryGetValue(primary, out var secondary))
 {
 inverseMap.Remove(secondary);
 map.Remove(primary);
 return true;
 }
 return false;
 }
 public bool TryGetValue(TPrimary primary, out TSecondary secondary)
 {
 return map.TryGetValue(primary, out secondary);
 }
 public bool ContainsKey(TPrimary primary)
 {
 return map.ContainsKey(primary);
 }
 public void Clear()
 {
 map.Clear();
 inverseMap.Clear();
 }
 public IEnumerator<KeyValuePair<TPrimary, TSecondary>> GetEnumerator()
 {
 return map.GetEnumerator();
 }
 IEnumerator IEnumerable.GetEnumerator()
 {
 return GetEnumerator();
 }
 }

The corresponding unittests:

 [TestClass]
 public class BidirectionalMapTests
 {
 class TestObject<T>
 {
 public TestObject(T value)
 {
 Value = value;
 }
 public T Value { get; }
 public static implicit operator T(TestObject<T> to) => to.Value;
 public static implicit operator TestObject<T>(T value) => new TestObject<T>(value);
 public override string ToString()
 {
 return Value?.ToString() ?? "";
 }
 }
 [TestMethod]
 public void InitializeFromSourceDictionary()
 {
 Dictionary<string, int> source = new Dictionary<string, int>
 {
 { "a", 1 },
 { "b", 2 }
 };
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>(source);
 Assert.AreEqual(1, map["a"]);
 Assert.AreEqual("b", map.Inverse[2]);
 }
 [TestMethod]
 public void InvalidInitializeFromSourceDictionary()
 {
 TestObject<string> one = new TestObject<string>("1");
 Dictionary<string, TestObject<string>> source = new Dictionary<string, TestObject<string>>
 {
 { "a", one },
 { "b", one }
 };
 BidirectionalMap<string, TestObject<string>> map = null;
 Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));
 Dictionary<TestObject<string>, string> inverseSource = new Dictionary<TestObject<string>, string>
 {
 { "a", "1" },
 { "b", "1" }
 };
 Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(inverseSource));
 source = new Dictionary<string, TestObject<string>>
 {
 { "a", null },
 { "b", "1" }
 };
 Assert.ThrowsException<ArgumentNullException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));
 }
 [TestMethod]
 public void Add()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 Assert.AreEqual(1, map["a"]);
 Assert.AreEqual("b", map.Inverse[2]);
 Assert.AreEqual(2, map.Count);
 }
 [TestMethod]
 public void InvalidAdd()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 Assert.ThrowsException<ArgumentException>(() => map.Add("a", 2));
 Assert.ThrowsException<ArgumentException>(() => map.Add("b", 1));
 Assert.AreEqual(1, map["a"]);
 }
 [TestMethod]
 public void AddNull()
 {
 BidirectionalMap<string, string> map = new BidirectionalMap<string, string>();
 Assert.ThrowsException<ArgumentNullException>(() => map.Add(null, "a"));
 Assert.ThrowsException<ArgumentNullException>(() => map.Add("a", null));
 Assert.AreEqual(0, map.Count);
 }
 [TestMethod]
 public void Remove()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 Assert.AreEqual(2, map.Count);
 map.Remove("a");
 Assert.AreEqual(1, map.Count);
 map.Inverse.Remove(2);
 Assert.AreEqual(0, map.Count);
 }
 [TestMethod]
 public void RemoveNonExistingValue()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 Assert.IsFalse(map.Remove("c"));
 Assert.AreEqual(2, map.Count);
 }
 [TestMethod]
 public void Set()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 map["a"] = 3;
 Assert.AreEqual(2, map.Count);
 Assert.IsTrue(map.TryGetValue("a", out int second));
 Assert.AreEqual(3, second);
 Assert.IsTrue(map.Inverse.TryGetValue(3, out string first));
 Assert.AreEqual("a", first);
 Assert.IsFalse(map.Inverse.TryGetValue(1, out _));
 }
 [TestMethod]
 public void SetWithExistingSecondValue()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 map.Add("a", 1);
 map.Add("b", 2);
 map["a"] = 2;
 Assert.AreEqual(1, map.Count);
 Assert.IsTrue(map.TryGetValue("a", out int second));
 Assert.AreEqual(2, second);
 Assert.IsTrue(map.Inverse.TryGetValue(2, out string first));
 Assert.AreEqual("a", first);
 Assert.IsFalse(map.TryGetValue("b", out _));
 }
 [TestMethod]
 public void TryGetValue()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>
 {
 { "a", 1 },
 { "b", 2 }
 };
 Assert.IsTrue(map.TryGetValue("a", out int second));
 Assert.AreEqual(1, second);
 Assert.IsTrue(map.Inverse.TryGetValue(2, out string first));
 Assert.AreEqual("b", first);
 Assert.IsFalse(map.TryGetValue("c", out _));
 Assert.IsFalse(map.Inverse.TryGetValue(3, out _));
 }
 [TestMethod]
 public void Indexer()
 {
 BidirectionalMap<string, string> map = new BidirectionalMap<string, string>
 {
 { "a", "1" },
 { "b", "2" }
 };
 Assert.AreEqual("1", map["a"]);
 Assert.AreEqual("b", map.Inverse["2"]);
 }
 [TestMethod]
 public void Inverse()
 {
 BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
 Assert.AreEqual(map, map.Inverse.Inverse);
 map = new BidirectionalMap<string, int>(10);
 Assert.AreEqual(map, map.Inverse.Inverse);
 map = new BidirectionalMap<string, int>(EqualityComparer<string>.Default, EqualityComparer<int>.Default);
 Assert.AreEqual(map, map.Inverse.Inverse);
 map = new BidirectionalMap<string, int>(EqualityComparer<string>.Default, EqualityComparer<int>.Default);
 Assert.AreEqual(map, map.Inverse.Inverse);
 Dictionary<string, int> source = new Dictionary<string, int>
 {
 { "a", 1 },
 { "b", 2 }
 };
 map = new BidirectionalMap<string, int>(source);
 Assert.AreEqual(map, map.Inverse.Inverse);
 Dictionary<int, string> inverseSource = new Dictionary<int, string>
 {
 { 1, "a" },
 { 2, "b" }
 };
 map = new BidirectionalMap<string, int>(inverseSource);
 Assert.AreEqual(map, map.Inverse.Inverse);
 }
 }
answered Sep 5, 2019 at 18:08
\$\endgroup\$
11
  • 2
    \$\begingroup\$ This would be cool if you named the dictionaries map and pam for the inverted one :-P The code would nicely align then in a couple of places. \$\endgroup\$ Commented Sep 5, 2019 at 18:17
  • 1
    \$\begingroup\$ I also think that now with single APIs for each operation it'd should be save to actually implement the IDictionary interface. \$\endgroup\$ Commented Sep 5, 2019 at 18:25
  • 2
    \$\begingroup\$ I'm shocked that they actually allow the comparer to be null. I first thought it was a bug in your code but then checked the docs and indeed, it is ok to pass a null :-\ everytime I think I know the framework they have to surprise me with new (un)consistency conventions. \$\endgroup\$ Commented Sep 5, 2019 at 18:50
  • 1
    \$\begingroup\$ @t3chb0t: Initially I actually threw ArgumentNullException in a null check, but also realized that it was unnecessary. \$\endgroup\$ Commented Sep 5, 2019 at 19:00
  • 2
    \$\begingroup\$ I think we can, I would leave only one of the constructors TPrimary, TSecondary in order to be consistent with the other APIs and add a factory method FromInverse instead, to still support what the now removed cunstructor allowed. \$\endgroup\$ Commented Sep 5, 2019 at 19:08

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.