Lambda expressions
As early as C #1.0, C # introduced the concept of the delegate type. By using this type, we can pass functions as parameters. In a sense, a delegate can be understood as a managed strong type function pointer.
Generally, it takes some steps to use delegation to pass functions:
- Defines a delegate that includes the specified parameter type and return value type.
- In the method that needs to receive function parameters, use the delegate type to define the parameter signature of the method.
- Creates a delegate instance for the specified passed function.
This may sound complicated, but it does. Steps 1 and 2 are not required. The C # compiler can complete this step.
Fortunately, generic is introduced in C #2.0. Now we can write generic classes, generic methods, and the most important: generic delegation. Nevertheless, microsoft realized that only two generic delegation could meet 3.5 of the requirements until. Net 99%:
- Action: no input parameter, no return value
- Action <t1,..., T16>: 1-16 input parameters are supported, and no return value is returned.
- Func <t1,..., T16, tout>: Supports 1-16 input parameters with return values.
The action delegate returns the void type, and the func delegate returns the value of the specified type. By using these two types of delegation, the above step 1 can be omitted in most cases. However, step 2 is still required, but only action and func are required.
So what if I just want to execute some code? In C #2.0, an anonymous function is created. Unfortunately, this syntax is not popular. The following is an example of a simple anonymous function:
Func<double, double> square = delegate(double x) { return x * x; };
To improve these syntaxes, lambda expressions are introduced in the. NET 3.5 framework and C #3.0.
First, let's take a look at the origins of lambda expressions. In fact, this name is derived from λ in calculus mathematics. Its meaning is to declare what a function needs. More specifically, it describes a mathematical logic system that expresses computation by combining and replacing variables. Therefore, we basically have 0-N input parameters and a return value. In programming languages, we also provide void support without return values.
Let's look at some examples of lambda expressions:
1 // The compiler cannot resolve this, which makes the usage of var impossible! 2 // Therefore we need to specify the type. 3 Action dummyLambda = () => 4 { 5 Console.WriteLine("Hello World from a Lambda expression!"); 6 }; 7 8 // Can be used as with double y = square(25); 9 Func<double, double> square = x => x * x;10 11 // Can be used as with double z = product(9, 5);12 Func<double, double, double> product = (x, y) => x * y;13 14 // Can be used as with printProduct(9, 5);15 Action<double, double> printProduct = (x, y) => { Console.WriteLine(x * y); };16 17 // Can be used as with 18 // var sum = dotProduct(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 });19 Func<double[], double[], double> dotProduct = (x, y) =>20 {21 var dim = Math.Min(x.Length, y.Length);22 var sum = 0.0;23 for (var i = 0; i != dim; i++)24 sum += x[i] + y[i];25 return sum;26 };27 28 // Can be used as with var result = matrixVectorProductAsync(...);29 Func<double[,], double[], Task<double[]>> matrixVectorProductAsync =30 async (x, y) =>31 {32 var sum = 0.0;33 /* do some stuff using await ... */34 return sum;35 };
From these statements, we can directly understand:
- If there is only one input parameter, parentheses can be omitted.
- If there is only one line of statement, and return in this statement, the braces can be omitted, and the return keyword can also be omitted.
- By using the async keyword, lambda expressions can be declared as Asynchronous execution.
- In most cases, the VaR declaration may not be available and can be used only in some special cases.
When VaR is used, if the compiler cannot deduce the delegate type through the parameter type and return value type, an error message "cannot assign Lambda expression to an implicitly-typed local variable." will be thrown. Let's take a look at the following examples:
Now we have learned most of the basic knowledge, but some of the cool lambda expressions have not been mentioned yet.
Let's take a look at this Code:
1 var a = 5;2 Func<int, int> multiplyWith = x => x * a;3 4 var result1 = multiplyWith(10); // 505 a = 10;6 var result2 = multiplyWith(10); // 100
We can see that the peripheral variables, that is, closures, can be used in lambda expressions.
1 static void DoSomeStuff() 2 { 3 var coeff = 10; 4 Func<int, int> compute = x => coeff * x; 5 Action modifier = () => 6 { 7 coeff = 5; 8 }; 9 10 var result1 = DoMoreStuff(compute); // 5011 12 ModifyStuff(modifier);13 14 var result2 = DoMoreStuff(compute); // 2515 }16 17 static int DoMoreStuff(Func<int, int> computer)18 {19 return computer(5);20 }21 22 static void ModifyStuff(Action modifier)23 {24 modifier();25 }
What happened here? First, we create a local variable and two lambda expressions. The first Lambda expression shows that it can access the local variable in other scopes. In fact, this shows powerful capabilities. This means that we can protect a variable, but we can still access it in other methods, without worrying that the method is defined in the current class or other classes.
The second Lambda expression demonstrates the ability to modify peripheral variables in lambda expressions. This means that by passing lambda expressions between functions, we can modify local variables in other scopes in other methods. Therefore, I think closures are particularly powerful, but sometimes some unexpected results may be introduced.
1 var buttons = new Button[10]; 2 3 for (var i = 0; i < buttons.Length; i++) 4 { 5 var button = new Button(); 6 button.Text = (i + 1) + ". Button - Click for Index!"; 7 button.OnClick += (s, e) => { Messagebox.Show(i.ToString()); }; 8 buttons[i] = button; 9 }10 11 //What happens if we click ANY button?!
What is the result of this strange question? Is button 0 0, and button 1 1 1? The answer is: 10 is displayed for all buttons!
As the for loop traverses, the local variable I value has been changed to the length of buttons 10. A simple solution is similar:
1 var button = new Button();2 var index = i;3 button.Text = (i + 1) + ". Button - Click for Index!";4 button.OnClick += (s, e) => { Messagebox.Show(index.ToString()); };5 buttons[i] = button;
Copy the value of variable I by defining the index.
Note: If you use Visual Studio 2012 or a later version for testing, the test results may be different because the compiler used is different from Visual Studio 2010. See: Visual C # Breaking changes in Visual Studio 2012
Expression Tree
When using lambda expressions, an important question is how the target method knows the following information:
- What is the name of the variable we passed?
- What is the structure of the expression body we use?
- What types are used in the expression body?
Now, the expression tree helps us solve the problem. It allows us to look at the specific expressions generated by the compiler. In addition, we can execute a given function, just like using func and action delegate. It also allows us to parse lambda expressions at runtime.
Let's look at an example to describe how to use the expression type:
1 Expression<Func<MyModel, int>> expr = model => model.MyProperty;2 var member = expr.Body as MemberExpression;3 var propertyName = memberExpression.Member.Name; //only execute if member != null
The above is a simple example of expression usage. The principle is very direct: by forming an expression-type object, the compiler generates metadata information based on the parsing of the Expression Tree. The resolution tree contains all relevant information, such as parameters and method bodies.
The method body contains the entire parsing tree. With this, we can access operators, operation objects, and complete statements. The most important thing is to access the names and types of returned values. Of course, the returned variable name may be null. In most cases, we are still very interested in the content of the expression. The benefit for developers is that we no longer misspelled the attribute name, because each spelling error will cause compilation errors.
If the programmer only wants to know the name of the call property, there is a simpler and more elegant way. You can use the special parameter property callermembername to obtain the name of the called method or attribute. The compiler automatically records these names. Therefore, if we only need to know these names without more type information, we can refer to the following code:
1 string WhatsMyName([CallerMemberName] string callingName = null)2 {3 return callingName;4 }
Performance of lambda expressions
There is a big question: How fast is a Lambda expression? Of course, we expect it to be as fast as regular functions, because lambda expressions are also generated by the compiler. In the next section, we will see that the msil generated for lambda expressions is not much different from the conventional functions.
A very interesting discussion is about whether closures in lambda expressions are faster than using global variables, the most interesting part is whether the performance is affected when all available variables are in the local scope.
Let's look at some code to measure various performance benchmarks. Through these four different benchmark tests, we should have enough evidence to illustrate the differences between conventional functions and lambda expressions.
1 class StandardBenchmark : Benchmark 2 { 3 static double[] A; 4 static double[] B; 5 6 public static void Test() 7 { 8 var me = new StandardBenchmark(); 9 10 Init();11 12 for (var i = 0; i < 10; i++)13 {14 var lambda = LambdaBenchmark();15 var normal = NormalBenchmark();16 me.lambdaResults.Add(lambda);17 me.normalResults.Add(normal);18 }19 20 me.PrintTable();21 }22 23 static void Init()24 {25 var r = new Random();26 A = new double[LENGTH];27 B = new double[LENGTH];28 29 for (var i = 0; i < LENGTH; i++)30 {31 A[i] = r.NextDouble();32 B[i] = r.NextDouble();33 }34 }35 36 static long LambdaBenchmark()37 {38 Func<double> Perform = () =>39 {40 var sum = 0.0;41 42 for (var i = 0; i < LENGTH; i++)43 sum += A[i] * B[i];44 45 return sum;46 };47 var iterations = new double[100];48 var timing = new Stopwatch();49 timing.Start();50 51 for (var j = 0; j < iterations.Length; j++)52 iterations[j] = Perform();53 54 timing.Stop();55 Console.WriteLine("Time for Lambda-Benchmark: \t {0}ms", 56 timing.ElapsedMilliseconds);57 return timing.ElapsedMilliseconds;58 }59 60 static long NormalBenchmark()61 {62 var iterations = new double[100];63 var timing = new Stopwatch();64 timing.Start();65 66 for (var j = 0; j < iterations.Length; j++)67 iterations[j] = NormalPerform();68 69 timing.Stop();70 Console.WriteLine("Time for Normal-Benchmark: \t {0}ms", 71 timing.ElapsedMilliseconds);72 return timing.ElapsedMilliseconds;73 }74 75 static double NormalPerform()76 {77 var sum = 0.0;78 79 for (var i = 0; i < LENGTH; i++)80 sum += A[i] * B[i];81 82 return sum;83 }84 }
Of course, with Lambda expressions, we can write the code above more elegantly. The reason for this writing is to prevent interference with the final result. Therefore, we only provide three necessary methods, one of which is responsible for executing Lambda testing, the other is responsible for testing general functions, and the third method is in general functions. The fourth method is our Lambda expression, which is already embedded in the first method. The calculation method is not important. We use random numbers to avoid Compiler optimization. Finally, we are most interested in the differences between regular functions and lambda expressions.
After running these tests, we will find that Lambda expressions do not normally behave worse than regular functions. One of the strange results is that Lambda expressions actually behave better in some cases than conventional methods. Of course, if the closure is used, the results will be different. This result tells us that you do not have to hesitate to use lambda expressions. However, we still need to carefully consider the performance loss when we use closures. In this scenario, we usually lose some performance, but it may still be acceptable. The causes of performance loss will be uncovered in the next section.
The following table shows the benchmark test results:
- Comparison of no input parameter and no closure
- Input parameter comparison
- Closure-included comparison
- Comparison of closures with input parameters
Test |
Lambda [MS] |
Normal [MS] |
0 |
45 +-1 |
46 +-1 |
1 |
44 +-1 |
46 +-2 |
2 |
49 +-3 |
45 +-2 |
3 |
48 +-2 |
45 +-2 |
Note: The test result varies according to the hardware configuration of the machine.
The following chart also shows the test results. We can see that regular functions have the same restrictions as lambda expressions. Using lambda expressions has no significant performance loss.
Msil discloses lambda expressions
Using the famous linqpad tool, we can view msil.
Let's take a look at the first example:
1 void Main()2 {3 DoSomethingLambda("some example");4 DoSomethingNormal("some example");5 }
Lambda expressions:
1 Action<string> DoSomethingLambda = (s) =>2 {3 Console.WriteLine(s);// + local4 };
Code of the corresponding method:
1 void DoSomethingNormal(string s)2 {3 Console.WriteLine(s);4 }
The msil code of the two codes:
1 IL_0001: ldarg.0 2 IL_0002: ldfld UserQuery.DoSomethingLambda 3 IL_0007: ldstr "some example" 4 IL_000C: callvirt System.Action<System.String>.Invoke 5 IL_0011: nop 6 IL_0012: ldarg.0 7 IL_0013: ldstr "some example" 8 IL_0018: call UserQuery.DoSomethingNormal 9 10 DoSomethingNormal:11 IL_0000: nop 12 IL_0001: ldarg.1 13 IL_0002: call System.Console.WriteLine14 IL_0007: nop 15 IL_0008: ret 16 17 <.ctor>b__0:18 IL_0000: nop 19 IL_0001: ldarg.0 20 IL_0002: call System.Console.WriteLine21 IL_0007: nop 22 IL_0008: ret
The biggest difference here is the name and usage of the function, rather than the declaration method. In fact, the declaration method is the same. The compiler creates a new method in the current class and infers its usage. This is nothing special, but it is much easier to use lambda expressions. From the msil perspective, we have done the same thing, that is, we have called a method on the current object.
We can put these analyses in a diagram to show the changes made by the compiler. In the following figure, we can see that the compiler moves the lambda expression to a separate method.
In the second example, we will show what lambda expressions are really amazing. In this example, we use a conventional method to access global variables, and then use a Lambda expression to capture local variables. The Code is as follows:
1 void Main() 2 { 3 int local = 5; 4 5 Action<string> DoSomethingLambda = (s) => { 6 Console.WriteLine(s + local); 7 }; 8 9 global = local;10 11 DoSomethingLambda("Test 1");12 DoSomethingNormal("Test 2");13 }14 15 int global;16 17 void DoSomethingNormal(string s)18 {19 Console.WriteLine(s + global);20 }
At present, it seems that there is nothing special. The key question is: how does the compiler handle lambda expressions?
1 IL_0000: newobj UserQuery+<>c__DisplayClass1..ctor 2 IL_0005: stloc.1 // CS$<>8__locals2 3 IL_0006: nop 4 IL_0007: ldloc.1 // CS$<>8__locals2 5 IL_0008: ldc.i4.5 6 IL_0009: stfld UserQuery+<>c__DisplayClass1.local 7 IL_000E: ldloc.1 // CS$<>8__locals2 8 IL_000F: ldftn UserQuery+<>c__DisplayClass1.<Main>b__0 9 IL_0015: newobj System.Action<System.String>..ctor10 IL_001A: stloc.0 // DoSomethingLambda11 IL_001B: ldarg.0 12 IL_001C: ldloc.1 // CS$<>8__locals213 IL_001D: ldfld UserQuery+<>c__DisplayClass1.local14 IL_0022: stfld UserQuery.global15 IL_0027: ldloc.0 // DoSomethingLambda16 IL_0028: ldstr "Test 1"17 IL_002D: callvirt System.Action<System.String>.Invoke18 IL_0032: nop 19 IL_0033: ldarg.0 20 IL_0034: ldstr "Test 2"21 IL_0039: call UserQuery.DoSomethingNormal22 IL_003E: nop 23 24 DoSomethingNormal:25 IL_0000: nop 26 IL_0001: ldarg.1 27 IL_0002: ldarg.0 28 IL_0003: ldfld UserQuery.global29 IL_0008: box System.Int3230 IL_000D: call System.String.Concat31 IL_0012: call System.Console.WriteLine32 IL_0017: nop 33 IL_0018: ret 34 35 <>c__DisplayClass1.<Main>b__0:36 IL_0000: nop 37 IL_0001: ldarg.1 38 IL_0002: ldarg.0 39 IL_0003: ldfld UserQuery+<>c__DisplayClass1.local40 IL_0008: box System.Int3241 IL_000D: call System.String.Concat42 IL_0012: call System.Console.WriteLine43 IL_0017: nop 44 IL_0018: ret 45 46 <>c__DisplayClass1..ctor:47 IL_0000: ldarg.0 48 IL_0001: call System.Object..ctor49 IL_0006: ret
The same is true. The two functions are the same in the call statement, or the same mechanism is applied as before. That is to say, the compiler generates a name for the function and replaces it with the code. The biggest difference here is that the compiler generates a class at the same time, and the function generated by the compiler is put into this class. So what is the purpose of creating this class? It gives variables a global scope, and before that, it has been used to capture variables. In this way, lambda expressions have the ability to access variables with local scopes (because from the msil point of view, it is only a global variable in the class instance ).
Then, all the variables are allocated and read from the new class instance. This solves the issue of reference between variables (an additional reference will be added to the class-this is indeed the case ). The compiler is smart enough to put the captured variables into this class. Therefore, we may expect that there will be no performance problems when using lambda expressions. However, here we must give a warning that this behavior may cause memory leakage because the object is still referenced by lambda expressions. As long as this function is still in use, its scope is still valid (we have understood this before, but now we know the reason ).
As before, we put these analyses into a graph. We can see that the closure is not the only method to be moved, and the captured variables are also moved. All moved objects are put into a class generated by the compiler. Finally, we instantiate an object from an unknown class.
The article content is translated and adapted fromWay to Lambda, Sections and code have been greatly modified, not including all content
The past and present of lambda expressions