Click here to Skip to main content
Click here to Skip to main content
Go to top

A generic Trictionary class

, 11 Mar 2009
Rate this:
Please Sign up or sign in to vote.
This article describes a generic Trictionary class derived from Dictionary that allows two values of different types per key

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>();

// ADDING:
// Option 1:
trict.Add(1, "A", 1.1);

// Option 2:
trict[2] = new DualObjectContainer<string,double>("B", 2.2);

// RETRIEVING:
// Option 1:
string s;
double d;
trict.Get(2, out s, out d);

// Option 2:
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) :

/// <summary>
/// Represents a dictionary with two distinct typed values per key
/// </summary>
/// <typeparam name="TKey">The type of the dictionary key</typeparam>
/// <typeparam name="TValue1">The type of the first value</typeparam>
/// <typeparam name="TValue2">The type of the second value</typeparam>
[Serializable]
public class Trictionary<TKey, TValue1, TValue2> 
  : Dictionary<TKey, DualObject<TValue1, TValue2>>
{
    /// <summary>
    /// Initializes a new instance of the Trictionary class
    /// </summary>
    public Trictionary()
    {
    }

    /// <summary>
    /// Initializes a new instance of the Trictionary 
    /// class for use with serialization
    /// </summary>
    /// <param name="info">SerializationInfo objects that holds the 
    /// required information for serialization</param>
    /// <param name="context">StreamingContext structure 
    /// for serialization</param>
    protected Trictionary(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }

    /// <summary>
    /// Gets or sets the values associated with the specified key
    /// </summary>
    /// <param name="key">The key of the values to get or set</param>
    /// <returns>Returns the DualObject associated with this key</returns>
    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]

/// <summary>
/// Represents a class that can hold two types values
/// </summary>
/// <typeparam name="TValue1">The first type</typeparam>
/// <typeparam name="TValue2">The second type</typeparam>
[Serializable]
public class DualObject<TValue1, TValue2> 
  : IEquatable<DualObject<TValue1, TValue2>>
{
    /// <summary>
    /// Initializes a new instance of the DualObject class
    /// </summary>
    /// <param name="value1">First value</param>
    /// <param name="value2">Second value</param>
    public DualObject(TValue1 value1, TValue2 value2)
    {
        this.FirstValue = value1;
        this.SecondValue = value2;
    }

    /// <summary>
    /// Initializes a new instance of the DualObject class
    /// </summary>
    /// <param name="value2">Second value</param>
    /// <param name="value1">First value</param>
    public DualObject(TValue2 value2, TValue1 value1)
        : this(value1, value2)
    {
    }

    /// <summary>
    /// Initializes a new instance of the DualObject class
    /// </summary>
    /// <param name="value">First value</param>
    public DualObject(TValue1 value)
    {
        this.FirstValue = value;
    }

    /// <summary>
    /// Initializes a new instance of the DualObject class
    /// </summary>
    /// <param name="value">Second value</param>
    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;
        }
    }

    /// <summary>
    /// Implicit conversion operator to convert to T1
    /// </summary>
    /// <param name="dualObject">The DualObject to convert from</param>
    /// <returns>The converted object</returns>
    public static implicit operator TValue1(
      DualObject<TValue1, TValue2> dualObject)
    {
        return dualObject.FirstValue;
    }

    /// <summary>
    /// Implicit conversion operator to convert to T2
    /// </summary>
    /// <param name="dualObject">The DualObject to convert from</param>
    /// <returns>The converted object</returns>
    public static implicit operator TValue2(
      DualObject<TValue1, TValue2> dualObject)
    {
        return dualObject.SecondValue;
    }

    /// <summary>
    /// Implicit conversion operator to convert from T1
    /// </summary>
    /// <param name="value">The object to convert from</param>
    /// <returns>The converted DualObject</returns>
    public static implicit operator DualObject<TValue1, TValue2>(
      TValue1 value)
    {
        return new DualObject<TValue1, TValue2>(value);
    }

    /// <summary>
    /// Implicit conversion operator to convert from T2
    /// </summary>
    /// <param name="value">The object to convert from</param>
    /// <returns>The converted DualObject</returns>
    public static implicit operator DualObject<TValue1, TValue2>(
      TValue2 value)
    {
        return new DualObject<TValue1, TValue2>(value);
    }

    /// <summary>
    /// Sets the value for the first type
    /// </summary>
    /// <param name="value">The value to set</param>
    public void Set(TValue1 value)
    {
        this.FirstValue = value;
    }

    /// <summary>
    ///  Sets the value for the second type
    /// </summary>
    /// <param name="value">The value to set</param>
    public void Set(TValue2 value)
    {
        this.SecondValue = value;
    }

    /// <summary>
    /// Sets the values from another DualObject
    /// </summary>
    /// <param name="dualObject">The DualObject 
    /// to copy the values from</param>
    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

    /// <summary>
    /// Indicates whether the current DualObject is 
    /// equal to another DualObject
    /// Note : This will return true if either one of the 
    /// values are equal.
    /// </summary>
    /// <param name="other">The DualObject to compare with</param>
    /// <returns>True if the objects are equal</returns>
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nish Sivakumar

United States United States
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
 
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site - www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff - blog.voidnish.com.
 
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy Summer Love and Some more Cricket as well as a programming book – Extending MFC applications with the .NET Framework.
 
Nish's latest book C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
 
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.

Comments and Discussions

 
GeneralMy vote of 1 Pinmemberbinarycheese13-Mar-09 7:08 
AnswerRe: My vote of 1 PinsitebuilderNishant Sivakumar13-Mar-09 11:42 
GeneralNice, and a few ideas PinmemberOmer Mor12-Mar-09 9:39 
GeneralRe: Nice, and a few ideas PinsitebuilderNishant Sivakumar12-Mar-09 9:57 
GeneralInteresting! PinmemberS. Senthil Kumar12-Mar-09 8:12 
GeneralRe: Interesting! PinsitebuilderNishant Sivakumar12-Mar-09 10:02 
Thanks Senthil.
 
Yeah I should perhaps have exposed those properties so people can explicitly access a specific value. But you are right, if the 2 values are int and short and they try to cast that to a float, you'd probably have to do an extra level of casting to get a specific value.
 
float f = (float)(int)value;
 
whereas they could probably do :
 
float f = (float)value.FirstValue;
 
I haven't tried it out yet but I think the compiler would resolve this without hitting an ambiguity issue.
 
----
 
p.s. I was hoping to meet you at the MVP summit last week.
 

GeneralNice PinprotectorMarc Clifton12-Mar-09 1:25 
GeneralRe: Nice PinsitebuilderNishant Sivakumar12-Mar-09 3:50 
GeneralRe: Nice PinprotectorMarc Clifton12-Mar-09 3:53 
GeneralNice PinmemberRobert Rohde11-Mar-09 23:14 
GeneralRe: Nice PinsitebuilderNishant Sivakumar12-Mar-09 3:25 
GeneralNote PinmemberUser of Users Group11-Mar-09 22:49 
GeneralRe: Note PinsitebuilderNishant Sivakumar12-Mar-09 3:21 
GeneralRe: Note [modified] PinmemberUser of Users Group12-Mar-09 8:44 
GeneralRe: Note PinsitebuilderNishant Sivakumar12-Mar-09 10:04 
GeneralRe: Note [modified] PinmemberUser of Users Group12-Mar-09 14:19 
GeneralRe: Note PinsitebuilderNishant Sivakumar13-Mar-09 11:46 
GeneralEquals Pinmemberandre511-Mar-09 21:32 
GeneralRe: Equals PinsitebuilderNishant Sivakumar12-Mar-09 3:39 
GeneralNice implementation PinmemberJoe Enos11-Mar-09 16:55 
GeneralRe: Nice implementation PinsitebuilderNishant Sivakumar12-Mar-09 3:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140916.1 | Last Updated 11 Mar 2009
Article Copyright 2009 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid