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:
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:
var a = new StringBuilder("x");
var b = new StringBuilder("x");
bool sameObject = ReferenceEquals(a, b); // false
bool sameText = a.ToString().Equals(b.ToString()); // trueInterview 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:
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 sharedInterview 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:
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:
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:
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:
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:
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.