The management of bidirectional mappings is a reoccuring topic. I took the time to write an (hopefully) efficient implementation.
using System;
using System.Collections;
using System.Collections.Generic;
namespace buka_core.misc
{
/// <summary>
///
/// File Bijection.cs
///
/// Provides an implementation of a discrete bijective mapping
///
/// The inverses are created using shallow copies of the underlying datastructures, which leads to
/// the original object and all its derived inverses being modified if one object changes. For this
/// reason the class implements the interface ICloneable which allows the user to create deep copies
///
/// The class also implements the interface IDictionary which provides easy access to the proto-
/// type
///
/// </summary>
/// <typeparam name="T_Proto">Datatype of keys for the prototype</typeparam>
/// <typeparam name="T_Inv">Datatype of keys for its inverse</typeparam>
public class Bijection<T_Proto, T_Inv> : ICloneable, IDictionary<T_Proto, T_Inv>
{
/// <summary>
/// Creates an empty discrete bijective mapping
/// </summary>
public Bijection()
{
}
/// <summary>
/// Used internally to efficiently generate inverses
/// </summary>
/// <param name="proto">The prototype mapping</param>
/// <param name="inverse">Its inverse mapping</param>
private Bijection(IDictionary<T_Proto, T_Inv> proto, IDictionary<T_Inv, T_Proto> inverse)
{
_Proto = proto;
_Inv = inverse;
}
/// <summary>
/// Indexer to insert and modify records
/// </summary>
/// <param name="key">Object for which the corresponding dictionary entry should be returned</param>
/// <returns>The value that key maps to</returns>
public T_Inv this[T_Proto key]
{
get
{
if (!_Proto.ContainsKey(key))
{
throw new KeyNotFoundException("[Bijection] The key " + key + " could not be found");
}
return _Proto[key];
}
set
{
this.Add(key, value);
}
}
/// <summary>
/// Returns a bijection for which keys and values are reversed
/// </summary>
public Bijection<T_Inv, T_Proto> Inverse
{
get
{
if (null == _inverse)
{
_inverse = new Bijection<T_Inv, T_Proto>(_Inv, _Proto);
}
return _inverse;
}
}
private Bijection<T_Inv, T_Proto> _inverse = null; // Backer for lazy initialisation of Inverse
/// <summary>
/// Prototype mapping
/// </summary>
private IDictionary<T_Proto, T_Inv> _Proto
{
get
{
if (null == _proto)
{
_proto = new SortedDictionary<T_Proto, T_Inv>();
}
return _proto;
}
/* private */
set
{
_proto = value;
}
}
private IDictionary<T_Proto, T_Inv> _proto = null; // Backer for lazy initialisation of _Proto
/// <summary>
/// Inverse prototype mapping
/// </summary>
private IDictionary<T_Inv, T_Proto> _Inv
{
get
{
if (null == _inv)
{
_inv = new SortedDictionary<T_Inv, T_Proto>();
}
return _inv;
}
/* private */
set
{
_inv = value;
}
}
private IDictionary<T_Inv, T_Proto> _inv = null; // Backer for lazy initialisation of _Inv
#region Implementation of ICloneable
/// <summary>
/// Creates a deep copy
/// </summary>
public object Clone()
{
return new Bijection<T_Proto, T_Inv>(
new SortedDictionary<T_Proto, T_Inv>(_Proto),
new SortedDictionary<T_Inv, T_Proto>(_Inv)
);
}
#endregion
#region Implementation of IDictionary<T_Proto, T_Inv>
public ICollection<T_Proto> Keys => _Proto.Keys;
public ICollection<T_Inv> Values => _Proto.Values;
public int Count => _Proto.Count;
public bool IsReadOnly => _Proto.IsReadOnly;
public bool Contains(KeyValuePair<T_Proto, T_Inv> item)
{
return _Proto.Contains(item);
}
public bool ContainsKey(T_Proto key)
{
return _Proto.ContainsKey(key);
}
public void Clear()
{
_Proto.Clear();
_Inv.Clear();
}
public void Add(T_Proto key, T_Inv value)
{
if (_Proto.ContainsKey(key))
{
_Inv.Remove(_Proto[key]);
}
if (_Inv.ContainsKey(value))
{
throw new ArgumentException("[Bijection] The inverse already maps " + value + " to " + _Inv[value]);
}
_Proto.Add(key, value);
_Inv.Add(value, key);
}
public void Add(KeyValuePair<T_Proto, T_Inv> item)
{
this.Add(item.Key, item.Value);
}
public bool Remove(T_Proto key)
{
if (_Proto.ContainsKey(key))
{
bool removed_inv = _Inv.Remove(_Proto[key]);
bool removed_proto = _Proto.Remove(key);
return (removed_proto && removed_inv); // == true
}
else
{
return false;
}
}
public bool Remove(KeyValuePair<T_Proto, T_Inv> item)
{
return this.Remove(item.Key);
}
public bool TryGetValue(T_Proto key, out T_Inv value)
{
return _Proto.TryGetValue(key, out value);
}
public void CopyTo(KeyValuePair<T_Proto, T_Inv>[] array, int arrayIndex)
{
_Proto.CopyTo(array, arrayIndex);
}
public IEnumerator<KeyValuePair<T_Proto, T_Inv>> GetEnumerator()
{
return _Proto.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _Proto.GetEnumerator();
}
#endregion
#region Overrides
public override bool Equals(object obj)
{
Bijection<T_Proto, T_Inv> obj_bijection = (obj as Bijection<T_Proto, T_Inv>); if (null == obj) return false;
if (this.Count != obj_bijection.Count) return false;
if (!_Proto.Equals(obj_bijection._Proto)) return false;
if (!_Inv.Equals(obj_bijection._Inv)) return false;
return true;
}
public override int GetHashCode()
{
return _Proto.GetHashCode();
}
public override string ToString()
{
return _Proto.ToString();
}
#endregion
}
}
Instances would be used as follows
Bijection<int, string> b = new Bijection<int, string>();
b[1] = "frog";
b[2] = "fish";
b[3] = "dog";
b[5] = "cat";
b[8] = "snake";
b[13] = "crocodile";
Console.WriteLine(b.Inverse["crocodile"]);
Console.WriteLine(b[13]);
Any feedback/ suggestions are welcome. Is it reasonable to keep the object and its inverse tied like this or would it be unexpected behavior that changing the inverse also changes the original object