C# classes and structs interview questions, with answers
C# offers several ways to define a type, and interviewers use them to find out whether you know what each one costs and promises. A class and a struct differ in how they are copied, a record adds value equality, a property can compute a value or guard a field, and a static class can never be instantiated. Every answer below comes with code whose output was produced by compiling and running it on .NET 10.
The questions start with the choice between a class and a struct, move through records and properties, and end with the newer ways C# lets you construct objects. Then take the free C# diagnostic — ten questions across every C# topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is the difference between a class and a struct in C#, and when should you use a struct?
In short: A class is a reference type with identity and inheritance; a struct is a value type copied on assignment, best for small, immutable data such as a coordinate or an amount.
Assigning or passing a struct copies all its fields, so each copy is independent, while assigning a class copies only a reference to one shared object. Structs cannot inherit from other structs or classes, though they can implement interfaces, and they cannot be null unless declared as a nullable value type, T?. They avoid a heap allocation when used as locals or array elements, which is why DateTime, TimeSpan and Guid are structs. The guidance is to use a struct only for small values, roughly 16 bytes or less, that are logically one value and immutable, ideally declared readonly struct: a large or mutable struct is copied more often than you expect, and changes made to a copy are silently lost.
var p = new Coord(1, 2); var q = p; q.X = 9; Console.WriteLine(p.X); struct Coord(int x, int y) { public int X = x; public int Y = y; } // 1
2.What is a record in C#?
In short: A record is a class, or with record struct a struct, whose generated equality compares values, with a readable ToString and non-destructive copying through with.
Declaring record Pet(string Name, int Age); generates a class with init-only properties for Name and Age, a constructor, a deconstructor, value-based Equals, GetHashCode, == and !=, and a ToString that lists the properties, as below. Two records with equal values are equal even though they are different objects, which suits data transfer objects, messages and other values. with creates a copy with some properties changed, p with { Age = 4 }, and leaves the original untouched, which is how records support immutable updates. record struct, from C# 10, gives the same features to a value type. Records can inherit from other records, and equality then also checks that both objects have the same run-time type.
var a = new Pet("Tom", 3); var b = a with { Age = 4 }; Console.WriteLine(b); Console.WriteLine(a.Age); record Pet(string Name, int Age); // Pet { Name = Tom, Age = 4 } // 3
3.What is an indexer in C#, and how do you write one?
In short: An indexer, declared as this[...] with get and set accessors, lets instances of a class be indexed with square brackets, like an array or a dictionary.
An indexer is a property with parameters: public T this[int i] { get => items[i]; set => items[i] = value; } lets callers write obj[3] and obj[3] = x, exactly as List<T> and Dictionary<TKey,TValue> do. The parameters can be of any type and number, so a grid can take this[int row, int col] and a settings class this[string key], and a class can overload indexers by parameter type, as Week does below with one indexer by position and one by name. Like properties, indexers can be read-only, validate their arguments, and throw ArgumentOutOfRangeException for a bad index. Since C# 8, index and range syntax, w[^1] and w[1..3], also works on types that provide a Length or Count and a suitable indexer or Slice method.
var w = new Week(); Console.WriteLine(w[1]); Console.WriteLine(w["Wed"]); class Week { string[] d = { "Mon", "Tue", "Wed" }; public string this[int i] => d[i]; public int this[string s] => Array.IndexOf(d, s); } // Tue // 2
4.What are static classes and static constructors in C#?
In short: A static class cannot be instantiated or inherited and holds only static members; a static constructor runs once, before the type is first used, to initialise static state.
Marking a class static, as with Math or Console, tells the compiler it is a container for static members only: new on it is a compile error, it cannot be a base class, and every member must be static, which is also where extension methods are declared. A static constructor, static Config() { ... }, takes no parameters and no access modifier, and the runtime calls it automatically exactly once, before the first instance is created or any static member is used, as the trace below shows; your code cannot call it. If it throws, the type stays unusable for the rest of the program, with every later access throwing TypeInitializationException, so static constructors should do as little as possible.
Console.WriteLine("start"); Console.WriteLine(Config.Port); static class Config { public static int Port; static Config() { Console.Write("init "); Port = 8080; } } // start // init 8080
5.What is a primary constructor in C# 12?
In short: Parameters declared after a class or struct name, as in class Account(decimal open), are in scope throughout the type body, replacing a constructor that only copies arguments into fields.
Records have had positional parameters since C# 9, and C# 12 extended the idea to every class and struct: the parameters written after the type name are available to initialisers and member bodies, so a type whose constructor would only copy arguments into fields needs no constructor at all, as below. Unlike a record's parameters, they do not become public properties: they are captured like private fields where they are used, and they are not readonly, so a method can reassign them. Any other constructor must chain to the primary one with this(...), which guarantees the parameters are always set. They suit dependency injection, class Service(ILogger log), where the parameter is simply used.
var acct = new Account(100m); acct.Add(25m); Console.WriteLine(acct.Total); class Account(decimal open) { decimal extra; public void Add(decimal m) => extra += m; public decimal Total => open + extra; } // 125
6.What does an object initializer do in C#, and how is it different from a constructor?
In short: new T { A = 1 } runs a constructor first and then assigns the listed members, so only members that are settable at that point can appear, and required members must.
An object initializer is syntax for calling a constructor, the parameterless one unless you name another, and then assigning each listed member in order, all in one expression. It keeps construction readable for types with many optional settings, and it nests: a collection initializer such as Items = { "pen", "ink" } calls Add on a collection the object already has, as below. It is not a constructor: the assignments run after the constructor has finished, so they can break invariants the constructor set up unless the setters validate, and a member must have an accessible set or init accessor to appear. C# 11's required modifier makes the compiler insist that callers include a member in every initializer.
var o = new Order { Id = 7, Items = { "pen", "ink" } }; var n = o.Items.Count; Console.WriteLine( $"{o.Id}: {n}"); class Order { public int Id { get; set; } public List<string> Items { get; } = new(); } // 7: 2
How the diagnostic asks it
One question from the C# bank, exactly as a sitting would show it. The bank has 5 on classes & structs and 30 across C#.
What does this C# code print?
var a = new Point(1, 2); var b = new Point(1, 2); Console.WriteLine($"{a == b} {ReferenceEquals(a, b)}"); record Point(int X, int Y);
- 1True True
- 2False False
- 3True Falsecorrect
- 4False True
A record is a class whose compiler-generated Equals, GetHashCode, == and != compare the values of its members, so a == b is True: both have X = 1 and Y = 2. They are still two separate objects, created by two calls to new, so ReferenceEquals is False. False False is what a plain class gives, since its == compares references. True True assumes equal values mean one shared object. False True reverses the two. Records also get a readable ToString and with expressions, which copy a record while changing some properties, as in b with { Y = 3 }.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 C# questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.