Menu
CoddyTech

Modern C# Features Interview Questions

Records, patterns, nullable references, init and Span<T>; they need a modern SDK, so output is in comments.

10 questions
  1. What is a record in C#, and how is it different from a class?

    A record (C# 9) is a class with generated value-based equality: two records are equal when the runtime type and all fields match. It also gets a readable ToString(), deconstruction and with copies.

    public record Person(string Name, int Age);   // positional record, init-only properties
    
    var a = new Person("Asha", 30);
    var b = new Person("Asha", 30);
    Console.WriteLine(a == b);          // True  (a class would print False)
    Console.WriteLine(a);               // Person { Name = Asha, Age = 30 }
    
    var older = a with { Age = 31 };    // copy with one property changed
    var (name, age) = older;            // deconstruction
    

    record struct (C# 10) is the value-type version. See records.

  2. What are switch expressions and pattern matching in modern C#?

    A switch expression (C# 8) is a switch that returns a value, with patterns in its arms: type, property, relational and and/or/not patterns (C# 9), and list patterns (C# 11).

    static string Classify(object o) => o switch
    {
        null                    => "null",
        int n when n < 0        => "negative int",
        int and (0 or 1)        => "bit",
        int                     => "int",
        string { Length: 0 }    => "empty string",
        string s                => $"string of {s.Length}",
        _                       => "something else"
    };
    
    int[] nums = [1, 2, 3];
    if (nums is [1, .., var last])          // list pattern, C# 11
        Console.WriteLine(last);            // 3
    

    Arms are tried top to bottom, so put specific patterns first. See switch expressions.

  3. What are nullable reference types in C# 8?

    With <Nullable>enable</Nullable>, string means "never null" and string? means "may be null", and the compiler warns on possible null dereferences.

    #nullable enable
    string name = null;          // warning CS8625: null literal to non-nullable type
    string? nick = null;         // fine
    
    int len = nick.Length;       // warning CS8602: dereference of a possibly null reference
    int ok = nick?.Length ?? 0;  // fine
    
    string forced = nick!;       // ! tells the compiler "trust me", no runtime check
    

    It is compile-time only: at run time a string can still be null, for example after deserialization.

  4. What do init and required do on properties?

    init (C# 9) makes a property settable only during object creation; required (C# 11) forces callers to set it in the object initializer.

    public class Order
    {
        public required string Id { get; init; }
        public decimal Total { get; init; }
    }
    
    var o = new Order { Id = "A-17", Total = 99m };  // fine
    o.Total = 10m;                                   // error: init-only property
    var bad = new Order { Total = 5m };              // error CS9035: Id is required
    

    Together they give immutable objects without a long constructor.

  5. What are top-level statements, global usings and file-scoped namespaces?

    Top-level statements (C# 9) let Program.cs hold code without a class Program and Main; global usings (C# 10) import a namespace for the whole project; file-scoped namespaces (C# 10) remove one level of braces.

    // Program.cs, the whole file (top-level statements)
    Console.WriteLine("Hello");
    return 0;
    
    // GlobalUsings.cs
    global using System.Text.Json;
    
    // Customer.cs, file-scoped namespace
    namespace Shop.Models;
    
    public class Customer { }
    

    Only one file per project can have top-level statements.

  6. What are primary constructors and collection expressions in C# 12?

    C# 12 lets a class or struct declare constructor parameters next to its name (primary constructors) and build collections with brackets and the .. spread (collection expressions).

    public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
    {
        public Order Get(int id)
        {
            log.LogInformation("Loading {Id}", id);
            return repo.Find(id);           // parameters are in scope everywhere in the class
        }
    }
    
    int[] a = [1, 2, 3];
    List<int> b = [0, .. a, 4];             // 0, 1, 2, 3, 4
    Span<char> empty = [];
    

    Trap: in a class, those parameters do not become public properties as in a record; they are mutable captured variables.

  7. What are Span<T> and Memory<T>, and why do they matter for performance?

    Span<T> (C# 7.2) is a view over contiguous memory (an array, part of a string, stack memory) that slices without copying. It is a ref struct that lives only on the stack; Memory<T> can be stored in fields and used across await.

    ReadOnlySpan<char> line = "2026-10-09,ORDER,149.50";
    int comma = line.IndexOf(',');
    ReadOnlySpan<char> date = line.Slice(0, comma);   // no new string allocated
    decimal total = decimal.Parse(line.Slice(line.LastIndexOf(',') + 1));
    
    Span<byte> buffer = stackalloc byte[256];         // stack memory, no GC
    

    A span cannot be a field of a class, be boxed, or be captured by a lambda.

  8. What are default interface methods (C# 8)?

    Since C# 8 (.NET Core 3.0 and later), an interface member can have a body that implementers get unless they write their own, so a library can add a member without breaking them.

    public interface ILogger
    {
        void Log(string message);
        void LogError(string message) => Log("ERROR: " + message);   // default body
    }
    
    class ConsoleLogger : ILogger
    {
        public void Log(string message) => Console.WriteLine(message);
    }
    
    ILogger log = new ConsoleLogger();
    log.LogError("disk full");        // works through the interface
    // new ConsoleLogger().LogError(...)  does not compile: not a class member
    

    The default member is reachable only through the interface type, and it does not work on .NET Framework.

  9. What do ^1 and .. mean in C#?

    They are indices and ranges (C# 8): ^n counts from the end, so a[^1] is the last element, and a[start..end] takes a slice with the end excluded. Either side of .. can be left out.

    int[] a = { 10, 20, 30, 40, 50 };
    
    Console.WriteLine(a[^1]);                      // 50, the last element
    Console.WriteLine(string.Join(",", a[1..3]));  // 20,30 (the end is excluded)
    Console.WriteLine(string.Join(",", a[^2..]));  // 40,50
    Console.WriteLine("interview"[..5]);           // inter
    
    Span<int> view = a.AsSpan()[1..4];             // 20, 30, 40 without copying
    

    On an array or a string a range makes a copy; on a Span<T> it slices without copying. ^0 is one past the end, so a[^0] throws IndexOutOfRangeException. The System.Index and System.Range types behind them need .NET Core 3.0 or later.

  10. What are async streams (IAsyncEnumerable<T> and await foreach)?

    Async streams (C# 8) combine yield return with await: the producer awaits between items, and the consumer handles each item as it arrives instead of waiting for a whole Task<List<T>>.

    static async IAsyncEnumerable<int> ReadPagesAsync(
        [EnumeratorCancellation] CancellationToken token = default)
    {
        for (int page = 1; page <= 3; page++)
        {
            await Task.Delay(100, token);    // stands in for a database or HTTP call
            yield return page;
        }
    }
    
    await foreach (int page in ReadPagesAsync())
        Console.WriteLine($"page {page}");   // page 1, page 2, page 3, one every 100 ms
    

    Use them for paged APIs, database cursors and streamed responses; EF Core's AsAsyncEnumerable() returns one. For cancellation, mark the token parameter [EnumeratorCancellation] so a caller can pass a token with WithCancellation(token).

Coddy programming languages illustration

Learn to code with Coddy

Get started