If I'm understanding you correctly, you're trying to find a statically-typed way to say "a value of type Section
can be either a value of type Highlights
, Chart
, or Tweets
". There are many terms for this, including tagged union, discriminated union, variant, or sum type but they all refer to the same concept. Sum types are a standard feature in statically-typed functional programming languages, but with a bit of ingenuity you can roll your own in C#. Using that answer as a template, and adding sealed
to make sure no one adds any extra subclasses, your Section
type would look something like this:
public abstract class Section
{
// Prevent subclassing outside of this scope
private Section() {}
// Subclass implementation calls the appropriate function.
public abstract R Match<R>(Func<H, R> IfHighlight, Func<C, R> IfChart, Func<T, R> IfTweet);
// Convenience wrapper for when the caller doesn't want to return a value
// from the match expression.
public sealed void Match(Action<H> IfHighlight, Action<C> IfChart, Action<T> IfTweet)
{
this.Match<int>(
IfHighlight: Section => { IfHighlight(Section); return 0; },
IfChart: Section => { IfChart(Section); return 0; },
IfTweet: Section => { IfTweet(Section); return 0; }
);
}
// From here on down, you define your specific types as usual, and override Match to call
// the correct function
public sealed class Highlight : Section
{
// Implementation goes here
public override R Match<R>(Func<H, R> IfHighlight, Func<C, R> IfChart, Func<T, R> IfTweet)
{
return IfHighlight(this);
}
}
public class Chart : Section
{
// Implementation goes here
public override R Match<R>(Func<H, R> IfHighlight, Func<C, R> IfChart, Func<T, R> IfTweet)
{
return IfChart(this);
}
}
public class Tweet : Section
{
// Implementation goes here
public override R Match<R>(Func<H, R> IfHighlight, Func<C, R> IfChart, Func<T, R> IfTweet)
{
return IfTweet(this);
}
}
}
With that in place, you can declare variables of type Section
, and act on them by passing 3 functions to the Match
methods:
Section section = deserializeJson()
section.Match(
IfHighlight: highlight => /* code to use if you get a Highlight */
IfChart: chart => /* ... */
IfTweet: tweet => /* ... */
);
You could just as easily have a collection of Section
s and operate on them that way when you iterate over it.
There may be a simpler way towards it.
Consider this:
public class NAryDictionary<TKey, TValue> :
Dictionary<TKey, TValue>
{
}
public class NAryDictionary<TKey1, TKey2, TValue> :
Dictionary<TKey1, NAryDictionary<TKey2, TValue>>
{
}
public class NAryDictionary<TKey1, TKey2, TKey3, TValue> :
Dictionary<TKey1, NAryDictionary<TKey2, TKey3, TValue>>
{
}
Then you can write:
class Program
{
static void Main(string[] args)
{
var dico3 = new NAryDictionary<bool, int, string, decimal>();
dico3[false] = new NAryDictionary<int, string, decimal>();
dico3[false][123] = new NAryDictionary<string, decimal>();
dico3[false][123]["foo"] = 123456789.012m;
Console.WriteLine(dico3[false][123]["foo"].ToString("0,0.000"));
Console.ReadKey();
}
}
Now, although it's Visual Studio intellisense-friendly (as you'll notice with automatic code completion proposals after typing the " = " signs in the above assignments), it is arguably a bit cumbersome, still, because of those
... new NAryDictionary<bool, int, string, decimal>()
... new NAryDictionary<int, string, decimal>()
and
... new NAryDictionary<string, decimal>()
Which can be alleviated with,
public static class NAryDictionaryExtensions
{
public static NAryDictionary<TKey2, TValue> New<TKey1, TKey2, TValue>(this NAryDictionary<TKey1, TKey2, TValue> dictionary)
{
return new NAryDictionary<TKey2, TValue>();
}
public static NAryDictionary<TKey2, TKey3, TValue> New<TKey1, TKey2, TKey3, TValue>(this NAryDictionary<TKey1, TKey2, TKey3, TValue> dictionary)
{
return new NAryDictionary<TKey2, TKey3, TValue>();
}
}
allowing now to write, more simply,
class Program
{
static void Main(string[] args)
{
var dico3 = new NAryDictionary<bool, int, string, decimal>();
dico3[true] = dico3.New();
dico3[true][456] = dico3[true].New();
dico3[true][456]["bar"] = 456789012.345m;
Console.WriteLine(dico3[true][456]["bar"].ToString("0,0.000"));
Console.ReadKey();
}
}
'Hope this helps.
Best Answer
I would go with specific classes - it will be much easier to write unit tests and maintain code even if you lose some performance. It is not a place where you should prefer performance over clarity.