Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Switch without cases (but with default) in System.Linq.Expressions

I have tried to create a switch expression with System.Linq.Expressions:

var value = Expression.Parameter(typeof(int));
var defaultBody = Expression.Constant(0);
var cases1 = new[] { Expression.SwitchCase(Expression.Constant(1), Expression.Constant(1)), };
var cases2 = new SwitchCase[0];
var switch1 = Expression.Switch(value, defaultBody, cases1);
var switch2 = Expression.Switch(value, defaultBody, cases2);

but in the last line I get an ArgumentException:

Non-empty collection required. Parameter name: cases

What is the reason of this exception? May be this a bug in Expression.Switch(…)?

In a C# a switch with "default" part only is correct:

switch(expr) {
default:
  return 0;
}//switch

UPD: I have submitted an issue to the CoreFX repo on GitHub

like image 911
Viacheslav Ivanov Avatar asked Feb 06 '15 07:02

Viacheslav Ivanov


1 Answers

There isn't a complete analogy between C#'s switch and SwitchExpression. In the other direction, consider that you can have:

var value = Expression.Parameter(typeof(int));
var meth = Expression.Lambda<Func<int, string>>(
  Expression.Switch(
    value,
    Expression.Call(value, typeof(object).GetMethod("ToString")),
    Expression.SwitchCase(Expression.Constant("Zero"), Expression.Constant(0, typeof(int))),
    Expression.SwitchCase(Expression.Constant("One"), Expression.Constant(1, typeof(int)))),
    value
  ).Compile();
Console.WriteLine(meth(0)); // Zero
Console.WriteLine(meth(1)); // One
Console.WriteLine(meth(2)); // 2

Here the SwitchExpression returns a value which is something switch cannot do.

So, just as being able to do something with SwitchExpression does not mean you can do it with a switch, so too there's no reason to assume that being able to do something with a switch means you can do it with a SwitchExpression.

That said, I see no good reason why SwitchExpression was set this way, except perhaps that it simplifies the case where an expression has no cases and no default body. That said, I think this was likely just a matter of the expression being generally intended to have multiple cases, and that was what it was coded to support.

I've submitted a pull-request to .NET Core that would allow such case-less expressions, by producing a SwitchExpression where the default value for the type of switchValue has the same body as the default body. This approach means anything that would be surprised by a SwitchExpression with no cases should still cope, avoiding backwards-compatibility issues. The case of there being no default either is dealt with by creating a noop expression that does nothing, so the only case that now still throws an ArgumentException is if there is no case and no default and the type is explicitly set to something other than void, this case being invalid under the typing rules that must obviously still be kept.

[Update: That approach was rejected, but a later pull-request was accepted, so case-less SwitchExpressions are now allowed by .NET Core, though if and when that is adopted by other versions of .NET is another matter].

In the meantime, or if you use another version of .NET, you're best-off using a helper method like:

public static Expression SwitchOrDefault(Type type, Expression switchValue, Expression defaultBody, MethodInfo comparison, IEnumerable<SwitchCase> cases)
{
  if (cases != null)
  {
    // It's possible that cases is a type that can only be enumerated once.
    // so we check for the most obvious condition where that isn't true
    // and otherwise create a ReadOnlyCollection. ReadOnlyCollection is
    // chosen because it's the most efficient within Switch itself.
    if (!(cases is ICollection<SwitchCase>))
      cases = new ReadOnlyCollection<SwitchCase>(cases);
    if (cases.Any())
      return Switch(type, switchValue, defaultBody, comparison, cases);
  }
  return Expression.Block(
    switchValue, // include in case of side-effects.
    defaultBody != null ? defaultBody : Expression.Empty() // replace null with a noop expression.
  );
}

Overloads like:

public static Expression SwitchOrDefault(Expression switchValue, Expression defaultBody, params SwitchCase[] cases)
{
  return SwitchOrDefault(switchValue, defaultBody, null, (IEnumerable<SwitchCase>)cases);
}

And so on can then be added.

This results in a trimmer Expression overall than my pull-request, because it cuts out the switch entirely in the no-cases case and just returns the default body. If you really need to have a SwitchExpression then you could create a similar helper method that follows the same logic as that pull-request does in creating a new SwitchCase and then using that.

like image 70
Jon Hanna Avatar answered Sep 22 '22 06:09

Jon Hanna