Expression Tree Practice: C# Loops and Loop Control

2019年12月15日 2855点热度 0人点赞 1条评论
内容目录

Expression Tree Practice: C# Loops

[TOC]

C# provides several types of loops.

Loop Type Description
while Loop Repeats a statement or group of statements while a given condition is true. It tests the condition before executing the loop body.
for/foreach Loop Executes a sequence of statements multiple times, simplifying the code for managing loop variables.
do...while Loop Similar to the while statement, except that the condition is tested at the end of the loop body.
Nested Loop You can use one or more loops within a while, for, or do..while loop.

Of course, there are also the following statements for controlling loops:

Control Statement Description
break Statement Terminates a loop or switch statement, and the program control continues with the statement immediately following the loop or switch.
continue Statement Causes the loop to skip the remaining body and immediately tests the condition again.

LabelTarget

LabelTarget is used to create loop labels.

Whether for or while, when writing loops, there must be a condition to exit the loop; sometimes a parameter needs to be incremented or decremented and used as the basis for the decision.

In C# expression trees, there isn't a dedicated representation for for/while; there is only a Loop. Let's take a look at the expression tree generated by Loop.

.Lambda #Lambda1<System.Func`1[System.Int32]>() {
    .Block(System.Int32 $x) {
        $x = 0;
        .Loop  {
            .If ($x < 10) {
                $x++
            } .Else {
                .Break #Label1 { $x }
            }
        }
        .LabelTarget #Label1:
    }
}

To implement loop control, there are two kinds of Expressions: break and continue.

        public static GotoExpression Break(LabelTarget target, Type type);
    public static GotoExpression Break(LabelTarget target, Expression value);

    public static GotoExpression Break(LabelTarget target);

    public static GotoExpression Break(LabelTarget target, Expression value, Type type);</code></pre>
        public static GotoExpression Continue(LabelTarget target, Type type);

        public static GotoExpression Continue(LabelTarget target);

Therefore, to implement loop control, LabelTarget must be used; otherwise, it will result in an infinite loop.

The best way to understand LabelTarget is to practice hands-on.

for / while Loop

Expression.Loop is used to create loops, including for and while, defined as follows:

        public static LoopExpression Loop(Expression body, LabelTarget @break, LabelTarget @continue);
  System.Linq.Expressions.LoopExpression.
    public static LoopExpression Loop(Expression body);

    public static LoopExpression Loop(Expression body, LabelTarget @break);</code></pre>

Within the expression tree, there is only Loop, with no distinction between for/while.

So, let's understand Loop looping and LabelTarget step by step.

Infinite Loop

                while (true)
                {
                    Console.WriteLine("Infinite Loop");
                }

So the corresponding Loop overload is as follows:

public static LoopExpression Loop(Expression body)

Using expression trees to write:

            BlockExpression _block = Expression.Block(
                new ParameterExpression[] { },
                Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),Expression.Constant("Infinite Loop") )
            );
        LoopExpression _loop = Expression.Loop(_block);

        Expression&lt;Action&gt; lambda = Expression.Lambda&lt;Action&gt;(_loop);
        lambda.Compile()();</code></pre>

The Simplest Loop

If I want to achieve the simplest loop like this using expression trees, how would I write it?

            while (true)
            {
                Console.WriteLine("I execute once and then end the loop");
                break;
            }

Expression tree writing:

            LabelTarget _break = Expression.Label();
        BlockExpression _block = Expression.Block(
           new ParameterExpression[] { },
           Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;I execute once and then end the loop&quot;)), Expression.Break(_break));
        LoopExpression _loop = Expression.Loop(_block, _break);

        Expression&lt;Action&gt; lambda = Expression.Lambda&lt;Action&gt;(_loop);
        lambda.Compile()();

        Console.ReadKey();</code></pre>

The generated expression tree:

.Lambda #Lambda1<System.Action>() {
    .Loop  {
        .Block() {
            .Call System.Console.WriteLine("I execute once and then end the loop");
            .Break #Label1 { }
        }
    }
    .LabelTarget #Label1:
}

Firstly, it's important to note that Expression.Label() can be empty; it acts as a label and does not participate in passing parameters or calculations. It can be either parameterless or with parameters, as long as they match in number and type.

However, the above loop only runs once. You can try changing the label above like this: LabelTarget _break = Expression.Label(typeof(int)); for reasons that will be explained later.

Moreover, the Expression.Label() variable needs to be consistent; otherwise, output cannot be exited.

Try the following code:

            BlockExpression _block = Expression.Block(
               new ParameterExpression[] { },
               Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }), Expression.Constant("I execute once and then end the loop")), Expression.Break(Expression.Label()));
            LoopExpression _loop = Expression.Loop(_block, Expression.Label());
        Expression&lt;Action&gt; lambda = Expression.Lambda&lt;Action&gt;(_loop);
        lambda.Compile()();

        Console.ReadKey();</code></pre>

Here we utilized Expression.Block(); Block() refers to a block, i.e., {}.

If Block() is at the outermost level, it is equivalent to a function; if it is nested, it is akin to {};

But it's not exactly like that... the expression tree does not fully reconstruct the operations according to C# syntax.

With regard to the use of Block(), more practice is needed.

Multiple Loops

Write a loop statement that loops ten times:

            for (int i = 0; i < 10; i++)
            {
                if (i < 10)
                {
                    Console.WriteLine(i);
                }
                else
                    break;
            }

Or use while:

            int i = 0;
            while (true)
            {
                if (i < 10)
                {
                    Console.WriteLine(i);
                }
                else
                    break;
                i++;
            }

Using expression trees to write:

            LabelTarget _break = Expression.Label(typeof(int));
            ParameterExpression a = Expression.Variable(typeof(int), "a");
        BlockExpression _block = Expression.Block(new ParameterExpression[] { },
            Expression.IfThenElse
            (
                Expression.LessThan(a, Expression.Constant(10)),
                Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), a),
                Expression.Break(_break, a)
            ),
            Expression.PostIncrementAssign(a)   // a++
            );

        LoopExpression _loop = Expression.Loop(_block, _break);

        Expression&lt;Action&lt;int&gt;&gt; lambda = Expression.Lambda&lt;Action&lt;int&gt;&gt;(_loop, a);
        lambda.Compile()(0);
        Console.ReadKey();</code></pre>

The generated expression tree is as follows:

.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $a) {
    .Loop  {
        .Block() {
            .If ($a < 10) {
                .Call System.Console.WriteLine($a)
            } .Else {
                .Break #Label1 { $a }
            };
            $a++
        }
    }
    .LabelTarget #Label1:
}

Try changing Expression.Break(_break, a) to Expression.Break(_break) and see what error you get...

The solution is to change the above label to LabelTarget _break = Expression.Label();.

Just like writing comments in code, the components inside are meant to make the code easier for others to understand.

Some students get caught up in Expression.Label(有参或无参); and Expression.Break(_break, a) vs. Expression.Break(_break); just look at the final generated expression tree to clarify.

Using break and continue Together

The C# loop code is as follows

            int i = 0;
            while (true)
            {
                if (i < 10)
                {
                    if (i % 2 == 0)
                    {
                        Console.Write("i is even:");
                        Console.WriteLine(i);
                        i++;
                        continue;
                    }
                    Console.WriteLine("Other task --");
                    Console.WriteLine("Other task --");
                }
                else break;
                i++;
            }

Written using C# Expression Trees (the author has detailed the steps, hence the code is longer)

            ParameterExpression a = Expression.Variable(typeof(int), "a");
        LabelTarget _break = Expression.Label();
        LabelTarget _continue = Expression.Label();

        //        if (i % 2 == 0)
        //        {
        //            Console.Write(&quot;i is even:&quot;);
        //            Console.WriteLine(i);
        //            i++;
        //            continue;
        //        }
        ConditionalExpression _if = Expression.IfThen(
            Expression.Equal(Expression.Modulo(a, Expression.Constant(2)), Expression.Constant(0)),
            Expression.Block(
                new ParameterExpression[] { },
                Expression.Call(null, typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;i is even:&quot;)),
                Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), a),
                Expression.PostIncrementAssign(a),
                Expression.Continue(_continue)
                )
            );

        //        if (i % 2 == 0)
        //        {
        //            Console.Write(&quot;i is even:&quot;);
        //            Console.WriteLine(i);
        //            i++;
        //            continue;
        //        }
        //        Console.WriteLine(&quot;Other task --&quot;);
        //        Console.WriteLine(&quot;Other task --&quot;);
        BlockExpression block1 = Expression.Block(
            new ParameterExpression[] { },
            _if,
            Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;Other task --&quot;)),
            Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;Other task --&quot;))
            );

        //    if (i < 10)
        //    {
        //        if (i % 2 == 0)
        //        {
        //            Console.Write(&quot;i is even:&quot;);
        //            Console.WriteLine(i);
        //            i++;
        //            continue;
        //        }
        //        Console.WriteLine(&quot;Other task --&quot;);
        //        Console.WriteLine(&quot;Other task --&quot;);
        //    }
        //    else break;
        ConditionalExpression if_else = Expression.IfThenElse(
           Expression.LessThan(a, Expression.Constant(10)),
            block1,
            Expression.Break(_break)
            );

        //    if (i < 10)
        //    {
        //        if (i % 2 == 0)
        //        {
        //            Console.Write(&quot;i is even:&quot;);
        //            Console.WriteLine(i);
        //            i++;
        //            continue;
        //        }
        //        Console.WriteLine(&quot;Other task --&quot;);
        //        Console.WriteLine(&quot;Other task --&quot;);
        //    }
        //    else break;
        //    i++ ;

        BlockExpression block2 = Expression.Block(
            new ParameterExpression[] { },
            if_else,
            Expression.PostIncrementAssign(a)
            );
        // while(true)
        LoopExpression loop = Expression.Loop(block2, _break, _continue);

        Expression&lt;Action&lt;int&gt;&gt; lambda = Expression.Lambda&lt;Action&lt;int&gt;&gt;(loop, a);
        lambda.Compile()(0);
        Console.ReadKey();</code></pre>

The generated expression tree is as follows

.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $a) {
    .Loop .LabelTarget #Label1: {
        .Block() {
            .If ($a < 10) {
                .Block() {
                    .If (
                        $a % 2 == 0
                    ) {
                        .Block() {
                            .Call System.Console.Write("i is even:");
                            .Call System.Console.WriteLine($a);
                            $a++;
                            .Continue #Label1 { }
                        }
                    } .Else {
                        .Default(System.Void)
                    };
                    .Call System.Console.WriteLine("Other task --");
                    .Call System.Console.WriteLine("Other task --")
                }
            } .Else {
                .Break #Label2 { }
            };
            $a++
        }
    }
    .LabelTarget #Label2:
}

To make it easier to understand, the above code has been broken down into many steps.

Here is a simplified version

            ParameterExpression a = Expression.Variable(typeof(int), "a");
        LabelTarget _break = Expression.Label();
        LabelTarget _continue = Expression.Label();

        LoopExpression loop = Expression.Loop(
            Expression.Block(
                new ParameterExpression[] { },
                Expression.IfThenElse(
                    Expression.LessThan(a, Expression.Constant(10)),
                    Expression.Block(
                        new ParameterExpression[] { },
                        Expression.IfThen(
                            Expression.Equal(Expression.Modulo(a, Expression.Constant(2)), Expression.Constant(0)),
                            Expression.Block(
                                new ParameterExpression[] { },
                                Expression.Call(null, typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;i is even:&quot;)),
                                Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), a),
                                Expression.PostIncrementAssign(a),
                                Expression.Continue(_continue)
                                )
                            ),
                        Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;Other task --&quot;)),
                        Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;Other task --&quot;))
                        ),
                    Expression.Break(_break)
                    ),
                Expression.PostIncrementAssign(a)
                ),
            _break,
            _continue
            );

        Expression&lt;Action&lt;int&gt;&gt; lambda = Expression.Lambda&lt;Action&lt;int&gt;&gt;(loop, a);
        lambda.Compile()(0);
        Console.ReadKey();</code></pre>

It is important to note that Expression.Break and Expression.Continue have distinct differences.

When label instances are instantiated with Expression.Label(),

Expression.Break(label);
Expression.Continue(label);

The difference is that continue can only be used with Expression.Label().

Break can be used like this

LabelTarget label = Expression.Label(typeof(int));
ParameterExpression a = Expression.Variable(typeof(int), "a");

Expression.Break(label, a)

痴者工良

高级程序员劝退师

文章评论