Skip to content

Latest commit

 

History

History
350 lines (289 loc) · 9.87 KB

File metadata and controls

350 lines (289 loc) · 9.87 KB

Lambda Expression Improvements

Overview

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.

Description

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

Syntax

// 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);

Best Practice Examples

✅ Good Practice: Use Default Parameters for Flexibility

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");
    }
}

✅ Good Practice: Natural Type Inference for Cleaner Code

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);
    }
}

✅ Good Practice: Combine Features for Powerful Patterns

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
    }
}

✅ Good Practice: Use Attributes for Cross-Cutting Concerns

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; }
}

Common Bad Practices

❌ Bad Practice: Overusing Default Parameters

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; }
}

❌ Bad Practice: Mixing Natural Types with Var Unclearly

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);
    }
}

❌ Bad Practice: Default Parameters Hiding Required Data

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);
    }
}

When to Use Lambda Improvements

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

Performance Considerations

  • 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)

Related Features