Click here to Skip to main content
15,867,594 members
Articles / Programming Languages / C#

A generic Trictionary class

Rate me:
Please Sign up or sign in to vote.
4.53/5 (19 votes)
11 Mar 2009CPOL4 min read 52.5K   140   20   21
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 :

C#
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.

C#
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.

C#
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) :

C#
/// <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]

C#
/// <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 :

C#
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)


Written By
United States United States
Nish Nishant is a technology enthusiast from Columbus, Ohio. He has over 20 years of software industry experience in various roles including Chief Technology Officer, Senior Solution Architect, Lead Software Architect, Principal Software Engineer, and Engineering/Architecture Team Leader. Nish is a 14-time recipient of the Microsoft Visual C++ MVP Award.

Nish authored C++/CLI in Action for Manning Publications in 2005, and co-authored Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his WordPress blog. Nish is experienced in technology leadership, solution architecture, software architecture, cloud development (AWS and Azure), REST services, software engineering best practices, CI/CD, mentoring, and directing all stages of software development.

Nish's Technology Blog : voidnish.wordpress.com

Comments and Discussions

 
GeneralMy vote of 1 Pin
Tawani Anyangwe13-Mar-09 7:08
Tawani Anyangwe13-Mar-09 7:08 
AnswerRe: My vote of 1 Pin
Nish Nishant13-Mar-09 11:42
sitebuilderNish Nishant13-Mar-09 11:42 
GeneralNice, and a few ideas Pin
Omer Mor12-Mar-09 9:39
Omer Mor12-Mar-09 9:39 
GeneralRe: Nice, and a few ideas Pin
Nish Nishant12-Mar-09 9:57
sitebuilderNish Nishant12-Mar-09 9:57 
GeneralInteresting! Pin
S. Senthil Kumar12-Mar-09 8:12
S. Senthil Kumar12-Mar-09 8:12 
GeneralRe: Interesting! Pin
Nish Nishant12-Mar-09 10:02
sitebuilderNish Nishant12-Mar-09 10:02 
GeneralNice Pin
Marc Clifton12-Mar-09 1:25
mvaMarc Clifton12-Mar-09 1:25 
GeneralRe: Nice Pin
Nish Nishant12-Mar-09 3:50
sitebuilderNish Nishant12-Mar-09 3:50 
GeneralRe: Nice Pin
Marc Clifton12-Mar-09 3:53
mvaMarc Clifton12-Mar-09 3:53 
GeneralNice Pin
Robert Rohde11-Mar-09 23:14
Robert Rohde11-Mar-09 23:14 
GeneralRe: Nice Pin
Nish Nishant12-Mar-09 3:25
sitebuilderNish Nishant12-Mar-09 3:25 
GeneralNote Pin
User of Users Group11-Mar-09 22:49
User of Users Group11-Mar-09 22:49 
GeneralRe: Note Pin
Nish Nishant12-Mar-09 3:21
sitebuilderNish Nishant12-Mar-09 3:21 
GeneralRe: Note [modified] Pin
User of Users Group12-Mar-09 8:44
User of Users Group12-Mar-09 8:44 
GeneralRe: Note Pin
Nish Nishant12-Mar-09 10:04
sitebuilderNish Nishant12-Mar-09 10:04 
GeneralRe: Note [modified] Pin
User of Users Group12-Mar-09 14:19
User of Users Group12-Mar-09 14:19 
GeneralRe: Note Pin
Nish Nishant13-Mar-09 11:46
sitebuilderNish Nishant13-Mar-09 11:46 
GeneralEquals Pin
andre511-Mar-09 21:32
andre511-Mar-09 21:32 
GeneralRe: Equals Pin
Nish Nishant12-Mar-09 3:39
sitebuilderNish Nishant12-Mar-09 3:39 
GeneralNice implementation Pin
User 304249811-Mar-09 16:55
User 304249811-Mar-09 16:55 
GeneralRe: Nice implementation Pin
Nish Nishant12-Mar-09 3:25
sitebuilderNish Nishant12-Mar-09 3:25 

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

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