Modern C# Features

Learning Objectives

  • Understand features introduced after C# 6, such as nullable reference types, pattern matching, records, and tuples.
  • Learn how to use asynchronous programming with async and await.
  • Apply modern C# features to enhance code readability, reliability, and performance.

C# Version Updates

Nullable Reference Types

In earlier versions of C#, reference types could be null by default, often leading to runtime errors (null reference exceptions). Starting with C# 8, nullable reference types were introduced to provide better null safety.

  • You can declare a reference type as nullable (string? name) or non-nullable (string name).
  • This feature helps in catching potential null issues during compile time.

Code Example:

string? name = null;
if (name != null)
{
    Console.WriteLine(name.Length);
}

Real-Life Example: In a user management system, ensuring fields like email are non-nullable helps prevent runtime errors when accessing user data.

Pattern Matching

Pattern matching helps simplify complex conditional logic by allowing you to match an object against a set of patterns.

Introduced in C# 7, this feature has been enhanced over versions to support switch expressions, type patterns, and relational patterns.

Code Example:

object value = 42;
if (value is int number && number > 0)
{
    Console.WriteLine($"The number is {number}");
}

Real-Life Example: In an e-commerce application, pattern matching can be used to validate different types of payment methods before processing a transaction.

Records

were introduced in C# 9 to simplify creating data-carrying classes. Unlike traditional classes, records provide value-based equality and can be easily used to represent immutable data.

They are especially useful for storing data that doesn’t need to change after initialization.

Code Example:

public record Product(string Name, decimal Price);

Real-Life Example: In an e-commerce system, you can use records to represent product data that doesn’t change, such as the name and price of an item.

Tuples

Tuples provide an easy way to store multiple values without creating a separate class. C# 7 introduced improved value tuples that support better deconstruction.

Code Example:

var product = (Name: "Laptop", Price: 1200.00M);
Console.WriteLine($"Product: {product.Name}, Price: {product.Price}");

Real-Life Example: Use tuples to return multiple values from a method, such as returning product details and availability status in a single call.

Asynchronous Programming

Async/Await

Asynchronous programming is key for building responsive and scalable APIs. Using async and await allows you to run time-consuming operations without blocking the main thread.

  • Task: Represents an ongoing operation that will complete in the future.
  • await: Ensures that the method is paused until the awaited task completes, allowing the application to remain responsive.

Code Example:

public async Task<string> GetProductAsync(int id)
{
    await Task.Delay(1000); // Simulate async work, like a database call
    return $"Product {id}";
}

Real-Life Example: In a financial application, async/await can be used for querying transaction details, ensuring the user interface stays responsive while waiting for server responses.

Best Practices for Asynchronous Programming:

  1. Avoid Blocking Calls: Avoid using .Wait() or .Result() in async code as it may lead to deadlocks.
  2. Use ConfigureAwait(false) when awaiting in library code to prevent capturing the calling context, thus avoiding potential deadlocks.

Examples

Pattern Matching with Switch Expressions

Use a switch expression with pattern matching to determine discounts based on customer type.

Code Snippet:

public decimal GetDiscount(object customer)
{
    return customer switch
    {
        RegularCustomer => 0.05m,
        PremiumCustomer => 0.10m,
        Employee => 0.15m,
        _ => 0.00m
    };
}

The GetDiscount method takes an object named customer as its parameter and determines the discount percentage based on the type of the customer. It uses a switch expression with pattern matching to identify the customer type and return the appropriate discount.

The method checks if the customer is an instance of certain types, like RegularCustomer, PremiumCustomer, or Employee.

Each type has a corresponding discount value:

  • RegularCustomer gets a 5% discount (0.05m).
  • PremiumCustomer gets a 10% discount (0.10m).
  • Employee gets a 15% discount (0.15m).
  • If the customer doesn't match any of these types, the default case (_) returns a discount of 0% (0.00m).

The m after the numeric values indicate that the number is a decimal type, which is often used in financial calculations for precision.

Classes for Different Customer Types

For this method to work, we need classes to represent the different customer types (RegularCustomer, PremiumCustomer, Employee):

public class RegularCustomer
{
    public string Name { get; set; }
    public RegularCustomer(string name) => Name = name;
}

public class PremiumCustomer
{
    public string Name { get; set; }
    public PremiumCustomer(string name) => Name = name;
}

public class Employee
{
    public string Name { get; set; }
    public Employee(string name) => Name = name;
}

Context and Example Usage

Let’s say you have a shopping scenario where you want to apply discounts based on the type of customer making the purchase.

Below is an example of how you might call the GetDiscount method.

public class Program
{
    public static void Main()
    {
        // Creating instances of different customer types
        var regularCustomer = new RegularCustomer("Alice");
        var premiumCustomer = new PremiumCustomer("Bob");
        var employee = new Employee("Charlie");

        // Creating an instance of the discount calculator
        var discountCalculator = new DiscountCalculator();

        // Calling GetDiscount for different customers
        decimal regularCustomerDiscount = discountCalculator.GetDiscount(regularCustomer);
        decimal premiumCustomerDiscount = discountCalculator.GetDiscount(premiumCustomer);
        decimal employeeDiscount = discountCalculator.GetDiscount(employee);

        // Outputting the discount values
        Console.WriteLine($"Regular customer discount: {regularCustomerDiscount * 100}%");
        Console.WriteLine($"Premium customer discount: {premiumCustomerDiscount * 100}%");
        Console.WriteLine($"Employee discount: {employeeDiscount * 100}%");
    }
}

public class DiscountCalculator
{
    public decimal GetDiscount(object customer)
    {
        return customer switch
        {
            RegularCustomer => 0.05m,
            PremiumCustomer => 0.10m,
            Employee => 0.15m,
            _ => 0.00m
        };
    }
}

Key Takeaways

  • Modern C# features, like nullable reference types, pattern matching, and records, help improve code quality, readability, and safety.
  • Async/await is crucial for building scalable, non-blocking APIs.
  • Applying these features in real-world examples, such as e-commerce or user management, shows their value in professional projects.

Practical Questions

  1. What is the purpose of nullable reference types, and how do they improve code safety?
  2. How can pattern matching simplify conditional logic in your code?
  3. What are the benefits of using async/await for API development?

ניווט במאמר

מאמרים אחרונים

Weekly Tutorial