Tuesday, February 26th, 2008
Avoiding Overload Hell in C#
C# lacks default parameters. The C# answer to default params is overloaded methods. With a lot of options, this can quickly scale to hell. Even if it had default parameters, they are pretty limited too. This post talks a bit about a couple of other solutions I found to the problem that avoid the combinatorial sea of overloads.
The Problem
For kicks, I wrote a little curses-like terminal library in C#. It only really has a few core methods: Write() for writing text, Fill() to fill in a space, and Draw() to draw lines and boxes using awesome ASCII lines.
The tricky part is that each of those can take a bunch of different parameters. For our talk, we’ll simplify it down:
- Position: You can specify where on the screen to write using a
Point, x and y coordinates, or nothing to write at the current cursor position. - Color: You can specify both the fore- and background color, just the foreground color, or neither to use the current colors.
And that’s it. Three methods, and two kinds of parameters with three options each.
Overloads: The Vanilla Solution
So the normal way to address this is by overloading Write(), Fill(), and Draw(). If we go with the vanilla overload solution, we have 27 overloaded methods to implement every combination. Using the code looks like this:
Terminal terminal = new Terminal();
terminal.Write("foo");
terminal.Fill(1, 2, Color.Red);
terminal.Draw(Color.Green, Color.Blue);
Not bad. But implementing it looks like:
public class Terminal
{
public void Write(Point pos,
Color fore, Color back, string text) { /* stuff... */ }
public void Write(int x, int y,
Color fore, Color back, string text) { /* stuff... */ }
public void Write(Color fore,
Color back, string text) { /* stuff... */ }
public void Write(Point pos,
Color fore, string text) { /* stuff... */ }
public void Write(int x, int y,
Color fore, string text) { /* stuff... */ }
public void Write(Color fore,
string text) { /* stuff... */ }
public void Write(Point pos,
string text) { /* stuff... */ }
public void Write(int x, int y,
string text) { /* stuff... */ }
public void Write(string text) { /* stuff... */ }
public void Fill(Point pos,
Color fore, Color back) { /* stuff... */ }
public void Fill(int x, int y,
Color fore, Color back) { /* stuff... */ }
public void Fill(Color fore,
Color back) { /* stuff... */ }
public void Fill(Point pos,
Color fore) { /* stuff... */ }
public void Fill(int x, int y,
Color fore) { /* stuff... */ }
public void Fill(Color fore) { /* stuff... */ }
public void Fill(Point pos) { /* stuff... */ }
public void Fill(int x, int y) { /* stuff... */ }
public void Fill() { /* stuff... */ }
public void Draw(Point pos,
Color fore, Color back) { /* stuff... */ }
public void Draw(int x, int y,
Color fore, Color back) { /* stuff... */ }
public void Draw(Color fore,
Color back) { /* stuff... */ }
public void Draw(Point pos,
Color fore) { /* stuff... */ }
public void Draw(int x, int y,
Color fore) { /* stuff... */ }
public void Draw(Color fore) { /* stuff... */ }
public void Draw(Point pos) { /* stuff... */ }
public void Draw(int x, int y) { /* stuff... */ }
public void Draw() { /* stuff... */ }
}
Lame! Worse, if you start adding more options, it goes up combinatorially. 27 is just for our toy terminal. For my actual terminal lib, it would take 324 overloaded methods to support every combination. There has to be a better way.
Variable Argument Lists: Who Needs Strong Typing Anyway?
It’s true that C# doesn’t support default parameters, but it does support variable length argument lists. Maybe that’s a solution that lets us pass in a variety of options without having to overload. Calling it would still look like:
Terminal terminal = new Terminal();
terminal.Write("foo");
terminal.Fill(1, 2, Color.Red);
terminal.Draw(Color.Green, Color.Blue);
But the class itself just looks like:
public class Terminal
{
public void Write(string text,
params object[] options) { /* stuff... */ }
public void Fill(params object[] options) { /* stuff... */ }
public void Draw(params object[] options) { /* stuff... */ }
}
That doesn’t look so bad. Except when you try to implement one of the methods:
public void Write(string text, params object[] options)
{
Point pos = mCurrentPos;
Color? foreColor = null;
Color? backColor = null;
int? x = null;
int? y = null;
foreach (object obj in options)
{
if (obj is Point)
{
pos = (Point)obj;
}
else if (obj is Color)
{
if (foreColor == null) foreColor = (Color)obj;
else backColor = (Color)obj;
}
else if (obj is int)
{
if (x == null) x = (int)obj;
else y = (int)obj;
}
}
if (x == null) x = mCurrentX;
if (y == null) y = mCurrentY;
// write using pos, color, etc.
}
That’s… just… no. And that doesn’t even have any error handling. The following is totally valid:
terminal.Write("foo", 17, Color.White, Color.Aqua,
Color.Beige, "wtf?");
OK, scratch that.
Parameter Object: Strong Typing is Strong
So we know we want strong typing. How about if we bundle all of the parameters into a parameter object? This is basically the ProcessStartInfo solution.
We define a class something like:
public class TerminalParams
{
// use nullable so that null = default value
public Point? Pos;
public Color? Fore;
public Color? Back;
}
And our Terminal looks something like:
public class Terminal
{
public void Write(TerminalParams paramObj, string text)
{ /* stuff... */ }
public void Fill(TerminalParams paramObj)
{ /* stuff... */ }
public void Draw(TerminalParams paramObj)
{ /* stuff... */ }
}
In C# 3.0 with object initializers, we can call it something like:
Terminal terminal = new Terminal();
terminal.Write(new TerminalParams(), "foo");
terminal.Fill(new TerminalParams
{ Pos = new Point(1, 2), Fore = Color.Red });
terminal.Draw(new TerminalParams
{ Fore = Color.Green, Back = Color.Blue });
I won’t say that’s the greatest thing ever, but it’s not too bad. Of course, if you’re not on 3.0 yet, you’re up a creek. All you’ve basically accomplished is moved your overloads to constructor overloads in TerminalParams. And I still think there’s a little too much… junk… in those lines of code. “new TerminalParams {” and “new Point(...)“, none of that really adds value.
Parameter Groups
Really, the overloaded methods do have the best calling convention so far. Let’s look at it a little closer:
terminal.Write(1, 2, Color.Blue, Color.Red, "foo");
While that’s an uninterrupted list of parameters, they’re conceptually grouped like this:
terminal.Write( 1, 2, Color.Blue, Color.Red, "foo" ); // (position) (color ) (text)
How cool would it be if we could actually group the parameters like that in the code? Something like:
terminal.Write(1, 2)(Color.Blue, Color.Red)("foo");
C# doesn’t have functors, but maybe we’re on to something.
Insert several hours of mucking around with properties that return delegates that return delegates, etc.
Or… not. But let’s not give up yet. Let’s try re-arranging things:
terminal.Write("foo")(1, 2)(Color.Blue, Color.Red);
This is a little better because it puts the method (Write()) next to the one parameter it really needs, the string. The problem is that that line, if it were possible to write, would execute from left to right. So we would have to write before we looked at the parameters.
Let’s do some more shuffling:
terminal(1, 2)(Color.Blue, Color.Red).Write("foo");
This looks promising, but we’re stuck with the fact that C# doesn’t have functors. We can’t do terminal(). Is there anything that sort of looks like ()?
You’re Using What?!
So the question is, “what kind of grouping construct can we define at the object level?” Astute readers are already thinking it: indexers. Right now, some of you may be shuddering in horror at the weirdness we’re about to unleash, but let’s do it anyway. Can we make all of these lines of code work:
terminal.Write("foo");
terminal[new Point(3, 4)].Write("foo");
terminal[1, 2][Color.Red].Write("foo");
terminal[Color.Brown].Write("foo");
terminal[Color.Green, Color.Blue].Draw();
And can we make them work all at the same time? Not only that, can we make the following not work:
// bad, cannot specify position twice
terminal[1, 2][new Point(3, 4)].Write("foo");
// bad, cannot specify half a position
terminal[1].Write("foo");
// sorta bad, for consistency would always like
// to have position before color
terminal[Color.Brown][1, 2].Write("foo");
It turns out, we can. The core idea is to define a chain of interfaces that inherit from each other.
public interface ITerminalMethods
{
void Write(string text);
void Fill();
void Draw();
}
public interface ITerminalColor : ITerminalMethods
{
ITerminalMethods this[Color fore] { get; }
ITerminalMethods this[Color fore, Color back] { get; }
}
public interface ITerminalPosColor : ITerminalColor
{
ITerminalColor this[Point pos] { get; }
ITerminalColor this[int x, int y] { get; }
}
public interface ITerminal : ITerminalPosColor { }
Each one represents one of the parameter types: position, color, etc. The innermost one, ITerminalMethods, contains the actual methods, Write(), Fill(), etc.
The other interfaces define indexers for each parameter type that return the next interface up the chain. The position interface, ITerminalPosColor, has indexers that return the color interface, ITerminalColor, which has indexers which return the method interface, ITerminalMethods. Since the interfaces inherit from each other too, you can also skip a step. You can go from ITerminalPosColor straight to ITerminalMethods because ITerminalColor interface inherits from it.
Calling it looks like:
terminal.Write("foo");
terminal[new Point(3, 4)].Write("foo");
terminal[1, 2][Color.Red].Write("foo");
terminal[Color.Brown].Write("foo");
terminal[Color.Green, Color.Blue].Draw();
This is all well and good for defining it, but is it possible to actually implement this thing?
We can do that too. The idea behind that is that the
public class TerminalData
{
// terminal core data:
public char[] Characters;
public Color[] ForeColors;
public Color[] BackColors;
}
public class Terminal : ITerminal
{
public Terminal() { mData = new TerminalData(); }
#region ITerminalPosColor Members
public ITerminalColor this[Point pos]
{
get { return new Terminal(mData, pos, mFore, mBack); }
}
public ITerminalColor this[int x, int y]
{
get { return new Terminal(mData,
new Point(x, y), mFore, mBack); }
}
#endregion
#region ITerminalColor Members
public ITerminalMethods this[Color fore]
{
get { return new Terminal(mData, mPos, fore, mBack); }
}
public ITerminalMethods this[Color fore, Color back]
{
get { return new Terminal(mData, mPos, fore, back); }
}
#endregion
#region ITerminalMethods Members
public void Write(string text) { /* stuff... */ }
public void Fill() { /* stuff... */ }
public void Draw() { /* stuff... */ }
#endregion
private Terminal(TerminalData data, Point pos,
Color fore, Color back)
{
mData = data;
mPos = pos;
mFore = fore;
mBack = back;
}
private TerminalData mData;
private Point mPos;
private Color mFore;
private Color mBack;
}
Is it Worth Doing?
So this is how my little terminal library works, and I’ve grown pretty fond of it. However, this is a personal project, so my willingness to deal with very idiomatically strange code is pretty high. (Ask me about NotNull<T> sometime.)
In a production environment, this might be too strange looking for some teams. But maybe if it gets popularized enough what was once strange will become familiar.
February 26th, 2008 at 12:28 pm
You’re crazy, man. (in a good way)
February 26th, 2008 at 3:51 pm
It took me a while to figure it all out. Even after I did, I wasn’t sure I understood it.
In my real lib, it’s a little more complex because there’s two types of terminals: a regular terminal that owns its data, and a “window” terminal that represents a rectangular portion of a “real” terminal. Both of which of course support this stuff. So getting the class diagram figured out was a little confusing.
February 27th, 2008 at 1:59 am
I agree, just look at that mess called TextWriter…
February 27th, 2008 at 11:39 pm
Whoa! That is really thinking outside of the box! I never would have considered methods returning delegates or using indexers like that!
I have a different idea: you could try method chaining. The calling syntax would look like this:
terminal.Position(2, 2).ForeColor(Color.Red).Write(”foo”);
You could make a helper class with all of the Draw(), Write(), etc. methods in it, plus Position(), ForeColor(), and BackColor() methods. The Position(), ForeColor() and BackColor() methods each set some state in private fields and then “return this;”. The Draw(), Fill(), etc. methods actually execute something based on the state, so they have to be last and don’t return this;
You could also have the Position(), ForeColor() and BackColor() methods on the Terminal class, but those guys will return a new instantiation of the helper class mentioned in the above paragraph. And you could factor out all of the common methods into an interface and have Terminal and this helper class implement it.
So the following would be legal:
terminal.Position(1,2).ForeColor(Color.Red).Write(”foo”);
terminal.ForeColor(Color.Red).BackColor(Color.Blue).Position(1,2).Draw();
and since this helper object has state, you could even make the following illegal:
terminal.Position(1,2).Position(1,2); // Error: Position set twice
terminal.ForeColor(Color.Blue).Draw(); // Error: no Position set
I give full credit to the Hibernate project for first turning me on to this idea. Sweet, huh?
March 1st, 2008 at 12:19 pm
Yeah, method chaining is cool. That’s how the LINQ stuff works, it I remember right. That’s kind of how I started down this path at all. I think I briefly considered that, but having to type out “Position” when I’m specifying a point seemed kind of redundant to me. I wanted the minimum amount of useless characters in there. :)
The other thing I considered, but didn’t put a lot of effort into was using operator overloading to do it C++ style:
terminal << Color.Blue << “foo”;
But then I never liked that style anyway. Or maybe something like:
terminal.Write(”foo, Point(1, 2) & Color.Blue);
where the second param is a TerminalState class that has implicit cast operators to cast from Point, Color, etc. and an & operator that combines them. But then specifying a point with two ints gets tricky.
June 11th, 2008 at 7:15 am
Very clever.
…Or you could just use C++ hehe
July 7th, 2008 at 1:31 am
I like the indexes approach, if for no other reason then because I would probably never had thought about it myself. But now I am definitely going to give it a try on some of my own solutions.
July 7th, 2008 at 2:08 pm
I have copied your code, but when testing I encountered the following problem (or inconvenience, rather):
Using Visual Studio, I could use Intellisense to write:
(1) terminal.Write(”foo”);
but not:
(2) terminal[Color.Brown].Write(”foo”);
The code (2) still works, but Visual Studio apparently does not recognize it. I’m guessing it’s because the method is not implemented in the object returned by terminal[Color.Brown], i.e. IterminalMethods.
I am calling the problem an inconvenience, but a serious one, since this will prevent me from knowing which methods are actually available, using Intellisense. Thus, I continuously need to have the documentation for the objects at hand, instead of seeing them through Intellisense while coding.
Am I missing something, or is it the indexes approach that is missing something?
July 17th, 2008 at 7:12 am
> The code (2) still works, but Visual Studio apparently does not recognize it.
Unfortunately, I noticed the same thing. It should, and I don’t think it’s a problem with the method not being implemented in the returned class. It’s still defined in the interface type, so the Intellisense should just be for that. My guess is that Intellisense just wasn’t coded to handle this syntax, even though it’s valid. :(