Lambda expression
As early as C # 1.0, the concept of a delegate (delegate) type was introduced in C #. By using this type, we can pass the function as a parameter. In a sense, a delegate can be understood as a managed, strongly typed function pointer.
Typically, it takes a few steps to pass a function using a delegate:
-
Defines a delegate that contains the specified parameter type and return value type.
-
-
In a method that needs to receive a function parameter, use that 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's essentially true. The 3rd step above is usually not required, and the C # compiler can complete this step, but steps 1 and 2 are still required.
Fortunately, generics are introduced in C # 2.0. Now we can write generic classes, generic methods, and most importantly: generic delegates. However, until. NET 3.5, Microsoft realized that it could actually meet 99% of the requirements with only two generic delegates:
-
Action: No input parameter, no return value
-
-
Action: Supports 1-16 input parameters, no return value
-
-
Func: Supports 1-16 input parameters, with return value
The Action delegate returns a void type, and the Func delegate returns a value of the specified type. By using both of these delegates, in the vast majority of cases, step 1 above can be omitted. However, step 2 is still required, but only Action and Func are required.
So, what if I just want to execute some code? A way to create anonymous functions is provided in C # 2.0. But unfortunately, this grammar 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 were introduced in the .NET 3.5 framework and C # 3.0.
First, let's understand the origin of Lambda expression names. In fact, the name comes from λ in calculus mathematics, and its meaning is to declare what is needed to express a function. Rather, it describes a system of mathematical logic that expresses calculations by combining and replacing variables. So basically we have 0-n input parameters and a return value. In programming languages, we also provide void support with no return value.
Let's look at some examples of Lambda expressions:
// The compiler cannot resolve this, which makes the usage of var impossible!
// Therefore we need to specify the type.
Action dummyLambda = () =>
{
Console.WriteLine ("Hello World from a Lambda expression!");
};
// Can be used as with double y = square (25);
Func <double, double> square = x => x * x;
// Can be used as with double z = product (9, 5);
Func <double, double, double> product = (x, y) => x * y;
// Can be used as with printProduct (9, 5);
Action <double, double> printProduct = (x, y) => {Console.WriteLine (x * y);};
// Can be used as with
// var sum = dotProduct (new double [] {1, 2, 3}, new double [] {4, 5, 6});
Func <double [], double [], double> dotProduct = (x, y) =>
{
var dim = Math.Min (x.Length, y.Length);
var sum = 0.0;
for (var i = 0; i! = dim; i ++)
sum + = x [i] + y [i];
return sum;
};
// Can be used as with var result = matrixVectorProductAsync (...);
Func <double [,], double [], Task <double [] >> matrixVectorProductAsync =
async (x, y) =>
{
var sum = 0.0;
/ * do some stuff using await ... * /
return sum;
};
From these statements we can directly understand:
If there is only one parameter, parentheses can be omitted.
If there is only one line of statements and returns in that statement, you can omit the braces, and you can also omit the return keyword.
Lambda expressions can be declared to execute asynchronously by using the async keyword.
In most cases, the var declaration may not be used, and it can only be used in some special cases.
When using var, if the compiler cannot infer the delegate type through parameter type and return value type inference, it will throw "Cannot assign lambda expression to an implicitly-typed local variable." Take a look at these examples:
Now that we know most of the basics, some cool parts of Lambda expressions haven't been mentioned yet.
Let's take a look at this code:
var a = 5;
Funcint, int> multiplyWith = x => x * a;
var result1 = multiplyWith (10); // 50
a = 10;
var result2 = multiplyWith (10); // 100
As you can see, you can use peripheral variables, which are closures, in Lambda expressions.
static void DoSomeStuff ()
{
var coeff = 10;
Funcint, int> compute = x => coeff * x;
Action modifier = () =>
{
coeff = 5;
};
var result1 = DoMoreStuff (compute); // 50
ModifyStuff (modifier);
var result2 = DoMoreStuff (compute); // 25
}
static int DoMoreStuff (Funcint, int> computer)
{
return computer (5);
}
static void ModifyStuff (Action modifier)
{
modifier ();
}
What happened here? First we created a local variable and two Lambda expressions. The first Lambda expression shows that it can access the local variable in other scopes, which in fact has already shown its power. This means we can protect a variable, but we can still access it in other methods, regardless of whether the method is defined in the current class or another class.
The second Lambda expression demonstrates the ability to modify peripheral variables in a Lambda expression. This means that by passing Lambda expressions between functions, we can modify local variables in other scopes in other methods. Therefore, I consider closures to be a particularly powerful feature, but sometimes it can also introduce some undesirable results.
var buttons = new Button [10];
for (var i = 0; i <buttons.Length; i ++)
{
var button = new Button ();
button.Text = (i + 1) + ". Button-Click for Index!";
button.OnClick + = (s, e) => {Messagebox.Show (i.ToString ());};
buttons [i] = button;
}
// What happens if we click ANY button ?!
What is the result of this weird question? Yes Button 0 displays 0, Button 1 displays 1? The answer is: all Buttons show 10!
Because with the for loop, the value of the local variable i has been changed to the length of buttons 10. A simple solution is similar to:
var button = new Button ();
var index = i;
button.Text = (i + 1) + ". Button-Click for Index!";
button.OnClick + = (s, e) => {Messagebox.Show (index.ToString ());};
buttons [i] = button;
Copy the value in variable i by defining the variable index.
Note: If you use Visual Studio 2012 or later for testing, because the compiler used is different from Visual Studio 2010, the test results here may be different. See also: Visual C # Breaking Changes in Visual Studio 2012
Expression tree
An important question when using Lambda expressions is how the target method knows the following information:
What is the name of the variable we pass?
What is the structure of the expression body we use?
What types do we use in expression bodies?
Now, expression trees have helped us solve the problem. It allows us to delve into how expressions are generated by specific compilers. In addition, we can execute a given function, just as we would with Func and Action delegates. It also allows us to parse Lambda expressions at runtime.
Let's look at an example that describes how to use the Expression type:
Expressionint >> expr = model => model.MyProperty;
var member = expr.Body as MemberExpression;
var propertyName = memberExpression.Member.Name; // only execute if member! = null
The above is the simplest example of the use of Expression. The principle is very straightforward: by forming an Expression type object, the compiler generates metadata information based on the analysis of the expression tree. The parse tree contains all relevant information, such as parameters and method bodies.
The method body contains the entire parse tree. It gives us access to operators, operands, and complete statements, and most importantly, the name and type of the return value. Of course, the name of the return variable may be null. Nevertheless, in most cases we are still interested in the content of expressions. The benefit for developers is that we no longer misspell the names of properties because every spelling error will cause a compilation error.
If the programmer just wants to know the name of the calling property, there is a simpler and more elegant way. You can get the name of the called method or property by using the special parameter property CallerMemberName. The compiler automatically records these names. So, if we just need to know these names without more type information, we can refer to the following code:
string WhatsMyName ([CallerMemberName] string callingName = null)
{
return callingName;
}
Lambda expression performance
The big question is: how fast is a Lambda expression? Of course, we expect it to be as fast as a regular function, 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 regular functions.
A very interesting discussion is about whether closures in Lambda expressions are faster than using global variables, and the most interesting part is whether there will be a performance impact when all available variables are in local scope.
Let's look at some code to measure various performance benchmarks. With these 4 different benchmarks, we should have enough evidence to explain the difference between regular functions and Lambda expressions.
class StandardBenchmark: Benchmark
{
static double [] A;
static double [] B;
public static void Test ()
{
var me = new StandardBenchmark ();
Init ();
for (var i = 0; i 10; i ++)
{
var lambda = LambdaBenchmark ();
var normal = NormalBenchmark ();
me.lambdaResults.Add (lambda);
me.normalResults.Add (normal);
}
me.PrintTable ();
}
static void Init ()
{
var r = new Random ();
A = new double [LENGTH];
B = new double [LENGTH];
for (var i = 0; i)
{
A [i] = r.NextDouble ();
B [i] = r.NextDouble ();
}
}
static long LambdaBenchmark ()
{
Funcdouble> Perform = () =>
{
var sum = 0.0;
for (var i = 0; i)
sum + = A [i] * B [i];
return sum;
};
var iterations = new double [100];
var timing = new Stopwatch ();
timing.Start ();
for (var j = 0; j)
iterations [j] = Perform ();
timing.Stop ();
Console.WriteLine ("Time for Lambda-Benchmark: t {0} ms",
timing.ElapsedMilliseconds);
return timing.ElapsedMilliseconds;
}
static long NormalBenchmark ()
{
var iterations = new double [100];
var timing = new Stopwatch ();
timing.Start ();
for (var j = 0; j)
iterations [j] = NormalPerform ();
timing.Stop ();
Console.WriteLine ("Time for Normal-Benchmark: t {0} ms",
timing.ElapsedMilliseconds);
return timing.ElapsedMilliseconds;
}
static double NormalPerform ()
{
var sum = 0.0;
for (var i = 0; i)
sum + = A [i] * B [i];
return sum;
}
}
Of course, with Lambda expressions, we can write the above code more elegantly. The reason for this is to prevent interference with the final result. So we only provide 3 necessary methods, one of which is responsible for performing Lambda tests, one is for regular function tests, and the third is for regular functions. The fourth method that is missing is our Lambda expression, which is already embedded in the first method. The calculation method used is not important. We use random numbers to avoid compiler optimizations. 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 generally perform worse than regular functions. One of the strange results is that Lambda expressions actually perform better than regular methods in some cases. Of course, if you use closures, the results will be different. This result tells us that there is no need to hesitate to use Lambda expressions. But we still need to carefully consider the performance lost when we use closures. In this scenario, we usually lose a bit of performance, but may still be acceptable. The reasons for the loss of performance will be revealed in the next section.
The results of the benchmark tests are shown in the following table:
Comparison without arguments and closures
Included reference comparison
Comparison with closures
Comparison of Included Parameters and Inclusive Closures
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 results are different according to the hardware configuration of the machine
The test results are also shown in the chart below. We can see that regular functions have the same restrictions as Lambda expressions. There is no significant performance penalty for using Lambda expressions.
MSIL reveals Lambda expressions
Using the well-known tool LINQPad we can look at MSIL.
Let's look at the first example:
void Main ()
{
DoSomethingLambda ("some example");
DoSomethingNormal ("some example");
}
Lambda expression:
Actionstring> DoSomethingLambda = (s) =>
{
Console.WriteLine (s); // + local
};
The code of the corresponding method:
void DoSomethingNormal (string s)
{
Console.WriteLine (s);
}
MSIL code for two codes:
IL_0001: ldarg.0
IL_0002: ldfld UserQuery.DoSomethingLambda
IL_0007: ldstr "some example"
IL_000C: callvirt System.Action.Invoke
IL_0011: nop
IL_0012: ldarg.0
IL_0013: ldstr "some example"
IL_0018: call UserQuery.DoSomethingNormal
DoSomethingNormal:
IL_0000: nop
IL_0001: ldarg.1
IL_0002: call System.Console.WriteLine
IL_0007: nop
IL_0008: ret
b__0:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call System.Console.WriteLine
IL_0007: nop
IL_0008: ret
The biggest difference here is the function's naming and usage, not the way it is declared. In fact, the way it is declared is the same. The compiler creates a new method in the current class and then infers its usage. This is nothing special, except that using Lambda expressions is much more convenient. From the MSIL perspective, we do the same thing, which is to call a method on the current object.
We can put these analyses in a diagram to show the changes made by the compiler. In the image below we can see that the compiler moved the Lambda expression into a separate method.
In the second example, we will show you the magic of Lambda expressions. In this example, we used a regular method to access global variables, and then used a Lambda expression to capture local variables. code show as below:
void Main ()
{
int local = 5;
Actionstring> DoSomethingLambda = (s) => {
Console.WriteLine (s + local);
};
global = local;
DoSomethingLambda ("Test 1");
DoSomethingNormal ("Test 2");
}
int global;
void DoSomethingNormal (string s)
{
Console.WriteLine (s + global);
}
It seems nothing special at the moment. The key question is: how does the compiler handle Lambda expressions?
IL_0000: newobj UserQuery + c__DisplayClass1..ctor
IL_0005: stloc.1 // CS $ 8__locals2
IL_0006: nop
IL_0007: ldloc.1 // CS $ 8__locals2
IL_0008: ldc.i4.5
IL_0009: stfld UserQuery + c__DisplayClass1.local
IL_000E: ldloc.1 // CS $ 8__locals2
IL_000F: ldftn UserQuery + c__DisplayClass1.b__0
IL_0015: newobj System.Action..ctor
IL_001A: stloc.0 // DoSomethingLambda
IL_001B: ldarg.0
IL_001C: ldloc.1 // CS $ 8__locals2
IL_001D: ldfld UserQuery + c__DisplayClass1.local
IL_0022: stfld UserQuery.global
IL_0027: ldloc.0 // DoSomethingLambda
IL_0028: ldstr "Test 1"
IL_002D: callvirt System.Action.Invoke
IL_0032: nop
IL_0033: ldarg.0
IL_0034: ldstr "Test 2"
IL_0039: call UserQuery.DoSomethingNormal
IL_003E: nop
DoSomethingNormal:
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.0
IL_0003: ldfld UserQuery.global
IL_0008: box System.Int32
IL_000D: call System.String.Concat
IL_0012: call System.Console.WriteLine
IL_0017: nop
IL_0018: ret
c__DisplayClass1.b__0:
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.0
IL_0003: ldfld UserQuery + c__DisplayClass1.local
IL_0008: box System.Int32
IL_000D: call System.String.Concat
IL_0012: call System.Console.WriteLine
IL_0017: nop
IL_0018: ret
c__DisplayClass1..ctor:
IL_0000: ldarg.0
IL_0001: call System.Object..ctor
IL_0006: ret
Still the same, the two functions are the same from the calling statement, or are they applied The same mechanism as before. That is, the compiler generates a name for the function and replaces it into 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 placed in this class. So, what is the purpose of creating this class? It gives the variable global scope, which was previously used to capture variables. In this way, Lambda expressions have the ability to access locally scoped variables (because from MSIL's point of view, it is just a global variable in the class instance).
Then, through the instance of this newly generated class, all variables are allocated and read from this instance. This solves the problem of references between variables (adding an extra reference to the class-it does). The compiler is smart enough to put those captured variables into this class. So, we might expect no performance issues with Lambda expressions. However, we have to warn that this behavior can cause memory leaks because the object is still referenced by the Lambda expression. As long as this function is still there, its scope is still valid (we already knew this before, but now we know why).
As before, we put these analyses into a graph. We can see that closures are not the only method being moved, and captured variables are also moved. All moved objects are placed into a compiler-generated class. Finally, we instantiated an object from an unknown class.