Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Module 1 · Modern C#, Async and ASP.NET Core · Lesson 2 of 10

    Equality, copying, nullability, and safe APIs

    Question 17. What is IEquatable<T> and why is it useful?

    Answer: IEquatable<T> provides a strongly typed equality method. For value types, it avoids boxing during equality checks. For domain keys, it makes equality explicit and faster in collections.

    Example:

    C#
    public readonly struct ProductCode : IEquatable<ProductCode>
    {
        public string Value { get; }
        public bool Equals(ProductCode other) => Value == other.Value;
        public override bool Equals(object? obj) => obj is ProductCode other && Equals(other);
        public override int GetHashCode() => Value.GetHashCode(StringComparison.Ordinal);
    }

    Interview tip: If you implement IEquatable<T>, still override object.Equals and GetHashCode consistently.

    Question 18. ReferenceEquals vs Equals: what is the difference?

    Answer: ReferenceEquals checks whether two references point to the same object instance. Equals checks logical equality and can be overridden. For strings and records, Equals often means value equality.

    Example:

    C#
    var a = new StringBuilder("x");
    var b = new StringBuilder("x");
    bool sameObject = ReferenceEquals(a, b); // false
    bool sameText = a.ToString().Equals(b.ToString()); // true

    Interview tip: Use ReferenceEquals only when identity matters, such as detecting object reuse or sentinel objects.

    Question 19. What is shallow copy vs deep copy?

    Answer: A shallow copy copies the object and its references. A deep copy also copies the referenced objects. Shallow copies are fast but shared nested state can cause bugs.

    Example:

    C#
    var original = new Order
    {
        Id = 1,
        Items = new List<string> { "Book" }
    };
    var shallow = original with { Id = 2 }; // for records
    shallow.Items.Add("Pen"); // original.Items also sees Pen if Items is shared

    Interview tip: For immutable nested values, shallow copy is usually fine. For mutable nested values, be explicit.

    Question 20. What are checked and unchecked numeric contexts?

    Answer: checked makes integer overflow throw an OverflowException. unchecked allows wraparound. Constant expressions may be checked by default, while runtime arithmetic depends on compiler settings or explicit blocks.

    Example:

    C#
    checked
    {
        int x = int.MaxValue;
        x++; // throws OverflowException
    }
    
    unchecked
    {
        int y = int.MaxValue + 1; // wraps to int.MinValue
    }

    Interview tip: Use checked for money-like counters and business-sensitive arithmetic. Use unchecked only when wraparound is intentional.

    Question 21. What are nullable analysis attributes such as MemberNotNull?

    Answer: Nullable attributes give the compiler extra information about null-state that it cannot infer. MemberNotNull tells the compiler a method initializes specific nullable members.

    Example:

    C#
    private string? _connectionString;
    [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_connectionString))]
    private void EnsureConfigured()
    {
        _connectionString ??= LoadConnectionString();
    }
    
    void Connect()
    {
        EnsureConfigured();
        Console.WriteLine(_connectionString.Length);
    }

    Interview tip: These attributes do not perform runtime validation by themselves; they improve compile-time analysis.

    Question 22. What is the difference between a guard clause and validation?

    Answer: A guard clause protects a method from invalid programmer input immediately. Validation checks user or external data and usually returns a friendly error collection. Mixing them causes poor API behavior.

    Example:

    C#
    public void CreateOrder(CreateOrderRequest request)
    {
        ArgumentNullException.ThrowIfNull(request); // guard
        if (request.Quantity <= 0) // validation
            throw new ValidationException("Quantity must be positive.");
    }

    Interview tip: In Web APIs, user-input validation usually returns 400; programmer errors should fail fast during development.

    Question 23. What are discriminated unions, and how do developers approximate them in C#?

    Answer: C# does not have built-in discriminated unions like F#. Developers approximate them with records, inheritance, OneOf-style libraries, or result types.

    Example:

    C#
    public abstract record PaymentResult;
    public sealed record PaymentAccepted(string TransactionId) : PaymentResult;
    public sealed record PaymentDeclined(string Reason) : PaymentResult;
    static string Describe(PaymentResult result) => result switch
    {
        PaymentAccepted ok => $"Accepted: {ok.TransactionId}",
        PaymentDeclined bad => $"Declined: {bad.Reason}",
        _ => throw new InvalidOperationException()
    };

    Interview tip: This pattern is useful when a method has multiple valid outcomes and exceptions would be misleading.

    Question 24. What is a Result type and when is it better than exceptions?

    Answer: A Result type models expected success/failure as data. It is better for business outcomes such as validation failure, insufficient balance, or duplicate request. Exceptions should remain for unexpected failures.

    Example:

    C#
    public sealed record Result<T>(bool Success, T? Value, string? Error)
    {
        public static Result<T> Ok(T value) => new(true, value, null);
        public static Result<T> Fail(string error) => new(false, default, error);
    }

    Interview tip: Do not replace all exceptions with Result. Use it where failure is part of the normal domain flow.

    Sign in to mark lessons done and keep your place in the course.Sign in