8

I have a few questions about the System.Linq.Expressions.LabelExpression and its ancillary classes and methods.

1) The documentation for the LabelExpression class reads thus:

Represents a label, which can be put in any Expression context. If it is jumped to, it will get the value provided by the corresponding GotoExpression. Otherwise, it receives the value in DefaultValue. If the Type equals System.Void, no value should be provided.

What does it mean to return a value to a label target? In other words, what does it mean for a label target to receive a value? I've never done that in my life -- pass a value to a label target when I jump to it?

2) While it perfectly makes sense to go to a label target, what does it mean to return to and continue to and break to a label target?

  1. public static GotoExpression Return(LabelTarget target);
  2. public static GotoExpression Continue(LabelTarget target);
  3. public static GotoExpression Break(LabelTarget target)
Water Cooler v2
  • 32,724
  • 54
  • 166
  • 336
  • 1
    Definitely better; thanks! – Andrew Barber Dec 17 '14 at 14:06
  • Curious about this myself. It appears that the Return, Continue, and Break all return essentially the same GotoExpression with the different being the .Kind property. This property is described as "informational only" – Grax32 Dec 17 '14 at 14:11
  • Thanks, @Grax. I guess we empathize with each other. It is confusing. You're describing an informational property named `GotoExpressionKind`, which is an enum, which is for informational purposes only. However, I am asking about the meaning of two things: (1) How do you return, continue to, or break to a label target? and (2) How can a label receive a value? – Water Cooler v2 Dec 17 '14 at 14:53

2 Answers2

7

It is sometimes helpful to think of Linq Expressions as a way to build code in something that resembles C#, but isn't C# exactly. This is one of those times.

The below code is an implementation of a Math.Max(int a, int b) using Expressions. There is no shortcut for the return statements like in C#. You have to create labels.

        // (a, b => 
        // {
        //      if(a > b)
        //          return a;
        //      else
        //          return b;
        // }

        var a = Expression.Parameter(typeof(int), "a");
        var b = Expression.Parameter(typeof(int), "b");
        var returnLabel = Expression.Label(typeof (int));
        Expression<Func<int, int, int>> returnMax = (Expression<Func<int, int, int>>)Expression.Lambda
            (
                Expression.Block
                (
                    Expression.IfThenElse
                    (
                        Expression.GreaterThan(a, b),
                        Expression.Return(returnLabel, a),
                        Expression.Return(returnLabel, b)
                    ),
                    Expression.Label(returnLabel, Expression.Constant(0))
                ),
                a,
                b
            );
        var shouldBeSix = returnMax.Compile()(5, 6);

The key to understanding why the LabelExpression needs a value: Expressions are always typed (for our purposes here, void is a type), and almost always return a value. A BlockExpression, for example, take on the value of the last statement. An AssignExpression takes on the value of assignment. Similarly, a LabelExpression must return a value. When used in conjunction with a GotoExpression of any sort, that default value is never used, but the following code is legal:

        var returnLabel = Expression.Label(typeof (int));
        Expression<Func<int>> returnsSix = (Expression<Func<int>>)Expression.Lambda
            (
                Expression.Label(
                    returnLabel, 
                    Expression.Constant(6)
                )
            );

        var alsoSix = returnsSix.Compile()();

... hence the need for a default value.

Since a LabelExpression must have a type, and a value, the types of the default value, the LabelTarget and the GotoExpression all must match. The original sample code uses 0 as a default value, but as you can see, that will never be used. If you switch the 0 for 0.0 or null, the Expression will fail on the .Compile() call.

2) As you can see from the sample code, there is no way to 'return' out of a function without using a label target. As @Grax implied, Expression.Goto, Expression.Continue, Expression.Break, Expression.Return all return GotoExpressions that function almost identically.

Shlomo
  • 14,102
  • 3
  • 28
  • 43
  • What a beautiful explanation! I love @Grax's answer too, but I have to give it to you for the astounding clarity of language and structure. You answer turned the key in my mind. Just one thing, though: with the documentation not at all having much, *how* did you know all this? You know if you tell me that, you'll be teaching the man to fish, and all. – Water Cooler v2 Dec 18 '14 at 09:57
  • 1
    At one point, I worked on a project where we looked at using Expressions extensively. There was a lot of trial & error. If you want a fun, masochistic, challenge: There's no built in short-cut to make a for-loop (`Expression.Loop` essentially makes a while loop). Try building one. – Shlomo Dec 18 '14 at 13:46
  • 1
    I also found the first couple posts of this blog series helpful: http://blogs.msdn.com/b/mattwar/archive/2008/11/18/linq-links.aspx – Shlomo Dec 18 '14 at 13:56
  • Thanks, @Shlomo. Over the last year or so, I've gone through the posts on The Wayward Weblog. I'm going to be banging out code for composing and visiting all the 73 families (I've been at work counting and categorizing) of expressions in the `System.Linq.Expressions` namespace soon. :-) – Water Cooler v2 Dec 18 '14 at 17:19
1

The purpose of the label value appears to be to provide a return value. If you look at my sample code below, the return value of "payload" is passed to the "target" label and becomes the return code for the expression. I tried Expression.Return and Expression.Break and got the same results for either. Expression.Continue did not have the overload to pass a value to the Label.

var target = Expression.Label(typeof(string));
var debugPrint = typeof(Debug).GetMethod("Print", new Type[] { typeof(string) });

var expr = Expression.Block(typeof(string),
    new Expression[] {
        Expression.Call(debugPrint,Expression.Constant("Before")),
        Expression.Return(target,Expression.Constant("payload"),typeof(string)),
        //Expression.Break(target,Expression.Constant("payload")),
        Expression.Call(debugPrint,Expression.Constant("During")),
        Expression.Label(target,Expression.Constant("Default")),
    }
);

var result = Expression.Lambda<Func<string>>(expr).Compile()();

This expression is roughly equivalent to the method below.

string Demo()
{
    Debug.Print("Before");
    return "payload";
    Debug.Print("During");
    return "Default";
}

To address the second question: You always "go to" a label target. "Return", "Continue", and "Break" are styles of how you might "go to" the target. Return implies that the label target is at the end of the method and you pass it the return value. Continue and Break imply that the label target is participating in a loop.

Grax32
  • 3,986
  • 1
  • 17
  • 32