Skip to content

8.0

C# 8.0, released with .NET Core 3.0 in September 2019, introduced several significant features that aimed to improve code safety, readability, and reduce boilerplate code. Here are some of the key features introduced in C# 8.0, along with examples and comparisons to earlier versions of C#:

1. Nullable Reference Types

C# 8.0 introduced nullable reference types to help developers avoid null reference exceptions. This feature allows the compiler to provide warnings when code might dereference null.

C# 8.0

#nullable enable
string? nullableString = null;
string nonNullableString = nullableString; // Warning: Converting null literal or possible null value to non-nullable type.

C# < 8.0 In previous versions, reference types could always hold null, but the compiler didn't provide warnings for potential null dereference.

string nullableString = null;
string nonNullableString = nullableString; // No warning, but could result in a runtime null reference exception.

2. Default Interface Methods

C# 8.0 allows interfaces to define default implementations for members. This lets you add new methods to an interface without breaking existing implementations.

C# 8.0

interface ILogger
{
    void Log(string message);
    void LogWarning(string message) => Log($"Warning: {message}");
}

C# < 8.0 Previously, adding a new method to an interface required implementing the method in all classes that implement the interface, potentially breaking existing code.

interface ILogger
{
    void Log(string message);
    // Adding a new method here would require changes to all implementing classes.
}

3. Pattern Matching Enhancements

C# 8.0 expanded pattern matching capabilities, including switch expressions, property patterns, tuple patterns, and positional patterns.

C# 8.0

public string GetQuadrant(Point point) => point switch
{
    { X: 0, Y: 0 } => "Origin",
    { X: > 0, Y: > 0 } => "Quadrant 1",
    _ => "Other"
};

C# < 8.0 Pattern matching existed before C# 8.0 but was less flexible. For instance, switch statements were more verbose and didn't support deconstructing types or property patterns.

public string GetQuadrant(Point point)
{
    switch (point)
    {
        case Point var p when p.X > 0 && p.Y > 0:
            return "Quadrant 1";
        case Point var p when p.X == 0 && p.Y == 0:
            return "Origin";
        default:
            return "Other";
    }
}

4. Async Streams

C# 8.0 introduced the IAsyncEnumerable<T> interface and await foreach syntax to support asynchronous streams, which are useful for processing sequences of data asynchronously.

C# 8.0

public async IAsyncEnumerable<int> GetNumbersAsync()
{
    for (int i = 0; i < 3; i++)
    {
        await Task.Delay(100); // Simulate async work
        yield return i;
    }
}

await foreach (var number in GetNumbersAsync())
{
    Console.WriteLine(number);
}

C# < 8.0 Asynchronous processing of streams required more cumbersome approaches, such as using Task<IEnumerable<T>> which doesn't allow for streaming of data as it's produced.

public async Task<IEnumerable<int>> GetNumbersAsync()
{
    List<int> numbers = new List<int>();
    for (int i = 0; i < 3; i++)
    {
        await Task.Delay(100); // Simulate async work
        numbers.Add(i);
    }
    return numbers;
}

var numbers = await GetNumbersAsync();
foreach (var number in numbers)
{
    Console.WriteLine(number);
}

5. Using Declarations

C# 8.0 introduced using declarations, a more concise way to ensure that IDisposable objects are properly disposed of, reducing the need for boilerplate using blocks.

C# 8.0

using var file = new StreamWriter("file.txt");
file.WriteLine("Hello, C# 8.0!");
// The StreamWriter is automatically disposed at the end of the scope.

C# < 8.0 Previously, using statements required more verbose block syntax.

using (var file = new StreamWriter("file.txt"))
{
    file.WriteLine("Hello, C#!");
    // Exiting the using block disposes the StreamWriter.
}