Development

Advanced Tips & Tricks in C#

C# is a powerful and versatile programming language that provides many advanced features and techniques to make your code more efficient, maintainable, and robust. This article covers some advanced tips and tricks in C# with code examples that demonstrate how to take full advantage of these features.

1. Using Span<T> and Memory<T> for Efficient Memory Handling

Handling large datasets or performance-critical operations requires careful memory management. In .NET, Span<T> and Memory<T> allow you to work with contiguous memory in a more efficient way without allocating unnecessary heap memory. These types are stack-allocated and can be used to avoid copying large amounts of data.

C#
public static void ProcessData()
{
    var data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    
    // Span allows us to work with slices of the array without copying it
    Span<byte> span = data.AsSpan(2, 5);
    
    foreach (var item in span)
    {
        Console.WriteLine(item);  // Outputs 3, 4, 5, 6, 7
    }
}

Memory<T> is similar but allows for async-friendly operations, as it can be used across async/await boundaries.

2. Pattern Matching Enhancements

C# supports powerful pattern matching capabilities, which allow for concise and expressive code. In C# 9 and above, you can use pattern matching in switch expressions, property patterns, and more.

C#
public static string GetShapeDescription(object shape) => shape switch
{
    Circle { Radius: var r } => $"Circle with radius {r}",
    Rectangle { Width: var w, Height: var h } => $"Rectangle with width {w} and height {h}",
    _ => "Unknown shape"
};

public class Circle
{
    public double Radius { get; set; }
}

public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }
}

// Usage
var shape = new Circle { Radius = 5.0 };
Console.WriteLine(GetShapeDescription(shape)); // Output: Circle with radius 5

3. Local Functions

Local functions are methods defined within another method. They allow for cleaner, more organized code by encapsulating functionality that is only relevant within the parent method. These functions have access to the variables and parameters of the enclosing method, making them useful for refactoring complex logic.

C#
public static int Factorial(int n)
{
    if (n < 0) throw new ArgumentOutOfRangeException(nameof(n));

    return CalculateFactorial(n);

    int CalculateFactorial(int num)
    {
        return num <= 1 ? 1 : num * CalculateFactorial(num - 1);
    }
}

Local functions are more efficient than using lambda expressions for recursive logic since they do not allocate memory on the heap.

4. Asynchronous Streams (IAsyncEnumerable<T>)

In C# 8 and above, asynchronous streams (IAsyncEnumerable<T>) allow you to work with data streams in an async/await pattern. This is particularly useful when consuming large datasets that can be processed asynchronously, such as data from a database or a web API.

C#
public static async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(100); // Simulating async operation
        yield return i;
    }
}

public static async Task ProcessNumbersAsync()
{
    await foreach (var number in GenerateNumbersAsync(10))
    {
        Console.WriteLine(number);
    }
}

Using IAsyncEnumerable<T>, you can efficiently handle large or infinite data streams without blocking the main thread.

5. Records in C#

Records, introduced in C# 9, provide a succinct way to define immutable data types with built-in value-based equality. Records are ideal for situations where you need objects that represent data without the overhead of custom equality logic or mutability.

C#
public record Person(string FirstName, string LastName);

var person1 = new Person("John", "Doe");
var person2 = new Person("John", "Doe");

// Value-based equality comparison
Console.WriteLine(person1 == person2); // True

// Immutable by default, but you can create a new record with changes using "with" expressions
var person3 = person1 with { LastName = "Smith" };
Console.WriteLine(person3);  // Output: Person { FirstName = John, LastName = Smith }

Records are particularly useful in scenarios where data integrity and immutability are critical, such as in functional programming styles.

6. Expression-Bodied Members

Expression-bodied members allow you to write shorter, cleaner code by defining simple methods, properties, and constructors with expression syntax (=>). This feature is ideal for cases where the method body consists of a single expression.

C#
public class Point
{
    public int X { get; }
    public int Y { get; }

    // Constructor using expression-bodied syntax
    public Point(int x, int y) => (X, Y) = (x, y);

    // Read-only properties using expression-bodied syntax
    public int Distance => (int)Math.Sqrt(X * X + Y * Y);

    // Method using expression-bodied syntax
    public override string ToString() => $"Point ({X}, {Y})";
}

This feature reduces boilerplate and improves readability for simple operations.

7. Using Lock Efficiently with Lazy<T> for Thread-Safe Initialization

Thread safety is crucial in concurrent environments, and using Lazy<T> helps ensure that a resource is initialized in a thread-safe way without needing to implement complicated locking mechanisms.

C#
public class Singleton
{
    private static readonly Lazy<Singleton> _instance = new(() => new Singleton());

    public static Singleton Instance => _instance.Value;

    private Singleton()
    {
        // Private constructor to prevent instantiation
    }
}

// Usage
var singletonInstance = Singleton.Instance;

Lazy<T> initializes the object only when it’s first accessed, ensuring that multiple threads do not create multiple instances of the object.

8. Delegates and Events with Action and Func

In modern C#, you often don’t need to declare custom delegate types. Instead, you can use predefined delegates like Action, Func, and Predicate for more readable and concise code.

C#
// Using Func delegate
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4)); // Output: 7

// Using Action delegate
Action<string> printMessage = msg => Console.WriteLine(msg);
printMessage("Hello, world!"); // Output: Hello, world!

// Events with Action delegate
public class Publisher
{
    public event Action<string> OnMessageReceived;

    public void SendMessage(string message)
    {
        OnMessageReceived?.Invoke(message);
    }
}

// Usage
var publisher = new Publisher();
publisher.OnMessageReceived += (msg) => Console.WriteLine($"Message received: {msg}");
publisher.SendMessage("Hello Event!");  // Output: Message received: Hello Event!

These delegates are especially useful for events, callbacks, and other functional-style programming patterns.

C# is a rich language with a wide array of features designed to make your code more efficient, maintainable, and expressive. By incorporating these advanced techniques into your programming toolkit, you can write better-performing and more concise code. From efficient memory handling with Span<T> to immutability with records and more expressive syntax with pattern matching, C# offers a wide range of tools to improve your development experience.

Shares: