C# 14 introduces several improvements to lambda expressions, including natural type inference, default parameters, and enhanced type inference. These improvements make lambda expressions more flexible and easier to use.
Lambda expressions in C# 14 have been enhanced with:
- Natural types: The compiler can infer a natural delegate type for lambdas
- Default parameters: Lambda parameters can have default values
- Improved type inference: Better inference of types from context
- Attributes on lambdas: Apply attributes to lambda expressions
// Lambda with default parameters
var greet = (string name = "World") => $"Hello, {name}!";
// Natural type inference
var add = (int a, int b) => a + b; // Inferred as Func<int, int, int>
// Attributes on lambdas
var validate = [LoggerMessage(Level = LogLevel.Information)]
(string input) => !string.IsNullOrEmpty(input);public class ConfigurationService
{
public void ConfigureLogging()
{
// Lambda with default parameters
var createLogger = (
string name,
LogLevel level = LogLevel.Information,
bool includeScopes = true) =>
{
return LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(level);
if (includeScopes)
builder.AddFilter((category, logLevel) =>
logLevel >= level);
}).CreateLogger(name);
};
// Call with all parameters
var logger1 = createLogger("MyApp", LogLevel.Debug, false);
// Call with default parameters
var logger2 = createLogger("MyApp");
}
}public class MathOperations
{
// Natural type is inferred as Func<int, int, int>
public void DemonstrateNaturalTypes()
{
var add = (int a, int b) => a + b;
var multiply = (int a, int b) => a * b;
var divide = (int a, int b) => b != 0 ? a / b : 0;
// Use them like normal delegates
int result1 = PerformOperation(10, 5, add); // 15
int result2 = PerformOperation(10, 5, multiply); // 50
int result3 = PerformOperation(10, 5, divide); // 2
}
private int PerformOperation(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}
}public class DataProcessor
{
public void ProcessData()
{
// Lambda with default parameters and natural type inference
var processItem = (
string item,
bool trim = true,
bool toLower = false) =>
{
var result = trim ? item.Trim() : item;
result = toLower ? result.ToLower() : result;
return result;
};
var items = new[] { " Hello ", "WORLD ", " C# 14 " };
// Use with different combinations
var trimmed = items.Select(x => processItem(x));
var lowerCase = items.Select(x => processItem(x, toLower: true));
var both = items.Select(x => processItem(x, true, true));
}
// Reusable lambda with defaults
private readonly Func<int, int, int> _clamp =
(int value, int min = 0, int max = 100) =>
Math.Max(min, Math.Min(max, value));
public int NormalizeScore(int score)
{
return _clamp(score); // Uses defaults: 0 and 100
}
public int NormalizePercentage(int value)
{
return _clamp(value, -100, 100); // Custom range
}
}public class ValidationService
{
public bool ValidateUser(User user)
{
// Apply attributes to lambdas for logging, validation, etc.
var validateEmail = [ValidateEmail]
(string email) => email.Contains('@') && email.Contains('.');
var validateAge = [ValidateRange(Min = 18, Max = 120)]
(int age) => age >= 18 && age <= 120;
return validateEmail(user.Email) && validateAge(user.Age);
}
}
// Custom attribute for lambda validation
[AttributeUsage(AttributeTargets.Method)]
public class ValidateEmailAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Method)]
public class ValidateRangeAttribute : Attribute
{
public int Min { get; set; }
public int Max { get; set; }
}public class OvercomplicatedLambda
{
public void BadExample()
{
// Too many parameters with defaults - hard to understand
var process = (
string input,
bool trim = true,
bool toLower = false,
bool toUpper = false,
bool removeSpaces = false,
bool removePunctuation = false,
int maxLength = 100,
string prefix = "",
string suffix = "") =>
{
// Complex logic...
var result = input;
if (trim) result = result.Trim();
if (toLower) result = result.ToLower();
// ... too complex for a lambda
return result;
};
}
}Warning: Lambdas with many parameters become hard to read and maintain. Extract to a proper method.
Better approach:
public class StringProcessor
{
// Use a proper method with optional parameters
public string ProcessString(
string input,
bool trim = true,
bool toLower = false,
bool toUpper = false)
{
var result = input;
if (trim) result = result.Trim();
if (toLower) result = result.ToLower();
if (toUpper) result = result.ToUpper();
return result;
}
// Or use options pattern for complex configurations
public string ProcessString(string input, StringProcessingOptions options)
{
var result = input;
if (options.Trim) result = result.Trim();
if (options.ToLower) result = result.ToLower();
if (options.ToUpper) result = result.ToUpper();
return result;
}
}
public record StringProcessingOptions
{
public bool Trim { get; init; } = true;
public bool ToLower { get; init; }
public bool ToUpper { get; init; }
}public class UnclearTypes
{
public void ConfusingCode()
{
// What's the type? Hard to tell at a glance
var operation1 = (int a, int b) => a + b;
var operation2 = (a, b) => a * b; // Types unclear
var operation3 = (double a, double b) => a / b;
// Easy to pass wrong types
var result = operation1(5, 10); // OK
// var badResult = operation2(5.0, 10.0); // Compile error if types don't match
}
}Warning: When type inference is ambiguous, be explicit about types for clarity.
Better approach:
public class ClearTypes
{
// Be explicit with delegate types when reusing
private readonly Func<int, int, int> _addInts = (a, b) => a + b;
private readonly Func<double, double, double> _divideDoubles = (a, b) => a / b;
public void ClearCode()
{
// Or use explicit types for clarity
Func<int, int, int> multiply = (a, b) => a * b;
// Now it's clear what types are expected
var result1 = _addInts(5, 10);
var result2 = multiply(5, 10);
var result3 = _divideDoubles(5.0, 2.0);
}
}public class MisleadingDefaults
{
public void ProcessOrders()
{
// Default makes it seem optional, but it's usually needed
var calculateTotal = (
decimal subtotal,
decimal taxRate = 0.0m, // Misleading - tax should be explicit
decimal shipping = 0.0m) =>
{
return subtotal * (1 + taxRate) + shipping;
};
// Easy to forget tax
var total = calculateTotal(100.0m); // Wrong! No tax applied
}
}Warning: Don't use defaults for values that should typically be provided. Defaults should be for truly optional parameters.
Better approach:
public class ClearRequirements
{
public void ProcessOrders()
{
// Make required parameters explicit
var calculateTotal = (
decimal subtotal,
decimal taxRate, // Required
decimal shipping = 0.0m) => // Truly optional
{
return subtotal * (1 + taxRate) + shipping;
};
// Forces caller to think about tax
var total = calculateTotal(100.0m, 0.08m);
var totalWithShipping = calculateTotal(100.0m, 0.08m, 5.99m);
}
}Use default parameters when:
- Parameters have sensible defaults
- Callers frequently use the same value
- The default doesn't hide important business logic
Use natural type inference when:
- The lambda is simple and types are obvious
- You're assigning to var and context is clear
- You want concise code without ceremony
Avoid when:
- Types are ambiguous or complex
- The lambda is part of a public API
- Clarity is more important than brevity
- Lambda improvements have no runtime performance impact
- Natural type inference is compile-time only
- Default parameters are resolved at compile time
- Captured variables still create closures (allocations)