Switch Composition in C#
Some time ago, I encountered a situation where I had to use three switch statements, and the results of the first one were used as arguments for the second. I needed this approach for the PhoneNumber primitive in the ToString method to support multiple formats. Here's an example of how that might look:
The entire code is provided as an example to illustrate the underlying motivation behind the approach.
public string ToString(string? format, IFormatProvider? formatProvider)
{
if (string.IsNullOrEmpty(format))
{
format = "e"; //default format
}
switch (format)
{
case "e":
return Number;
case "i":
return Util.Format(_phoneNumber, PhoneNumberFormat.INTERNATIONAL);
case "r":
return Util.Format(_phoneNumber, PhoneNumberFormat.RFC3966);
case "n":
return Util.Format(_phoneNumber, PhoneNumberFormat.NATIONAL);
default:
throw new FormatException("Bad phone format.");
}
}At first, I didn't feel satisfied with this code because the responsibilities seemed too muddled. There was no clear separation of concerns: default configuration, format mapping, and actual formatting were all mixed together. So, I started refactoring it and ended up with something like this:
public string ToString(string? format, IFormatProvider? formatProvider)
{
const char defaultFormat = 'e';
format = format switch
{
null or "" => "e",
var f => f,
};
var internalFormat = format switch
{
"e" => PhoneNumberFormat.E164,
"i" => PhoneNumberFormat.INTERNATIONAL,
"r" => PhoneNumberFormat.RFC3966,
"n" => PhoneNumberFormat.NATIONAL,
_ => throw new FormatException("Bad phone format.")
};
return internalFormat switch
{
PhoneNumberFormat.E164 => Number,
var f => Util.Format(_phoneNumber, f)
};
}This version clearly separates the default format handling, the mapping of the format to an internal format, and the actual formatting. But after thinking about it more, I wondered if I could further improve it by combining everything into a single composition. The result was this version:
public string ToString(string? format, IFormatProvider? formatProvider)
{
return format switch
{
null or "" => "e",
var f => f
}
switch
{
"e" => PhoneNumberFormat.E164,
"i" => PhoneNumberFormat.INTERNATIONAL,
"r" => PhoneNumberFormat.RFC3966,
"n" => PhoneNumberFormat.NATIONAL,
_ => throw new FormatException("Bad phone format.")
}
switch
{
PhoneNumberFormat.E164 => Number,
var f => Util.Format(_phoneNumber, f)
};
}It's strange and unusual; even formatting tools have trouble understanding what's going on here. However, it's a fully compilable and functional solution, and I actually like how it looks.