1. Nullable Reference Types
Nullable reference types were introduced in C# 8.0 to help reduce NullReferenceException
s by enabling compile-time nullability checks. By default, reference types are non-nullable, but you can explicitly declare a reference type as nullable using the ?
syntax.
Example: Nullable Reference Types
class Program
{
static string? GetName(bool returnNull)
{
if (returnNull) return null;
return "John Doe";
}
static void Main()
{
string? name = GetName(true); // Nullable reference type
if (name != null)
{
Console.WriteLine(name.ToUpper());
}
else
{
Console.WriteLine("Name is null");
}
}
}
- Explanation: The
GetName
method returns a nullable string (string?
). The compiler will issue warnings if you attempt to dereferencename
without checking if it's null, reducing the chances of null reference exceptions.
2. Async Streams
Async streams, introduced in C# 8.0, allow for asynchronous iteration over data sources using the IAsyncEnumerable<T>
interface and the await foreach
syntax. This is useful for processing data asynchronously in a streaming manner.
Example: Async Streams
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
// Async method returning an async stream of integers
static async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(1000); // Simulate async work
yield return i;
}
}
static async Task Main()
{
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine(number); // Outputs 1 to 5, with 1 second delay between each
}
}
}
- Explanation: The
GenerateNumbersAsync
method returns anIAsyncEnumerable<int>
, allowing theawait foreach
loop inMain
to asynchronously consume the data as it is yielded.
3. Ranges and Indices
Ranges and indices simplify array and collection slicing in C# 8.0. The ^
operator is used to count from the end of a collection, and the ..
operator allows you to specify ranges.
Example: Ranges and Indices
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
// Using range syntax to get a slice of the array
int[] slice = numbers[1..3]; // Contains {2, 3}
Console.WriteLine(string.Join(", ", slice));
// Using index from the end
int last = numbers[^1]; // Last element, 5
Console.WriteLine(last);
}
}
- Explanation: The
1..3
range slices the array from index 1 to index 3 (excluding the last index), and the^1
index returns the last element of the array.
4. Default Interface Methods
Default interface methods allow you to define method implementations within an interface. This helps avoid breaking existing implementations when adding new methods to an interface.
Example: Default Interface Methods
interface ILogger
{
void Log(string message);
// Default method implementation
void LogInfo(string message)
{
Log($"INFO: {message}");
}
}
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
class Program
{
static void Main()
{
ILogger logger = new ConsoleLogger();
logger.LogInfo("This is a log message."); // Output: INFO: This is a log message.
}
}
- Explanation: The
LogInfo
method inILogger
is a default implementation that can be overridden by implementing classes but is available by default if they don’t.
5. Switch Expressions
Switch expressions are a more concise way of writing switch statements, introduced in C# 8.0. They use expression syntax, making them cleaner and easier to read.
Example: Switch Expressions
class Program
{
static string GetSeason(int month) => month switch
{
12 or 1 or 2 => "Winter",
3 or 4 or 5 => "Spring",
6 or 7 or 8 => "Summer",
9 or 10 or 11 => "Fall",
_ => "Unknown"
};
static void Main()
{
Console.WriteLine(GetSeason(3)); // Output: Spring
}
}
- Explanation: The switch expression maps the
month
value to a string representing a season. It’s a more concise and readable alternative to the traditional switch statement.
6. Pattern Matching Enhancements
Pattern matching has been extended to support more scenarios, including property patterns, tuple patterns, and positional patterns. These enhancements allow for more flexible and powerful pattern matching in switch statements and expressions.
Example: Pattern Matching Enhancements
class Program
{
static string DescribePerson(object person) => person switch
{
(string name, int age) when age < 18 => $"{name} is a minor.",
(string name, int age) => $"{name} is an adult.",
_ => "Unknown person"
};
static void Main()
{
var person = ("Alice", 17);
Console.WriteLine(DescribePerson(person)); // Output: Alice is a minor.
}
}
- Explanation: The switch expression uses tuple patterns to match both the name and age of a person. This example demonstrates positional pattern matching with tuples.
7. Static Local Functions
Static local functions were introduced in C# 8.0 and are local functions that do not capture any variables from the surrounding scope. This prevents unintentional closures and improves performance.
Example: Static Local Functions
class Program
{
static int Multiply(int a, int b)
{
static int MultiplyValues(int x, int y) => x * y; // Static local function
return MultiplyValues(a, b);
}
static void Main()
{
Console.WriteLine(Multiply(3, 4)); // Output: 12
}
}
- Explanation: The static local function
MultiplyValues
is declared inside theMultiply
method and does not capture any variables from the enclosing method. This improves performance and ensures that the local function is self-contained.
Summary
- Nullable Reference Types: Introduces nullable annotations to reduce null reference exceptions by enforcing nullability checks at compile time.
- Async Streams: Provides asynchronous iteration over data sources using the
IAsyncEnumerable<T>
interface and theawait foreach
syntax. - Ranges and Indices: Simplifies array and collection slicing with the
^
(index from end) and..
(range) operators. - Default Interface Methods: Allows interfaces to have default method implementations, making it easier to add new methods without breaking existing implementations.
- Switch Expressions: Introduces a more concise syntax for switch statements, enabling simpler and more readable expressions.
- Pattern Matching Enhancements: Extends pattern matching capabilities to include property, tuple, and positional patterns, making it more versatile.
- Static Local Functions: Local functions that are static and do not capture state from their enclosing scope, improving performance and preventing unintended closures.