Microsoft released the newest version of the C# language, C#12. This new release has fresh improvements that make C# even better for developers, such as default values for lambda expressions.

To become familiar with this change, let’s dive into the topic and learn its benefits and drawbacks.

To download the source code for this article, you can visit our GitHub repository.

Lambda Improvements

The C# development team continues to enhance the lambda expression improvements started in C# 10. In C# 12 they made lambda expressions even more powerful by adding support for default values and params arrays.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Before C# 12, we had to create local functions or use DefaultParameterValue to work around the need for default values in lambda functions. Now, the new syntax allows us to create more concise and clear lambda functions. Another benefit is the ability to combine similar logic into a single lambda expression.

Before C#12

In .NET 7 and earlier, if we try to use a default parameter or params in a lambda expression, the compiler raises errors:

Using default values results in errors prior to C#12

For both situations, the compiler raises the warning CS9058 which stands for “Feature is not available. User newer language version.” In essence, the error indicates that we can’t use optional parameters or params in these contexts.

Prerequisites For .NET 8

Since this is a new C# 12 feature, we need to be sure our development environment is up to date, to build and run the code from this article. If we don’t have it yet, we need to download and install the new .NET 8 SDK from Microsoft’s official website.

New Behavior In C#12

After updating our development environment, we should no longer have compilation errors when we define a default parameter in a lambda method declaration:

var greetUser = (string name = "User") => $"Hello, {name}!";
greetUser();
greetUser("Peter");

In this example, we define a lambda function and assign it to the variable greetUser. Now, that C# accepts lambda default values, our method is more resilient when the parameter is not provided:

Hello, User!
Hello, Peter!

For cases where we need to use params in lambda definitions, the behavior is similar:

var greetTeam = (params string[] members) => $"Hello, team: {string.Join(", ", members)}";
greetTeam("John", "Peter", "Isaac");
greetTeam("John", "Peter", "Isaac", "Theo", "Matteo"));

Here, we create a new delegate that accepts a variable number of strings as input and returns the formatted string message. This way, we simplify the definition of our lambda expression and make it more flexible:

Hello, team: John, Peter, Isaac.
Hello, team: John, Peter, Isaac, Theo, Matteo.

Breaking Changes

These improvements come with breaking changes, so we may encounter compilation errors if older code uses one of the previously allowed features. Specifically, the issue at hand concerns the use of var to infer the type of method group. Since .NET SDK 7.0.200 and later, any application using the highlighted code no longer compiles:

void ExecuteAction(Action<string> act, string parameter) => act(parameter);
void Hello(string name = "User") => Console.WriteLine($"Hello, {name}");

var helloDelegate = Hello;
ExecuteAction(helloDelegate , "John");

int ExecuteFunc(Func<int[], int> func, params int[] parameter) => func(parameter);
int Total(params int[] numbers) => numbers.Sum();

var totalDelegate = Total;
ExecuteFunc(totalDelegate, new[] { 1, 2, 3 });

This is because the compiler cannot infer the correct type for the local variables helloDelegate and totalDelegate, so, the types are defined as anonymous delegates and the ExecuteAction and ExecuteFunc calls fail.

Instead, in place of using var, we must explicitly define the delegate type:

void ExecuteAction(Action<string> act, string parameter) => act(parameter);
void Hello(string name = "User") => Console.WriteLine($"Hello, {name}");

Action<string> helloDelegate = Hello;
ExecuteAction(helloDelegate, "John");

int ExecuteFunc(Func<int[], int> func, params int[] parameter) => func(parameter);
int Total(params int[] numbers) => numbers.Sum();

Func<int[], int> totalDelegate = Total;
ExecuteFunc(totalDelegate, new[] { 1, 2, 3 });

Now, the highlighted lines explicitly define Action<string> and Func<int[], int> respectively, and the compiler processes the code as expected.

Conclusion

The changes in lambda expressions continue the improvements introduced by Microsoft in C# 10. Now, developers can create more resilient and flexible anonymous methods when defining lambda expressions with default values. Despite the possible breaking changes that we need to be aware of, the benefits of this improvement are worth it.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!