I've run across a number of cases where a pattern for accessing items in a keyed collection (like a dictionary) is encumbered by the fact that the type of the key is not a simple type (string, int, double, etc) and isn't something that you would want to promote to an actual named class.
C# 3.0 introduces the concept of anonymous types which the compiler automatically generates. Unlike struct
's, these dynamically generated classes provide an implementation of both Equals()
and GetHashCode()
- which are designed to work well with dictionary and hashtable implementation in .NET.
I've hijacked this feature to create an opaque key - essentially a generic class that allows you to create keys on the fly by providing the types that are part of the key - and using an anonymous class to actually provide the Equals/GetHashCode behavior. The intent of this class is to ONLY provide a easy means to use several values together as a key into a dictionary or hashtable. It's not intended as a class to provide meaningful application logic or data manipulation.
The benefit of the pattern is that it makes it easy to implement composite keys that always provide appropriate equality and hashing behavior. It's also easily extensible to keys of any number of dimensions (however many C# can parse as template parameters at least). We can even make improvements by allowing the OpaqueKey<> class to be inherited from so that it's properties and constructor parameters can be given more instructive names.
I am worried that this pattern may have some unintended consequences or hidden pitfalls that I'm not considering.
Are there any reasons why the following OpaqueKey code may by undesirable?
Are there any edge cases that I haven't considered in the implementation?
Is there a simpler way to achieve the same functionality?
public class OpaqueKey<A,B>
{
private readonly object m_Key;
// Not strictly necessary, but possibly convenient...
public A First { get; private set; }
public B Second { get; private set; }
public OpaqueKey( A k1, B k2 )
{
m_Key = new { K1 = k1, K2 = k2 };
First = k1;
Second = k2;
}
public override bool Equals(object obj)
{
var otherKey = obj as OpaqueKey<A, B>;
return otherKey == null ? false : m_Key.Equals( otherKey.m_Key );
}
public override int GetHashCode()
{
return m_Key.GetHashCode();
}
}
public static void TrivialTestCase()
{
var dict = new Dictionary<OpaqueKey<string,string>, string>();
dict.Add(new OpaqueKey<string, string>("A", "B"), "AB");
dict.Add(new OpaqueKey<string, string>("A", "C"), "AC");
dict.Add(new OpaqueKey<string, string>("A", "D"), "AD");
dict.Add(new OpaqueKey<string, string>("A", "E"), "AE");
var value = dict[new OpaqueKey<string,string>("A","D")];
Debug.Assert( value == "AD" ); // trivial test case...
}