C# 14 — A Leap Toward Cleaner, Smarter Code
Exploring the most impactful features with practical examples
C# 14 marks a thoughtful evolution in the language, focusing on expressiveness, modularity, and developer productivity. While not a radical overhaul, this release introduces subtle yet powerful enhancements that make everyday coding more intuitive and maintainable. Whether you're building high-performance systems or crafting elegant APIs, C# 14 helps you write less boilerplate and more meaningful logic.
In this guide, we'll explore the most impactful features of C# 14, with practical examples and insights into how they improve real-world development.
๐งญ C# 14 Feature Highlights
Here are the key features we'll be diving into:
- Extension Members — Add properties and indexers to existing types via extension blocks.
 - Field Keyword in Properties — Access auto-generated backing fields directly with 
field. - Null-Conditional Assignment (
??=) — Assign only when the target is null. - Unbound Generic Types in 
nameof— Use open generic types for cleaner diagnostics. - Implicit Span Conversions — Seamless transitions between 
Span<T>andReadOnlySpan<T>. - Lambda Parameter Modifiers — Use 
ref,in, oroutin lambda expressions. - Partial Events and Constructors — Modularize event and constructor logic across files.
 - User-Defined Compound Assignment Operators — Customize behavior for 
+=,-=, etc. - Improved Interpolated Strings — Enhanced formatting and performance for string interpolation.
 - Collection Expressions (Preview) — Concise syntax for initializing collections.
 
1. Extension Members
๐ง What Are Extension Members?
Traditionally, C# extension methods let you "add" methods to existing types without modifying them. But with C# 14, you can now define:
- Properties
 - Indexers
 - Events
 - Even operators
 
…all inside an extension block scoped to a specific type.
๐งช Example: Extension Methods for IEnumerable<T>
            
public static class EnumerableExtensions
{
    // Method: Check if the collection is empty
    public static bool IsEmpty<T>(this IEnumerable<T> source)
        => !source.Any();
    // Method: Access element by index safely (returns default if out of range)
    public static T? ElementAtOrDefaultSafe<T>(this IEnumerable<T> source, int index)
        => source.Skip(index).DefaultIfEmpty(default!).FirstOrDefault();
    // Method: Get the last element safely
    public static T? LastOrDefaultSafe<T>(this IEnumerable<T> source)
        => source.LastOrDefault();
}
            ๐ก Usage Example
var numbers = new[] { 1, 2, 3, 4, 5 };
// Using the extension methods
if (!numbers.IsEmpty())
{
    Console.WriteLine($"Third element: {numbers.ElementAtOrDefaultSafe(2)}");
    Console.WriteLine($"Last element: {numbers.LastOrDefaultSafe()}");
}
            ๐ก Key Benefits:
- Cleaner API surface — Access properties and indexers naturally without method calls
 - Better IntelliSense — IDE suggestions now include extension properties and indexers
 - Type safety — Compile-time checks ensure proper usage
 
2. Field Keyword in Properties
๐ Direct Access to Backing Fields
The new field keyword allows you to access the auto-generated backing field directly in property accessors, eliminating the need to manually declare private fields.
๐งช Example: Smart Properties with Validation
public class Product
{
    public string Name { get; set; }
    public decimal Price
    {
        get => field;
        set
        {
            if (value < 0)
                throw new ArgumentException("Price cannot be negative");
            field = value;
        }
    }
    public int Quantity
    {
        get => field;
        set
        {
            field = Math.Max(0, value); // Ensure non-negative
            Console.WriteLine($"Stock updated: {field} units");
        }
    }
}
            ๐ก Why This Matters: Before C# 14, you had to declare a separate backing field and manually manage it. Now, you can add validation and logging to auto-properties without the boilerplate!
3. Null-Conditional Assignment (??=)
๐ฏ Assign Only When Null
The ??= operator assigns a value to a variable only if it's currently null. This simplifies lazy initialization patterns.
private Logger? _logger;
public Logger Logger
{
    get
    {
        // Initialize only if null
        _logger ??= new Logger();
        return _logger;
    }
}
            Comparison with traditional approach:
// Old way
if (_logger == null)
{
    _logger = new Logger();
}
// New way
_logger ??= new Logger();
        4. Unbound Generic Types in nameof
            
            ๐ What Are Unbound Generic Types?
Prior to C# 14, you couldn't use nameof with open generic types directly. You had to specify type parameters even when you just wanted the type's name. Now, you can reference unbound generic types cleanly.
๐งช Example: Cleaner Type Name References
// Before C# 14 - Had to provide type arguments
string typeName = nameof(Dictionary<string, int>); // "Dictionary"
// C# 14 - Can use unbound generic types
string typeName = nameof(Dictionary<,>); // "Dictionary"
string listName = nameof(List<>);       // "List"
            ๐ก Real-World Usage: Generic Factory with Logging
public class GenericFactory<T> where T : new()
{
    public T Create()
    {
        // Log the factory name without specifying T
        var factoryName = nameof(GenericFactory<>);
        Console.WriteLine($"Creating instance in {factoryName}");
        
        return new T();
    }
}
// Usage in error handling
public void ProcessRepository<TRepo, TEntity>()
{
    try
    {
        // Processing logic...
    }
    catch (Exception ex)
    {
        // Clean error messages without type parameters
        throw new InvalidOperationException(
            $"Failed in {nameof(ProcessRepository<,>)}", ex);
    }
}
            ๐ก Key Benefits:
- Cleaner code — No need to provide arbitrary type arguments just to get a name
 - Better refactoring — Generic type names update automatically when renamed
 - Improved diagnostics — More readable error messages and logs
 - API documentation — Reference generic types clearly in generated docs
 
๐ Use Cases
- Logging: Log generic type names without concrete type parameters
 - Diagnostics: Create meaningful exception messages referencing generic methods
 - Reflection: Build type metadata utilities more elegantly
 - Code generation: Generate code based on generic type names
 
5. Implicit Span Conversions
⚡ Seamless Span Type Transitions
C# 14 introduces implicit conversions between Span<T> and ReadOnlySpan<T>, making it much easier to work with high-performance memory operations. You can now pass Span<T> to methods expecting ReadOnlySpan<T> without explicit casting.
๐งช Example: Before vs. After
// Before C# 14 - Manual casting required
void ProcessData(ReadOnlySpan<byte> data)
{
    // Process read-only data
    Console.WriteLine($"Processing {data.Length} bytes");
}
Span<byte> buffer = new byte[1024];
ProcessData(buffer); // ❌ Compiler error in older versions
// Had to explicitly cast:
ProcessData((ReadOnlySpan<byte>)buffer);
// C# 14 - Implicit conversion
Span<byte> buffer = new byte[1024];
ProcessData(buffer); // ✅ Works seamlessly!
            ๐ก Real-World Example: Stream Processing
public class DataProcessor
{
    // Method that reads data without modifying it
    public int CalculateChecksum(ReadOnlySpan<byte> data)
    {
        int checksum = 0;
        foreach (var b in data)
        {
            checksum ^= b;
        }
        return checksum;
    }
    // Method that modifies data
    public void EncryptData(Span<byte> data, byte key)
    {
        for (int i = 0; i < data.Length; i++)
        {
            data[i] ^= key;
        }
    }
    public void ProcessStream()
    {
        Span<byte> buffer = stackalloc byte[512];
        
        // Fill buffer with data...
        
        // Implicit conversion to ReadOnlySpan<byte>
        var checksum = CalculateChecksum(buffer); // ✅ Works!
        
        // Modify the buffer
        EncryptData(buffer, 0x5A);
        
        // Calculate checksum again
        var newChecksum = CalculateChecksum(buffer); // ✅ Still works!
        
        Console.WriteLine($"Checksums: {checksum} → {newChecksum}");
    }
}
            ๐ฏ Performance Benefits
// Stack-allocated buffer for zero-allocation processing
public static bool ValidateData(ReadOnlySpan<char> input)
{
    return input.Length > 0 && input[0] == '{' && input[^1] == '}';
}
// Can now pass strings directly!
string json = "{\"name\":\"John\"}";
if (ValidateData(json)) // Implicit string → ReadOnlySpan<char>
{
    Console.WriteLine("Valid JSON structure");
}
// Stack-allocated buffer also works
Span<char> buffer = stackalloc char[100];
int length = FormatData(buffer);
if (ValidateData(buffer.Slice(0, length))) // Span → ReadOnlySpan
{
    Console.WriteLine("Buffer validated");
}
            ๐ก Key Benefits:
- Zero-copy operations — Pass data without allocations or copying
 - Cleaner API design — Methods can accept ReadOnlySpan for immutability guarantees
 - Better performance — Stack-allocated buffers with minimal overhead
 - Improved interop — Easier integration with native code and unsafe contexts
 - Type safety — Compiler enforces read-only vs. mutable semantics
 
๐ Common Patterns
// Pattern 1: Parse without allocation
public static int ParseNumber(ReadOnlySpan<char> text)
{
    return int.Parse(text);
}
string input = "12345";
var number = ParseNumber(input); // No substring allocation!
// Pattern 2: Slice and process
void ProcessSegments(ReadOnlySpan<char> data)
{
    var firstPart = data.Slice(0, 10);
    var secondPart = data.Slice(10);
    // Process without creating new strings
}
// Pattern 3: Safe buffer operations
void SafeReadBuffer()
{
    Span<byte> writeBuffer = new byte[100];
    FillBuffer(writeBuffer);
    
    // Pass as read-only to prevent modifications
    VerifyBuffer(writeBuffer); // Implicit → ReadOnlySpan
}
        6. Lambda Parameter Modifiers
๐ Enhanced Lambda Expressions
C# 14 allows you to use parameter modifiers (ref, in, out) in lambda expressions, enabling more efficient parameter passing and better performance in high-throughput scenarios.
๐งช Example: Using ref in Lambdas
// Before C# 14 - Had to use delegates explicitly
delegate void RefAction<T>(ref T value);
void ProcessWithRef(RefAction<int> action)
{
    int value = 10;
    action(ref value);
    Console.WriteLine(value);
}
// C# 14 - Use ref directly in lambda
var incrementer = (ref int x) => x++;
int number = 5;
incrementer(ref number);
Console.WriteLine(number); // Output: 6
            ๐ก Real-World Example: High-Performance Data Processing
public class DataTransformer
{
    // Transform large structs in-place without copying
    public void TransformInPlace<T>(
        List<T> items,
        RefAction<T> transformer) where T : struct
    {
        for (int i = 0; i < items.Count; i++)
        {
            var temp = items[i];
            transformer(ref temp);
            items[i] = temp;
        }
    }
}
// Usage: Transform points without allocation
struct Point
{
    public double X, Y;
}
var points = new List<Point> { new() { X = 1, Y = 2 } };
var transformer = new DataTransformer();
// Lambda with ref modifier - modifies in place
transformer.TransformInPlace(points, (ref Point p) => 
{
    p.X *= 2;
    p.Y *= 2;
});
            ๐ Using in for Read-Only References
// Use 'in' to pass large structs efficiently without copying
struct Matrix4x4
{
    public double[,] Data;
    // ... large struct with 16 doubles
}
// Custom delegate type with 'in' parameter
public delegate double InFunc<T>(in T value);
// Lambda with 'in' parameter - read-only, no copy
InFunc<Matrix4x4> calculateDeterminant = 
    (in Matrix4x4 matrix) =>
    {
        // Access matrix without copying the entire struct
        return matrix.Data[0, 0] * matrix.Data[1, 1];
    };
var matrix = new Matrix4x4();
var det = calculateDeterminant(in matrix);
            ๐ฏ Using out for Output Parameters
// Custom delegate type with 'out' parameter
public delegate bool TryParseFunc<T>(string input, out T result);
// Lambda that returns multiple values via out parameter
TryParseFunc<int> tryParse = 
    (string input, out int result) =>
    {
        return int.TryParse(input, out result);
    };
if (tryParse("42", out var value))
{
    Console.WriteLine($"Parsed: {value}");
}
// Practical example: Validation with details
public delegate bool TryValidate(string input, out string errorMessage);
TryValidate validateEmail = 
    (string email, out string errorMessage) =>
    {
        if (string.IsNullOrEmpty(email))
        {
            errorMessage = "Email cannot be empty";
            return false;
        }
        if (!email.Contains("@"))
        {
            errorMessage = "Invalid email format";
            return false;
        }
        errorMessage = string.Empty;
        return true;
    };
            ๐ก Key Benefits:
- Better performance — Avoid copying large structs
 - More expressive — Write functional code with side effects when needed
 - Cleaner APIs — Use lambdas in more scenarios without wrapper methods
 - Type safety — Compiler enforces parameter semantics
 
7. Partial Events and Constructors
๐ Modular Code Organization
C# 14 extends the partial keyword to events and constructors, allowing you to split their implementation across multiple files. This is especially useful for generated code scenarios and large class implementations.
๐งช Example: Partial Constructors
// File: Person.cs - Main implementation
public partial class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    // Partial constructor declaration
    public partial Person(string firstName, string lastName);
}
// File: Person.Generated.cs - Auto-generated validation logic
public partial class Person
{
    // Partial constructor implementation
    public partial Person(string firstName, string lastName)
    {
        // Generated validation and initialization
        if (string.IsNullOrWhiteSpace(firstName))
            throw new ArgumentException(nameof(firstName));
        
        if (string.IsNullOrWhiteSpace(lastName))
            throw new ArgumentException(nameof(lastName));
        
        FirstName = firstName;
        LastName = lastName;
        Age = 0;
        
        // Call generated initialization logic
        InitializeGeneratedProperties();
    }
    partial void InitializeGeneratedProperties();
}
            ๐ก Example: Partial Events
// File: DataService.cs - Main class
public partial class DataService
{
    // Partial event declaration
    public partial event EventHandler<DataChangedEventArgs> DataChanged;
    public void UpdateData(string newData)
    {
        // Business logic
        ProcessData(newData);
        
        // Raise the partial event
        DataChanged?.Invoke(this, new DataChangedEventArgs(newData));
    }
    private void ProcessData(string data) { /* ... */ }
}
// File: DataService.Logging.cs - Logging concerns
public partial class DataService
{
    // Partial event implementation with logging
    public partial event EventHandler<DataChangedEventArgs> DataChanged
    {
        add
        {
            Console.WriteLine($"Subscriber added to DataChanged event");
            field += value; // C# 14 field keyword
        }
        remove
        {
            Console.WriteLine($"Subscriber removed from DataChanged event");
            field -= value;
        }
    }
}
            ๐ฏ Real-World Use Case: Separating Generated Code
// File: ViewModel.cs - Hand-written code
public partial class UserViewModel
{
    private string _username;
    
    // Partial event for property changes
    public partial event PropertyChangedEventHandler PropertyChanged;
    public string Username
    {
        get => _username;
        set
        {
            if (_username != value)
            {
                _username = value;
                OnPropertyChanged(nameof(Username));
            }
        }
    }
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
// File: ViewModel.Generated.cs - Source generated code
public partial class UserViewModel
{
    // Generated partial constructor with dependency injection
    public partial UserViewModel(
        ILogger logger,
        IDataService dataService);
    public partial UserViewModel(
        ILogger logger,
        IDataService dataService)
    {
        // Generated initialization
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
        
        _logger.LogInformation("UserViewModel created");
    }
    // Generated partial event implementation with weak references
    public partial event PropertyChangedEventHandler PropertyChanged
    {
        add { _weakEventManager.AddHandler(value); }
        remove { _weakEventManager.RemoveHandler(value); }
    }
    private readonly WeakEventManager _weakEventManager = new();
    private readonly ILogger _logger;
    private readonly IDataService _dataService;
}
            ๐ก Key Benefits:
- Clean separation — Keep generated code separate from hand-written code
 - Better maintainability — Organize large classes across multiple files
 - Source generators — Perfect for code generation scenarios
 - Team collaboration — Different team members can work on different aspects
 
8. User-Defined Compound Assignment Operators
๐ง Customize Compound Operations
C# 14 allows you to define custom behavior for compound assignment operators like +=, -=, *=, etc. This enables more intuitive APIs for custom types.
๐งช Example: Custom Vector Type
public struct Vector3
{
    public double X, Y, Z;
    public Vector3(double x, double y, double z)
    {
        X = x; Y = y; Z = z;
    }
    // Define the fundamental operators
    public static Vector3 operator +(Vector3 a, Vector3 b)
        => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
    public static Vector3 operator *(Vector3 v, double scalar)
        => new(v.X * scalar, v.Y * scalar, v.Z * scalar);
}
// Usage — Compound assignments use the defined operators
var velocity = new Vector3(1, 2, 3);
var acceleration = new Vector3(0.1, 0.2, 0.3);
velocity += acceleration; // Uses operator + under the hood
velocity *= 0.5;          // Uses operator * under the hood
            ๐ก Example: StringBuilder-like Builder Pattern
public class QueryBuilder
{
    private StringBuilder _query = new();
    // Define fundamental operators
    public static QueryBuilder operator +(QueryBuilder builder, string clause)
    {
        if (builder._query.Length > 0)
            builder._query.Append(" ");
        builder._query.Append(clause);
        return builder;
    }
    public static QueryBuilder operator &(QueryBuilder builder, string condition)
    {
        if (!builder._query.ToString().Contains("WHERE"))
            builder._query.Append(" WHERE ");
        else
            builder._query.Append(" AND ");
        builder._query.Append(condition);
        return builder;
    }
    public override string ToString() => _query.ToString();
}
// Usage — Intuitive query building
var query = new QueryBuilder();
query += "SELECT * FROM Users";
query &= "Age > 18";
query &= "IsActive = 1";
query += "ORDER BY Name";
Console.WriteLine(query);
// Output: SELECT * FROM Users WHERE Age > 18 AND IsActive = 1 ORDER BY Name
            ๐ฏ Example: Collection Builder with Validation
public class ValidatedCollection<T>
{
    private List<T> _items = new();
    private Func<T, bool> _validator;
    public ValidatedCollection(Func<T, bool> validator)
    {
        _validator = validator;
    }
    // Define fundamental operators
    public static ValidatedCollection<T> operator +(
        ValidatedCollection<T> collection, T item)
    {
        if (collection._validator(item))
        {
            collection._items.Add(item);
            Console.WriteLine($"✓ Added: {item}");
        }
        else
        {
            Console.WriteLine($"✗ Rejected: {item}");
        }
        return collection;
    }
    public static ValidatedCollection<T> operator -(
        ValidatedCollection<T> collection, T item)
    {
        collection._items.Remove(item);
        return collection;
    }
    public int Count => _items.Count;
}
// Usage
var numbers = new ValidatedCollection<int>(x => x > 0);
numbers += 5;   // ✓ Added: 5
numbers += -3;  // ✗ Rejected: -3
numbers += 10;  // ✓ Added: 10
            ๐ก Key Benefits:
- Intuitive syntax — Make custom types behave like built-in types
 - Performance — Optimize in-place operations without temporary allocations
 - Fluent APIs — Create chainable, readable code
 - Domain modeling — Express domain concepts naturally
 
9. Improved Interpolated Strings
๐ Enhanced String Formatting
C# 14 brings significant improvements to string interpolation, including better performance, enhanced formatting options, and support for custom interpolated string handlers.
๐งช Example: Advanced Formatting
// Enhanced format specifiers
var price = 1234.567;
var date = new DateTime(2025, 11, 3);
// Multiple format specifiers with alignment
var formatted = $"Price: {price,15:C2} | Date: {date:yyyy-MM-dd}";
// Output: "Price:      $1,234.57 | Date: 2025-11-03"
// C# 14: Conditional formatting within interpolation
var stock = 5;
var message = $"Stock: {stock} ({(stock > 0 ? \"Available\" : \"Out of stock\")})";
// Nested interpolations (improved in C# 14)
var items = new[] { "Apple", "Banana", "Cherry" };
var list = $"Items: {string.Join(", ", items.Select(x => $"'{x}'"))}";
// Output: "Items: 'Apple', 'Banana', 'Cherry'"
            ๐ก Performance: Custom Interpolated String Handlers
// Custom handler for SQL query building with parameter safety
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct SqlInterpolatedStringHandler
{
    private System.Text.StringBuilder _builder;
    private System.Collections.Generic.List<object> _parameters;
    public SqlInterpolatedStringHandler(int literalLength, int formattedCount)
    {
    _builder = new System.Text.StringBuilder(literalLength);
    _parameters = new System.Collections.Generic.List<object>(formattedCount);
    }
    public void AppendLiteral(string s) => _builder.Append(s);
    public void AppendFormatted<T>(T value)
    {
        _parameters.Add(value);
        _builder.Append($"@p{_parameters.Count - 1}");
    }
    public (string Query, object[] Parameters) GetResult()
        => (_builder.ToString(), _parameters.ToArray());
}
// Usage - Safe parameterized queries
public static class SqlQuery
{
    public static (string Query, object[] Parameters) Create(
        SqlInterpolatedStringHandler handler)
    {
        return handler.GetResult();
    }
}
// Create safe SQL queries
var userId = 42;
var userName = "John";
var (query, parameters) = SqlQuery.Create(
    $"SELECT * FROM Users WHERE Id = {userId} AND Name = {userName}");
Console.WriteLine(query);
// Output: "SELECT * FROM Users WHERE Id = @p0 AND Name = @p1"
Console.WriteLine(string.Join(", ", parameters));
// Output: "42, John"
            ๐ฏ Example: Logging with Conditional Evaluation
// Custom logger that only evaluates expensive operations if needed
public enum LogLevel { Debug = 1, Info = 2, Warn = 3, Error = 4 }
public class SmartLogger
{
    private LogLevel _currentLevel = LogLevel.Info;
    public void Log(
        LogLevel level,
        [System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute("this", "level")]
        LogInterpolatedStringHandler handler)
    {
        if (level >= _currentLevel)
        {
            Console.WriteLine(handler.ToString());
        }
    }
}
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct LogInterpolatedStringHandler
{
    private System.Text.StringBuilder? _builder;
    private bool _enabled;
    public LogInterpolatedStringHandler(
        int literalLength,
        int formattedCount,
        SmartLogger logger,
        LogLevel level,
        out bool isEnabled)
    {
        _enabled = level >= logger._currentLevel;
        isEnabled = _enabled;
        _builder = _enabled ? new System.Text.StringBuilder(literalLength) : null;
    }
    public void AppendLiteral(string s)
    {
        if (_enabled) _builder!.Append(s);
    }
    public void AppendFormatted<T>(T value)
    {
        if (_enabled) _builder!.Append(value);
    }
    public override string ToString() => _builder?.ToString() ?? string.Empty;
}
// Usage - Expensive operations only run if logging is enabled
var logger = new SmartLogger();
var data = new List<int> { 1, 2, 3 };
string ExpensiveOperation(IEnumerable<int> items)
{
    return string.Join(",", items.Select(i => (i * i).ToString()));
}
// This expensive operation only runs if Debug level is enabled
logger.Log(LogLevel.Debug,
    $"Processing {data.Count} items, details: {ExpensiveOperation(data)}");
            ๐ Real-World Example: URL Builder
public class UrlBuilder
{
    public static string Build(UrlInterpolatedStringHandler handler)
        => handler.ToString();
}
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct UrlInterpolatedStringHandler
{
    private System.Text.StringBuilder _builder;
    public UrlInterpolatedStringHandler(int literalLength, int formattedCount)
    {
    _builder = new System.Text.StringBuilder(literalLength);
    }
    public void AppendLiteral(string s) => _builder.Append(s);
    public void AppendFormatted(string value)
    {
        // Automatically URL-encode interpolated values
        _builder.Append(Uri.EscapeDataString(value ?? string.Empty));
    }
    public override string ToString() => _builder.ToString();
}
// Usage - Automatic URL encoding
var searchTerm = "C# programming";
var category = "Books & Media";
var url = UrlBuilder.Build(
    $"https://example.com/search?q={searchTerm}&category={category}");
Console.WriteLine(url);
// Output: "https://example.com/search?q=C%23%20programming&category=Books%20%26%20Media"
            ๐ก Key Benefits:
- Zero-allocation — Custom handlers can optimize memory usage
 - Conditional evaluation — Skip expensive operations when not needed
 - Type safety — Enforce formatting rules at compile time
 - Domain-specific — Create specialized string builders (SQL, URLs, etc.)
 - Better performance — Reduced allocations in hot paths
 
10. Collection Expressions (Preview)
๐ฆ Concise Collection Initialization
C# 14 introduces collection expressions, providing a unified, concise syntax for creating and initializing collections of any type. This feature simplifies code and makes collection creation more intuitive.
๐งช Example: Basic Collection Expressions
// Traditional way
var oldList = new List<int> { 1, 2, 3, 4, 5 };
var oldArray = new int[] { 1, 2, 3, 4, 5 };
// C# 14: Collection expressions - unified syntax
List<int> list = [1, 2, 3, 4, 5];
int[] array = [1, 2, 3, 4, 5];
Span<int> span = [1, 2, 3, 4, 5];
// Empty collections
List<string> empty = [];
int[] emptyArray = [];
            ๐ก Example: Spread Operator
// Combine collections with spread operator ..
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] third = [7, 8, 9];
// Spread and combine
int[] combined = [..first, ..second, ..third];
// Result: [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Mix literals and spreads
int[] mixed = [0, ..first, 10, ..second, 20];
// Result: [0, 1, 2, 3, 10, 4, 5, 6, 20]
// Spread with LINQ
var numbers = [1, 2, 3, 4, 5];
var doubled = [..numbers.Select(x => x * 2)];
// Result: [2, 4, 6, 8, 10]
            ๐ฏ Example: Pattern Matching with Collections
// Pattern matching with collection expressions
string DescribeCollection(int[] numbers) => numbers switch
{
    [] => "Empty collection",
    [var single] => $"Single element: {single}",
    [var first, var second] => $"Two elements: {first}, {second}",
    [var first, .., var last] => $"First: {first}, Last: {last}",
    _ => $"Collection with {numbers.Length} elements"
};
// Usage
Console.WriteLine(DescribeCollection([]));           // "Empty collection"
Console.WriteLine(DescribeCollection([42]));         // "Single element: 42"
Console.WriteLine(DescribeCollection([1, 2]));       // "Two elements: 1, 2"
Console.WriteLine(DescribeCollection([1, 2, 3, 4])); // "First: 1, Last: 4"
            ๐ฅ Real-World Example: API Response Building
public record ApiResponse(string Status, object[] Data);
public class UserService
{
    public ApiResponse GetUsers(bool includeAdmin, bool includeGuests)
    {
        var regularUsers = GetRegularUsers();
        var adminUsers = includeAdmin ? GetAdminUsers() : [];
        var guestUsers = includeGuests ? GetGuestUsers() : [];
        // Build response with collection expressions
        return new ApiResponse(
            "success",
            [
                ..regularUsers,
                ..adminUsers,
                ..guestUsers
            ]
        );
    }
    private object[] GetRegularUsers() => [
        new { Id = 1, Name = "Alice", Role = "User" },
        new { Id = 2, Name = "Bob", Role = "User" }
    ];
    private object[] GetAdminUsers() => [
        new { Id = 100, Name = "Admin", Role = "Admin" }
    ];
    private object[] GetGuestUsers() => [
        new { Id = 200, Name = "Guest", Role = "Guest" }
    ];
}
            ๐ Example: Building Test Data
// Easy test data creation
public class Product
{
    public string Name { get; set; } = string.Empty;
    public int Price { get; set; }
}
public static class TestDataBuilder
{
    public static List<Product> CreateTestProducts()
    {
        var electronics = CreateElectronics();
        var books = CreateBooks();
        var featured = new Product { Name = "Featured Item", Price = 999 };
        // Combine with collection expressions
        return [featured, ..electronics, ..books];
    }
    private static Product[] CreateElectronics() => [
        new Product { Name = "Laptop", Price = 1200 },
        new Product { Name = "Phone", Price = 800 }
    ];
    private static Product[] CreateBooks() => [
        new Product { Name = "C# Guide", Price = 45 },
        new Product { Name = "Design Patterns", Price = 55 }
    ];
}
// Simple check without external test frameworks
int Sum(int[] numbers) => numbers.Sum();
Console.WriteLine(Sum([1,2,3]));   // 6
Console.WriteLine(Sum([10,20,30])); // 60
            ⚡ Performance Example: Stack Allocation
// Collection expressions work with Span for stack allocation
public static void ProcessData()
{
    // Stack-allocated span with collection expression
    Span<int> numbers = [1, 2, 3, 4, 5];
    
    // Zero heap allocation!
    foreach (var num in numbers)
    {
        Console.WriteLine(num * 2);
    }
    // Combine with stackalloc for larger data
    Span<byte> buffer = stackalloc byte[256];
    Span<byte> header = [0xFF, 0xFE, 0xFD];
    
    // Copy header to buffer
    header.CopyTo(buffer);
}
            ๐ก Key Benefits:
- Unified syntax — Same syntax for arrays, lists, spans, and more
 - More readable — Cleaner, less verbose collection initialization
 - Spread operator — Easy collection combination and manipulation
 - Pattern matching — Powerful collection destructuring
 - Performance — Works with stack-allocated spans
 - Type inference — Compiler figures out the target type
 
๐ Conclusion
C# 14 continues the language's tradition of thoughtful, developer-friendly improvements. From extension members that extend types more naturally, to the field keyword that eliminates boilerplate, these features help you write code that's both more expressive and maintainable.
The addition of lambda parameter modifiers enables more efficient functional programming patterns, while partial events and constructors improve code organization in large projects. User-defined compound assignment operators make custom types feel more natural, and improved interpolated strings provide both performance and flexibility.
Finally, collection expressions bring a modern, unified syntax for working with collections, making code more concise and readable. As you adopt C# 14 in your projects, you'll find these small enhancements add up to significant productivity gains. The language continues to evolve in ways that respect existing code while opening new possibilities for elegant solutions.
๐ Next Steps:
- Experiment with these features in your own projects
 - Explore the official C# 14 documentation for more details
 - Share your experiences and patterns with the community
 - Stay tuned for more advanced use cases and best practices
 
Happy coding with C# 14! ๐