C# data types interview questions, with answers
C# questions about types test the model underneath the rest of the language: whether a variable holds a value or a reference, what the compiler knows about a type and when, and what happens at the edges, when an int overflows, a value is boxed, or a reference might be null. Most are asked as a few lines and 'what does this print?', so every answer below comes with code that was compiled and run on .NET 10.
The questions start with the two kinds of type, move through type inference and nullability, and end with arithmetic and the type to use for money. 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 value types and reference types in C#?
In short: A value-type variable holds the data itself and is copied on assignment; a reference-type variable holds a reference to an object, so assignment shares the object.
Value types are the simple types, such as int, double, bool and char, plus enums and structs: a variable of a value type contains the value, so assigning it copies the value and each copy changes independently. Reference types are classes, arrays, strings, delegates and interfaces: a variable holds a reference, and assignment copies the reference, so two variables can refer to one object, as the array below shows. Where they are stored is secondary: a local of value type usually lives on the stack, but a value-type field lives inside its containing object on the heap. The distinction also decides defaults: a value type always has a value, such as 0 or false, while a reference can be null.
int a = 1; int b = a; b = 5; int[] x = { 1 }; int[] y = x; y[0] = 5; var s = $"{a} {x[0]}"; Console.WriteLine(s); // 1 5
2.What does var mean in C#, and how is it different from dynamic?
In short: var asks the compiler to infer a variable's static type from its initialiser, fixed for good; dynamic switches off compile-time checking, so members are resolved at run time.
var x = 5; declares x exactly as if it were written int x = 5;: the type is inferred once, at compile time, and assigning a string later is a compile error. var is only allowed for locals with an initialiser, and it is required for anonymous types. dynamic is a real type that makes the compiler let any member access or operation through, and the runtime binder resolves it when the code runs, so mistakes var would catch at compile time become exceptions, as the misspelt method below does with a RuntimeBinderException. dynamic exists for COM interop, dynamic languages and some reflection-heavy code; in everyday code it trades away the type checking that makes C# safe to refactor.
dynamic d = "text"; Console.WriteLine(d.Length); try { d.Lenght(); } catch (Exception e) { Console.WriteLine( e.GetType().Name); } // 4 // RuntimeBinderException
3.What are nullable reference types in C#?
In short: Since C# 8, in a nullable context string means 'should never be null' and string? means 'may be null', and the compiler warns when a possibly-null reference is dereferenced.
Every reference could always hold null, which made NullReferenceException the most common bug in C#. Nullable reference types add annotations and flow analysis without changing the runtime: in a nullable context, which new projects enable by default, a plain string declares that the variable should never be null and string? that it may be. The compiler then tracks each variable through the code and warns when a string? is dereferenced before a null check, or when null is assigned to a non-nullable reference; after the check below, it knows name is not null. These are warnings, not errors, unless a project treats them as errors, and the null-forgiving operator, ! after an expression, silences one. Kotlin's String and String? are the same idea, built in from the start.
string? name = Find('b'); if (name is null) Console.WriteLine("none"); else Console.WriteLine( name.Length); static string? Find(char k) => k == 'a' ? "alpha" : null; // none
4.What happens when an int overflows in C#?
In short: By default integer arithmetic is unchecked and wraps silently; in a checked context, or with overflow checking enabled for the project, it throws OverflowException.
C# evaluates non-constant integer arithmetic and conversions in an unchecked context by default, so a value that no longer fits keeps only its low bits: casting 3,000,000,000 held in a long to int gives -1294967296, as below. Wrapping an expression in checked(...), or a block in checked { }, makes overflow throw System.OverflowException instead, and the CheckForOverflowUnderflow project setting makes checked the default everywhere. Constant expressions are different: int.MaxValue + 1 written directly in the source is a compile error, because the compiler evaluates it. Use checked arithmetic wherever a silently wrong number would be worse than an exception, such as totals and money.
long big = 3_000_000_000; int wrapped = unchecked( (int)big); Console.WriteLine(wrapped); int safe = checked((int)big); // -1294967296 // throws OverflowException
5.What are boxing and unboxing in C#, and why do they cost performance?
In short: Boxing copies a value type into a new heap object so it can be treated as object or an interface; unboxing checks the type and copies the value back out.
When a value type is converted to object, or to an interface it implements, the runtime allocates an object on the heap and copies the value into it: that is boxing. Unboxing, an explicit cast back to the exact value type, checks the type and copies the value out, and it throws InvalidCastException if the type does not match exactly, even when the value types themselves would convert, as the cast to long below shows. The costs are an allocation, extra work for the garbage collector and a copy, which is why the non-generic ArrayList and Hashtable, which store everything as object, gave way to List<T> and Dictionary<TKey,TValue>, which store values directly. Boxing also happens quietly when a struct is used through an interface it implements.
int n = 64; object o = n; int back = (int)o; Console.WriteLine(back); long l = (long)o; // 64 // throws InvalidCastException
6.What is the difference between float, double and decimal in C#, and which should you use for money?
In short: float and double are binary floating point, fast but unable to store most decimal fractions exactly; decimal is base ten and exact for values like 0.1, so money uses decimal.
float, 32 bits, and double, 64 bits, follow IEEE 754 binary floating point, so a value such as 0.1 is stored as the nearest binary fraction, and sums drift: 0.1 + 0.2 is not exactly 0.3 as a double, as below. decimal stores a 96-bit integer and a power-of-ten scale, so decimal fractions of up to 28 or 29 significant digits are exact, and 0.1m + 0.2m is exactly 0.3m. The price is speed, since decimal arithmetic is done in software, and range, since decimal tops out near 7.9 × 10^28. decimal also has no NaN or infinity, so dividing a decimal by zero throws DivideByZeroException. Use double for measurements and science, decimal for money, and write decimal literals with the m suffix.
double d = 0.1 + 0.2; decimal m = 0.1m + 0.2m; Console.WriteLine(d == 0.3); Console.WriteLine(m == 0.3m); // False // True
7.What default values do C# variables get?
In short: Fields and array elements start at their type's default, 0, false or null; local variables get no default at all and must be assigned before they are read.
Every type has a default value, which default(T) or the default literal produces: zero for numeric types, false for bool, '\0' for char, and null for reference and nullable types; a struct's default has every field at its own default. The fields of a class and the elements of a new array are set to those defaults automatically, which is why new int[3] holds three zeros. Local variables are different: C# requires definite assignment, so reading a local before every path has assigned it is a compile error, CS0165, not the garbage value C would give. The same rule covers out parameters, which a method must assign before it returns, and struct locals, whose fields must all be assigned before use.
var arr = new int[3]; Console.WriteLine(arr[1]); bool flag = default; Console.WriteLine(flag); int n; Console.WriteLine(n); // does not compile // 0 // False
How the diagnostic asks it
One question from the C# bank, exactly as a sitting would show it. The bank has 5 on types & operators and 30 across C#.
What does this C# code print?
string? s = null; int? len = s?.Length; Console.Write(len ?? -1); s ??= "abc"; Console.Write(" " + s?.Length);
- 1-1 3correct
- 20 3
- 3It throws a NullReferenceException
- 4-1 -1
s?.Length checks s first: s is null, so the whole expression is null instead of throwing, and len, an int?, is null. len ?? -1 gives the right-hand value when the left is null, so -1 is printed. s ??= "abc" assigns only when s is null, which it is, so s becomes "abc" and s?.Length is 3: the output is -1 3. 0 3 assumes a null string has length 0, but null is not an empty string, and s.Length without the ? would throw. The NullReferenceException option ignores that ?. exists to prevent exactly that. -1 -1 assumes ??= never assigns.
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.