Modern C# Features

by mahidhar

C# continues to evolve, introducing new features that enhance the language and make it more powerful and expressive. In this section, we will explore some of the modern features introduced in C# 9 and C# 10, as well as Nullable Reference Types.

C# 9/10 Features
Record Types:

Records are a new reference type that provides built-in functionality for encapsulating immutable data. They are particularly useful for defining data objects with value equality semantics.
Example:

code
using System;

namespace NationalParks
{
    public record Park(string Name, int YearEstablished);

    class Program
    {
        static void Main(string[] args)
        {
            Park yellowstone = new Park("Yellowstone", 1872);
            Park anotherYellowstone = new Park("Yellowstone", 1872);

            // Value equality
            Console.WriteLine(yellowstone == anotherYellowstone); // True

            // Immutable properties
            // yellowstone.Name = "NewName"; // Compile-time error

            // Deconstruction
            var (name, year) = yellowstone;
            Console.WriteLine($"Name: {name}, Year Established: {year}");
        }
    }
}

Pattern Matching Enhancements:

C# 9 and 10 introduced several enhancements to pattern matching, making it more powerful and expressive.
Example:

code
using System;

namespace NationalParks
{
    public class Park
    {
        public string Name { get; set; }
        public int YearEstablished { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            object obj = new Park { Name = "Yellowstone", YearEstablished = 1872 };

            // Type pattern
            if (obj is Park park)
            {
                Console.WriteLine($"Park: {park.Name}, Year Established: {park.YearEstablished}");
            }

            // Property pattern
            if (obj is Park { Name: "Yellowstone", YearEstablished: 1872 })
            {
                Console.WriteLine("This is Yellowstone established in 1872.");
            }

            // Switch expression
            string result = obj switch
            {
                Park { Name: "Yellowstone" } => "Welcome to Yellowstone!",
                Park p => $"Welcome to {p.Name}",
                _ => "Unknown park"
            };

            Console.WriteLine(result);
        }
    }
}

Top-level Statements:

C# 9 introduced top-level statements, which allow you to write the main code of a program without explicitly defining a Main method.
Example:

code
using System;

string[] parks = { "Yellowstone", "Yosemite", "Zion", "Grand Canyon" };

foreach (var park in parks)
{
    Console.WriteLine(park);
}

Nullable Reference Types
Nullable reference types, introduced in C# 8, help you avoid null reference exceptions by distinguishing between nullable and non-nullable reference types.

Enabling Nullable Reference Types:

Enable nullable reference types in your project by adding #nullable enable at the top of your files or by configuring your project file.
Example:

code
#nullable enable
using System;

namespace NationalParks
{
    public class Park
    {
        public string Name { get; set; } // Non-nullable by default
        public string? Description { get; set; } // Nullable reference type

        public Park(string name, string? description = null)
        {
            Name = name;
            Description = description;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Park yellowstone = new Park("Yellowstone");

            Console.WriteLine($"Name: {yellowstone.Name}");
            Console.WriteLine($"Description: {yellowstone.Description ?? "No description available"}");
        }
    }
}

Handling Nullable Reference Types:

Use null-forgiving operator (!) to assert that a nullable reference is not null.
Use null-coalescing operator (??) to provide a default value if a nullable reference is null.
Use null-coalescing assignment operator (??=) to assign a value to a nullable reference if it is null.
Example:

code
#nullable enable
using System;

namespace NationalParks
{
    public class Park
    {
        public string Name { get; set; }
        public string? Description { get; set; }

        public Park(string name, string? description = null)
        {
            Name = name;
            Description = description;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Park? nullablePark = GetParkByName("Yellowstone");

            // Using null-forgiving operator
            Console.WriteLine(nullablePark!.Name); // Asserts that nullablePark is not null

            // Using null-coalescing operator
            string description = nullablePark?.Description ?? "No description available";
            Console.WriteLine(description);

            // Using null-coalescing assignment operator
            nullablePark.Description ??= "A beautiful national park.";
            Console.WriteLine(nullablePark.Description);
        }

        static Park? GetParkByName(string name)
        {
            // Simulate a null return value
            return name == "Yellowstone" ? new Park("Yellowstone") : null;
        }
    }
}

Advanced Usage and Best Practices
Combining Features for Enhanced Functionality:

Combine record types with nullable reference types for data models.
Use pattern matching in conjunction with nullable reference types to handle various conditions safely.

code
#nullable enable
using System;

namespace NationalParks
{
    public record Park(string Name, int YearEstablished, string? Description);

    class Program
    {
        static void Main(string[] args)
        {
            Park? nullablePark = GetParkByName("Yellowstone");

            // Pattern matching with nullable reference types
            if (nullablePark is { Description: not null } park)
            {
                Console.WriteLine($"Park: {park.Name}, Description: {park.Description}");
            }
            else if (nullablePark is { Description: null })
            {
                Console.WriteLine($"Park: {nullablePark.Name}, No description available.");
            }
            else
            {
                Console.WriteLine("Park not found.");
            }
        }

        static Park? GetParkByName(string name)
        {
            // Simulate a data retrieval
            return name == "Yellowstone" ? new Park("Yellowstone", 1872, "First national park in the world.") : null;
        }
    }
}

Best Practices:

Always enable nullable reference types for new projects to benefit from additional compile-time safety.
Use records for immutable data models, especially when equality and concise syntax are important.
Leverage pattern matching to simplify complex conditional logic.
Keep your code clean and expressive by using top-level statements for small programs or scripts.
By understanding and effectively using these modern C# features, you can write more robust, concise, and expressive code, making your applications easier to maintain and less prone to errors.