Introduction
I wrote this article and the accompanying code purely for fun. It's been a while since I wrote an article and I thought this would help me get into groove. The article was inspired by another article on the same topic :
That article describes a Trictionary
class that is essentially a dictionary but for each key there are two values, both of differing types. In many cases instead of doing this, the proper approach would most likely be to use a struct
that would have those two types as members. But there may also be scenarios where you may want to avoid having to unnecessarily create a struct
just for this purpose. You could also use an anonymous type, but that's really not so different in the sense you still end up having a new type in your assembly. Joe Enos's Trictionary
class interested me though I did not like the syntactic usage the class permitted. Here's some sample code from his article :
Trictionary<int, string, double> trict = new Trictionary<int, string, double>();
trict.Add(1, "A", 1.1);
trict[2] = new DualObjectContainer<string,double>("B", 2.2);
string s;
double d;
trict.Get(2, out s, out d);
DualObjectContainer<string, double> container = trict[1];
I did not like the fact that you had to call an Add
method or create an object just to add entries to the Trictionary
. I also did not like the retrieval mechanism, calling a Get
method with two out
parameters is not very elegant, and getting back the sub-object he uses to store the values is even more messy in my opinion. That's why I quickly put together this little class. I haven't ever had to use it myself (well given that I just wrote it an hour or so ago, I didn't have the opportunity to do so) but I may use it some time in future.
Usage
Here's some sample code that shows how my Trictionary
can be used.
static void Main()
{
Trictionary<int, string, double> trictionary =
new Trictionary<int, string, double>();
trictionary[10] = 10.7;
trictionary[10] = "sss";
trictionary[10] = "sss-mod";
string s = trictionary[10];
double d = trictionary[10];
Console.WriteLine(s);
Console.WriteLine(d);
trictionary[12] = 11.4;
trictionary[12] = "bbb";
trictionary[12] = 11.5;
trictionary[15] = 10.1;
trictionary[16] = "ppp";
trictionary[19] = "bbb";
trictionary[19] = 11.5;
foreach (var value in trictionary.Values)
{
Console.WriteLine("{0}, {1}", (string)value, (double)value );
}
}
Notice how the class can be used in a way that's a lot more intuitive to the developer assuming he's familiar with Dictionary
usage. In the above sample, the two types associated with the values are string
and double
. You just assign string
or double
values via the indexer (as you'd normally do with the Dictionary
class). Retrieval is a mere matter of casting to string
or double
. There may be folks out there who think Joe Enos's class provides a more intuitive usage structure - well, different people have different ideas on all these things. I am sure there'll be a few people who'll like my class usage better - so it really doesn't matter much.
Limitation
The two values cannot be of the same type. If that's the case then you don't need this class - you can just use List<T>
or ICollection<T>
as the value type of the regular Dictionary
class. This is "by design". The following code will not compile.
Trictionary<int, string, string> trictionary2 =
new Trictionary<int, string, string>();
trictionary2[1] = "hello";
trictionary2[2] = "world"
Implementation details
My Trictionary
class derives from Dictionary
. Here's the full class listing (re-formatted for the browser width) :
[Serializable]
public class Trictionary<TKey, TValue1, TValue2>
: Dictionary<TKey, DualObject<TValue1, TValue2>>
{
public Trictionary()
{
}
protected Trictionary(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
public new DualObject<TValue1, TValue2> this[TKey key]
{
get
{
return base[key];
}
set
{
if (this.ContainsKey(key))
{
base[key].Set(value);
}
else
{
base[key] = value;
}
}
}
}
I've try to bold out the important bits of code but it may not really stand out depending on the font used. Essentially the value type for the dictionary is my DualObject<>
class which I'll talk about soon. If this is the first time the key is being used it'll just add it to the dictionary, else it will use the Set
method in the DualObject
class to associate the new value of either type. So how does it accept values of either type when the value-type for the dictionary is the DualObject<>
class? Well that's all implicit operator magic as shown below. [The code has been re-formatted for the browser]
[Serializable]
public class DualObject<TValue1, TValue2>
: IEquatable<DualObject<TValue1, TValue2>>
{
public DualObject(TValue1 value1, TValue2 value2)
{
this.FirstValue = value1;
this.SecondValue = value2;
}
public DualObject(TValue2 value2, TValue1 value1)
: this(value1, value2)
{
}
public DualObject(TValue1 value)
{
this.FirstValue = value;
}
public DualObject(TValue2 value)
{
this.SecondValue = value;
}
private bool isFirstValueSet;
private TValue1 firstValue;
private TValue1 FirstValue
{
get
{
return this.firstValue;
}
set
{
this.firstValue = value;
this.isFirstValueSet = true;
}
}
private bool isSecondValueSet;
private TValue2 secondValue;
private TValue2 SecondValue
{
get
{
return this.secondValue;
}
set
{
this.secondValue = value;
this.isSecondValueSet = true;
}
}
public static implicit operator TValue1(
DualObject<TValue1, TValue2> dualObject)
{
return dualObject.FirstValue;
}
public static implicit operator TValue2(
DualObject<TValue1, TValue2> dualObject)
{
return dualObject.SecondValue;
}
public static implicit operator DualObject<TValue1, TValue2>(
TValue1 value)
{
return new DualObject<TValue1, TValue2>(value);
}
public static implicit operator DualObject<TValue1, TValue2>(
TValue2 value)
{
return new DualObject<TValue1, TValue2>(value);
}
public void Set(TValue1 value)
{
this.FirstValue = value;
}
public void Set(TValue2 value)
{
this.SecondValue = value;
}
public void Set(DualObject<TValue1, TValue2> dualObject)
{
if (dualObject.isFirstValueSet)
{
this.FirstValue = dualObject.FirstValue;
}
if (dualObject.isSecondValueSet)
{
this.SecondValue = dualObject.SecondValue;
}
}
#region IEquatable<DualObject<T1,T2>> Members
public bool Equals(DualObject<TValue1, TValue2> other)
{
bool firstEqual = this.FirstValue == null ?
other.FirstValue == null :
this.FirstValue.Equals(other.FirstValue);
bool secondEqual = this.SecondValue == null ?
other.SecondValue == null :
this.SecondValue.Equals(other.SecondValue);
return firstEqual || secondEqual;
}
#endregion
}
The implicit conversions are kinda self-explanatory. These operators allow me to pass either type to the Trictionary
(in the sample, that'd be string
and double
). They also allow me to cast back the DualObject<>
to either type (string
or double
in my sample code). The most interesting bit might be my Equals
implementation which might seem to be wrong at first sight. Should that ||
comparison have been an &&
comparison? Technically yes, it should have been that but for the sake of my Trictionary
class, two DualObject<>
objects are considered equal if either of their two values match. This is because I want ContainsValue
to work correctly. The following code will make it clear :
trictionary.Clear();
trictionary[19] = "bbb";
trictionary[19] = 11.5;
Console.WriteLine(trictionary.ContainsValue("bbb"));
Console.WriteLine(trictionary.ContainsValue("aaa"));
Console.WriteLine(trictionary.ContainsValue(11.5));
The above code will output True
, False
, True
as expected. Had I chosen not to implement Equals
this way, I'd have had to re-implement ContainsValue
and rewrite a lot of comparison code (something I wanted to avoid). Also I do not expect anyone to use my DualObject<>
class outside of Trictionary
. And even if they do, then I think it may actually help them in their cause to keep the Equals
behavior in this manner.
Feel free to post any comments and feedback. And I'd like to specially mention that any feedback here on the need for a Trictionary
type class should really go to Joe Enos and not to me *grin*
[Note that the downloadable code in the zip file is properly formatted as I did not have to add extra line-breaks for an improved browser display]
References
History
- March 11, 2009 - Article published