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

1

12

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

Pice answered 6/2, 2015 at 7:37 Comment(5)
What is the goal of such a construction? switch with default and no case would just execute defaultFrontality
For me switch without cases looks pretty meaningless, so I think this exception is reasonable.Mathematical
Yes, the C# spec says a switch-block has of zero or more switch-sections; but that doesn't mean that a switch expression has to confom to the C# spec. Since you create the expression at runtime, you could, as a workaround, simply add an Expression.SwitchCase with a value that is != switch value; or add the body of the default case as a switch case with value = switch value.Arango
@AndyKorneyev while I can't think of much of a reason to have the C# equivalent, I could certainly see how this would be useful when creating a switch dynamically; if you are going to have a variable number of SwitchCases depending on some criteria, you don't have to special-case the condition of their being zero.Microparasite
@JonHanna Yes, this is exactly my case.Pice
M
7

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.

Microparasite answered 14/8, 2015 at 11:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.