Nullable Reference Types, Async Streams, Ranges and Indices, Default Interface Methods, Switch Expressions, Pattern Matching Enhancements, and Static Local Functions

 

1. Nullable Reference Types

Nullable reference types were introduced in C# 8.0 to help reduce NullReferenceExceptions 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 dereference name 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 an IAsyncEnumerable<int>, allowing the await foreach loop in Main 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 in ILogger 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 the Multiply method and does not capture any variables from the enclosing method. This improves performance and ensures that the local function is self-contained.

Summary

  1. Nullable Reference Types: Introduces nullable annotations to reduce null reference exceptions by enforcing nullability checks at compile time.
  2. Async Streams: Provides asynchronous iteration over data sources using the IAsyncEnumerable<T> interface and the await foreach syntax.
  3. Ranges and Indices: Simplifies array and collection slicing with the ^ (index from end) and .. (range) operators.
  4. Default Interface Methods: Allows interfaces to have default method implementations, making it easier to add new methods without breaking existing implementations.
  5. Switch Expressions: Introduces a more concise syntax for switch statements, enabling simpler and more readable expressions.
  6. Pattern Matching Enhancements: Extends pattern matching capabilities to include property, tuple, and positional patterns, making it more versatile.
  7. Static Local Functions: Local functions that are static and do not capture state from their enclosing scope, improving performance and preventing unintended closures.

Post a Comment