Common Interview Questions
Common Interview Questions Interview with follow-up questions
1. Can you explain the difference between a struct and a class in C#?
Structs and classes are both used to define custom types in C#, but they differ fundamentally in how they are stored, copied, and managed.
Core distinction: value type vs reference type
| Feature | struct (value type) |
class (reference type) |
|---|---|---|
| Storage | Stack (locals) or inline in containing type | Heap |
| Assignment | Copies all fields | Copies the reference |
| Default value | Zero-initialized struct | null |
| Nullability | Not nullable by default (use T?) |
Nullable |
| Inheritance | Cannot inherit from other structs/classes | Single class inheritance |
| Interface impl | Yes | Yes |
| Destructor/finalizer | No | Yes |
| GC overhead | None (unless boxed) | Yes |
Example — copy semantics:
// Struct — independent copy
var p1 = new Point(1, 2);
var p2 = p1;
p2 = p2 with { X = 99 }; // record struct: non-destructive mutation
Console.WriteLine(p1.X); // 1 — unchanged
// Class — shared reference
var c1 = new Container { Value = 1 };
var c2 = c1;
c2.Value = 99;
Console.WriteLine(c1.Value); // 99 — same object
Modern struct variants:
readonly struct(C# 7.2+): all members are readonly; prevents accidental mutation and defensive copies when passed asinparameter.ref struct(C# 7.2+): stack-only; cannot be boxed or stored on heap.SpanandReadOnlySpanare ref structs.readonly record struct(C# 10+): combines immutability + value equality + concise syntax:csharp readonly record struct Point(double X, double Y);
Boxing gotcha: assigning a struct to object or an interface variable boxes it — allocates a heap object. This is a hidden performance cost. Generics (List) avoid boxing entirely.
Microsoft guideline for using structs:
- Size ≤ 16 bytes
- Immutable (or treated as such)
- Not frequently boxed
- Not passed by value in performance-critical loops
Interviewers probe: when would you use a struct over a class? Small, immutable value objects (coordinates, colors, monetary amounts, key-value pairs). DateTime, Guid, decimal, KeyValuePair are all structs in the BCL.
Follow-up 1
What are the default values for variables in a class and a struct?
In C#, the default values for variables in a class and a struct are as follows:
Class Variables: Class variables are initialized with default values based on their data type. For reference types (e.g., string, object), the default value is
null. For numeric types (e.g., int, float), the default value is0. For bool, the default value isfalse.Struct Variables: Struct variables are also initialized with default values based on their data type. For numeric types (e.g., int, float), the default value is
0. For bool, the default value isfalse. For reference types (e.g., string, object), the default value is an instance of the type with all its fields initialized to their default values.
Follow-up 2
When would you choose to use a struct over a class?
You would choose to use a struct over a class in the following scenarios:
Small Data Structures: Structs are more suitable for small data structures that primarily contain data and do not require complex behavior. They are lightweight and have a smaller memory footprint compared to classes.
Value Semantics: If you want value semantics, where the object is copied when assigned or passed, rather than a reference, a struct is a better choice. This can be useful in scenarios where you want to avoid unintended side effects caused by modifying shared data.
Performance Considerations: Structs are allocated on the stack and do not require garbage collection, making them more efficient in terms of memory usage and performance. If performance is a critical factor, using structs can be beneficial.
Immutable Data: If the data in the structure is immutable (i.e., it cannot be modified after creation), using a struct can help enforce this immutability and prevent accidental modifications.
Follow-up 3
Can a struct inherit from another struct or class?
In C#, a struct can only inherit from an interface. It cannot inherit from another struct or class. Structs do not support inheritance in the same way that classes do. However, a struct can implement interfaces, allowing it to define and implement the members specified by the interface.
Follow-up 4
Can a struct implement interfaces?
Yes, a struct in C# can implement interfaces. By implementing an interface, a struct can define and implement the members specified by the interface. This allows the struct to provide a certain behavior or functionality defined by the interface. However, it is important to note that structs cannot inherit from classes or other structs, so interface implementation is the only way to define additional behavior for a struct.
2. What is the purpose of the 'using' statement in C#?
using in C# serves two distinct purposes. Interviewers test both — confusing them is a red flag.
1. using directive — namespace import
using System;
using System.Collections.Generic;
using static System.Math; // import static members
using Json = System.Text.Json; // alias to resolve conflicts
Global usings (C# 10+) apply project-wide:
// GlobalUsings.cs
global using System;
global using Microsoft.Extensions.Logging;
Modern .NET SDK projects enable enable by default, auto-generating global usings for System, System.Linq, System.Threading.Tasks, etc.
2. using statement / declaration — resource management
Ensures IDisposable.Dispose() is called when a resource goes out of scope — even if an exception occurs.
// Block form (pre-C# 8)
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
// ...
} // Dispose() called here
// Declaration form (C# 8+) — preferred, less nesting
using var conn = new SqlConnection(connectionString);
conn.Open();
// Dispose() called at end of enclosing scope (method, block, etc.)
await using for async disposal (C# 8+):
await using var reader = new StreamReader("file.txt");
For types implementing IAsyncDisposable — allows async teardown without blocking.
How using works: the compiler transforms it into a try/finally block. Dispose() is always called in the finally, so cleanup happens even if the guarded code throws.
Common IDisposable types: SqlConnection, FileStream, StreamReader, StreamWriter, HttpResponseMessage, DbContext (EF Core), HttpClient (but prefer IHttpClientFactory).
Interviewers probe: what if Dispose() throws? The exception from Dispose() propagates and may mask the original exception. If you need to handle both, use explicit try/finally. Also: why not rely on the finalizer for cleanup? Finalizers are non-deterministic — a DB connection held until GC runs wastes server resources.
Follow-up 1
What happens if we do not use the 'using' statement?
If we do not use the 'using' statement, we need to manually dispose of the object by calling its Dispose() method. Failing to do so can result in resource leaks and can cause the application to consume more memory than necessary. It is important to properly dispose of objects that implement IDisposable to free up system resources and improve the overall performance of the application.
Follow-up 2
Can you give an example of a situation where the 'using' statement can be particularly useful?
Sure! One common example is when working with file streams. Instead of manually opening and closing the file stream, we can use the 'using' statement to automatically dispose of the stream after we are done with it. Here's an example:
using (FileStream fileStream = new FileStream("example.txt", FileMode.Open))
{
// Code to read or write to the file
}
In this example, the file stream will be automatically disposed of when the code execution leaves the 'using' block, regardless of whether an exception occurs or not.
Follow-up 3
What is the difference between 'using' and 'import' in C#?
In C#, the 'using' statement is used to define namespaces that are used in the code file, while the 'import' statement is used in other programming languages like Python to bring in modules or libraries.
The 'using' statement in C# is used to create a reference to a namespace, allowing the code to access types and members within that namespace without having to fully qualify their names. It helps to simplify the code and make it more readable.
On the other hand, the 'import' statement in languages like Python is used to bring in modules or libraries that are not part of the built-in functionality. It allows the code to use functions, classes, and other objects defined in those modules or libraries without having to fully qualify their names.
3. What is the difference between 'const' and 'readonly' in C#?
Both const and readonly represent values that shouldn't change, but they differ in when the value is determined and what types they support.
const — compile-time constant:
public class Config
{
public const int MaxRetries = 3;
public const string DefaultName = "Unknown";
public const double Pi = 3.14159;
}
// Used as: Config.MaxRetries
- Value must be a compile-time literal (primitive types and
string) - Value is baked into the calling assembly at compile time
- Implicitly
static— no instance qualifier - Cannot be changed at runtime
readonly — runtime constant:
public class Config
{
public readonly int MaxConnections;
public static readonly ImmutableList AllowedOrigins =
ImmutableList.Create("https://example.com");
public Config(int maxConnections)
{
MaxConnections = maxConnections; // set in constructor
}
}
- Value can be any type
- Assigned at declaration or in constructor only
- Can be instance-level or
static - Value evaluated at runtime
Critical const gotcha — assembly versioning:
If Library A defines public const int MaxSize = 100; and Library B uses it, Library B bakes 100 into its compiled code. If you change MaxSize to 200 in Library A and redeploy only Library A, Library B still uses 100 — you must recompile Library B too.
readonly does not have this problem because the value is read at runtime from Library A.
static readonly for complex objects:
// WRONG — const can't hold complex objects
// public const List Tags = new(); // compile error
// CORRECT
public static readonly List Tags = new() { "alpha", "beta" };
Summary — which to use:
const: true mathematical/physical constants, magic strings in switch cases, values that genuinely never change and are primitive.static readonly: complex objects, values that might change between library versions, anything requiring runtime initialization.
Interviewers probe: why might changing a const in a library break dependent assemblies that weren't recompiled? Because the value is baked in at compile time — the calling assembly never reads the field at runtime.
Follow-up 1
Can you change the value of a 'const' after it is declared?
No, you cannot change the value of a 'const' after it is declared. 'const' is a compile-time constant, and its value must be known at compile-time. Once a 'const' is declared, its value cannot be modified.
Follow-up 2
When would you use 'readonly' instead of 'const'?
You would use 'readonly' instead of 'const' when you need a value that can be assigned at runtime, but cannot be modified after initialization. 'readonly' allows the value to be assigned at runtime, but only in the constructor or at the declaration. This is useful when you want to initialize a constant value based on some runtime logic or when the value needs to be different for each instance of a class.
Follow-up 3
Can 'readonly' be used with reference types?
Yes, 'readonly' can be used with reference types. When 'readonly' is used with a reference type, it means that the reference itself cannot be changed after initialization. However, the properties or fields of the referenced object can still be modified. It is important to note that 'readonly' only applies to the reference itself, not the referenced object.
4. What is the difference between 'out' and 'ref' parameters in C#?
out and ref both pass arguments by reference (the method receives a pointer to the caller's variable), but they have different initialization requirements and semantic intent.
ref — read and write an existing value:
void Double(ref int value) => value *= 2;
int x = 5;
Double(ref x);
Console.WriteLine(x); // 10
- Caller must initialize the variable before passing
- Method can read the value, then optionally modify it
- Useful when the method needs both to receive input and modify it in place
out — write-only output:
bool TryParse(string input, out int result)
{
// result MUST be assigned before returning
return int.TryParse(input, out result);
}
// Caller — no need to initialize first; inline declaration (C# 7+)
if (int.TryParse("42", out int value))
Console.WriteLine(value); // 42
// Discard with _ when you only care about success
if (int.TryParse("42", out _))
Console.WriteLine("Valid number");
- Caller does not need to initialize the variable
- Method must assign the value before returning
- Clearly signals "this is output, not input"
in — read-only reference (C# 7.2+):
void Process(in LargeStruct data)
{
// data.Field = 5; // compile error — cannot modify
Console.WriteLine(data.Field);
}
Passes by reference but prevents modification — avoids copying large structs without the risk of accidental mutation. Use with readonly struct for maximum efficiency.
C# 13 relaxed restrictions: ref and unsafe can now be used inside iterators (yield return) and async methods in contexts where safety can be verified.
Modern alternatives to out:
Prefer tuples for multi-value returns over out:
(bool Success, int Value) TryParse(string input) =>
int.TryParse(input, out var n) ? (true, n) : (false, 0);
var (ok, val) = TryParse("42");
Interviewers probe: when would you still use out over returning a tuple? out is idiomatic for TryX patterns (matches BCL conventions like int.TryParse, Dictionary.TryGetValue) and is often clearer for boolean-plus-value returns. Tuples are better for new APIs returning multiple distinct values.
Follow-up 1
Can you give an example of when you would use each?
Sure! Here's an example of when you would use 'ref' parameter:
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 5;
int y = 10;
Swap(ref x, ref y);
Console.WriteLine(x); // Output: 10
Console.WriteLine(y); // Output: 5
And here's an example of when you would use 'out' parameter:
bool TryParse(string input, out int number)
{
return int.TryParse(input, out number);
}
string userInput = "123";
if (TryParse(userInput, out int result))
{
Console.WriteLine(result); // Output: 123
}
In the first example, 'ref' is used to swap the values of two variables. In the second example, 'out' is used to parse a string into an integer and return the result.
Follow-up 2
What happens if you try to use an uninitialized variable as an 'out' parameter?
If you try to use an uninitialized variable as an 'out' parameter, you will get a compile-time error. 'out' parameters must be assigned a value inside the method before they are used. This is because 'out' parameters are used for output only, and the method is responsible for assigning a value to the parameter.
Follow-up 3
Can 'out' and 'ref' be used with value types, reference types, or both?
'out' and 'ref' can be used with both value types and reference types. When used with value types, the parameter is passed by reference, allowing the method to modify the value of the parameter. When used with reference types, the parameter is still passed by reference, but the method can modify the object that the parameter refers to, not the reference itself.
5. What is the purpose of a destructor in C#?
A destructor (also called a finalizer) in C# is a method that the Garbage Collector calls before reclaiming an object's memory. It provides a last-resort opportunity to release unmanaged resources.
Syntax:
public class NativeResource
{
private IntPtr _handle;
public NativeResource() => _handle = NativeLib.Allocate();
~NativeResource() // finalizer
{
NativeLib.Free(_handle);
}
}
Why finalizers are problematic:
- Non-deterministic — you don't know when (or if, during a quick process exit) the finalizer runs.
- GC overhead — objects with finalizers are promoted to the next GC generation, meaning they survive at least one extra collection before being reclaimed.
- Ordering not guaranteed — two finalizers may run in any order.
- Exceptions in finalizers crash the process (in .NET, an unhandled exception in a finalizer terminates the application by default).
- Finalizer thread is shared — slow finalizers delay all other finalizations.
The correct pattern — IDisposable:
For deterministic cleanup, implement IDisposable and use using:
public class NativeResource : IDisposable
{
private IntPtr _handle;
private bool _disposed;
public NativeResource() => _handle = NativeLib.Allocate();
public void Dispose()
{
if (!_disposed)
{
NativeLib.Free(_handle);
_disposed = true;
GC.SuppressFinalize(this); // remove from finalizer queue
}
}
~NativeResource() // safety net for callers who forget Dispose()
{
if (!_disposed) NativeLib.Free(_handle);
}
}
GC.SuppressFinalize(this) in Dispose() removes the object from the finalizer queue — if the user correctly called Dispose(), the finalizer never runs, avoiding the GC penalty.
IAsyncDisposable — for async cleanup scenarios (await using var resource = new AsyncResource()).
Interviewers probe: when is a finalizer actually justified? Only when wrapping a raw unmanaged handle that must be freed even if Dispose() is never called — and even then, prefer inheriting from SafeHandle (which already implements the finalize/dispose pattern correctly).
Follow-up 1
When is a destructor called?
A destructor in C# is called automatically by the garbage collector when an object is no longer referenced or when the program terminates. It is not possible to explicitly call a destructor.
Follow-up 2
Can you manually call a destructor?
No, you cannot manually call a destructor in C#. The destructor is automatically called by the garbage collector when the object is no longer referenced or when the program terminates.
Follow-up 3
What is the difference between a destructor and the 'Dispose' method?
The main difference between a destructor and the 'Dispose' method is that a destructor is automatically called by the garbage collector, while the 'Dispose' method needs to be called explicitly by the developer to release resources. The 'Dispose' method is typically used for deterministic resource cleanup, while the destructor is used for non-deterministic resource cleanup.
6. What is the difference between `Task.WhenAll` and `Task.WhenAny`?
Both are used to coordinate multiple concurrent Task objects, but they have different completion semantics.
Task.WhenAll(tasks) — completes when all tasks complete:
var task1 = FetchUserAsync(1);
var task2 = FetchOrdersAsync(1);
var task3 = FetchPreferencesAsync(1);
var (user, orders, prefs) = await (task1, task2, task3); // ValueTuple await (.NET 7+)
// or:
await Task.WhenAll(task1, task2, task3);
- All tasks run concurrently
- If any task throws,
WhenAllcollects all exceptions in anAggregateException; awaiting it rethrows the first - Returns
Taskwhen all tasks return the same type
Task.WhenAny(tasks) — completes when the first task completes:
var timeout = Task.Delay(TimeSpan.FromSeconds(5));
var work = DoSlowWorkAsync();
var completed = await Task.WhenAny(work, timeout);
if (completed == timeout)
throw new TimeoutException();
- Returns the first completed
Task(not the result) - Other tasks continue running unless you cancel them
- Classic use: implementing timeouts, racing multiple sources for the fastest response
.NET 9 — Task.WhenEach(): process tasks as they complete (without knowing which finishes first):
await foreach (var completedTask in Task.WhenEach(task1, task2, task3))
{
var result = await completedTask;
Console.WriteLine($"Got: {result}");
}
Interviewers probe: what happens to the losing tasks in WhenAny? They keep running unless explicitly cancelled with a CancellationToken. Always pass a token and cancel on completion to avoid resource leaks.
Live mock interview
Mock interview: Common Interview Questions
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.