looking for a better switch/case statement?
I find myself sometimes wishing I had a better switch/case construct in C#.
My requirements of "better" are:
- Case statements are automatically scoped
- I can switch on more than simple types
- I can have custom logic for my matching criteria
But why not just use if/else if/else...?
Good question, I'm glad you asked! However, I don't have an excellent answer other than personal preference. if/else if/else is okay sometimes, but other times I have a deterministic set of conditions, and a switch type construct feels more symmetrical to me. I loves me some symmetric feeling code, it just feels cleaner somehow.
So I'll present a somewhat contrived example just so you can see what the usage looks like, then we'll see the code.
Usage
public void DoSomething()
{
object foo = new object();
object bar = new object();
object baz = new object();
Switch<object>.On(baz)
.Case(x => x.Equals(foo), () =>
{
Console.WriteLine("came into foo case");
})
.Case(x => x.Equals(bar), () =>
{
Console.WriteLine("came into bar case");
})
.Default( () =>
{
Console.WriteLine("came into default case");
})
.Go();
}
The output from the above, is as you'd expect, the default case, since baz wasn't equal to foo or bar.
Implementation
public class Switch<T>
{
private T _target;
private List<KeyValuePair<Predicate<T>, Action>> _cases;
private Action _default;
public static Switch<T> On(T target)
{
return new Switch<T>(target);
}
public Switch(T target)
{
_target = target;
_cases = new List<KeyValuePair<Predicate<T>, Action>>();
}
public Switch<T> Case(Predicate<T> @case, Action action)
{
_cases.Add(new KeyValuePair<Predicate<T>, Action>(@case, action));
return this;
}
public Switch<T> Default(Action action)
{
_default = action;
return this;
}
public void Go()
{
foreach(var @case in _cases)
if (@case.Key(_target))
{
@case.Value();
return;
}
if (_default != null)
_default();
}
}
The only part I don't really like is having to kick it off with the Go() at the end. Any ideas for a better way to kick it off, or at least a better name for it (instead of Go).
Feedback? Ideas?
Update
Paul had a great idea a couple great ideas (see comments) on how to get rid of the Go() at the end, and to force Default to only appear at the end. Here is the new implementation. Usage is the same, just remove the Go().
public class Switch<T>
{
private T _target;
private bool _alreadyMatched;
public static Switch<T> On(T target)
{
return new Switch<T>(target);
}
public Switch(T target)
{
_target = target;
_alreadyMatched = false;
}
public Switch<T> Case(Predicate<T> @case, Action action)
{
if (!_alreadyMatched && @case(_target))
{
_alreadyMatched = true;
action();
}
return this;
}
public void Default(Action action)
{
if (!_alreadyMatched)
{
_alreadyMatched = true;
action();
}
return this;
}
}

September 4th, 2008 - 14:21
What if you fire the action inside the Case() method instead of storing it? Then you wouldn’t need to call Go() at the end either.
The Case method could look something like this
public Switch Case(Predicate @case, Action action)
{
if (@case(_target))
action;
return this;
}
and the Default method would change to
public Switch Default(Action action)
{
action;
return this;
}
and you wouldn’t need the _cases and _default private fields.
September 4th, 2008 - 14:30
I just realized a flaw in my design. If 2 Cases are met, then both actions will be fired instead of only the first case.
An additional private flag will be needed to keep track if a Case has been met, and that flag will have to be checked before running an action
public Switch Case(Predicate @case, Action action)
{
if(!_caseMet && @case(_target))
{
_caseMet = true;
action;
}
return this;
}
September 4th, 2008 - 14:39
Awesome Idea! Thanks Paul! There is also one more change in behavior. If you specify your Default at some point other than the end of your list of Cases, it’ll catch every time and it will never get to the cases below your Default.
I’ll update the post with the updated implementation.
September 4th, 2008 - 15:09
Good point about the Default() method bug. Below is such an example:
int age;
Switch.On(“Paul”)
.Case(x => x == “George”, () => { age = 40; })
.Default( () => { age = 70; })
.Case(x => x == “Paul”, () => { age = 25; });
In this situation age would be set to 70 instead of 25.
I think the Default() method signature will have to be updated to return void instead of Switch. This will ensure that Default() is always at the end of the statement and is only called if none of the Cases have been met.