Собеседование C#: типы данных и приведение типов
Вопросы уровня Middle про value/reference types, boxing, nullable, checked и приведение — с неправильный ответми и разборами.
Содержание
Middle-разработчик знает базу. Именно поэтому вопросы на собеседовании формулируют так, чтобы база давала неверный ответ. Разбираем случаи, где уверенность играет против вас.
Value types и где они живут
Вопрос
int a = 5;
int b = a;
b = 10;
Console.WriteLine(a);
Что выведет? Объясните, почему именно так.
Неправильный ответ: вопрос кажется тривиальным, и кандидат отвечает быстро — «5, потому что int в стеке». Интервьюер уточняет: «а если это поле класса?»
class Container {
public int Value = 5;
}
Правильный ответ: a выведет 5 — value type копируется при присваивании. Но «int всегда в стеке» — неверно. Value type хранится там, где объявлен. Локальная переменная — стек. Поле класса — куча, вместе с объектом. Элемент массива int[] — тоже куча. Стек — лишь один из вариантов.
Вопрос
void Modify(int x) { x = 99; }
void Modify(int[] a) { a[0] = 99; }
int n = 5;
Modify(n);
int[] arr = { 1, 2, 3 };
Modify(arr);
Console.WriteLine(n); // ?
Console.WriteLine(arr[0]); // ?
Неправильный ответ: оба выглядят как «передача аргумента в метод» — кандидат отвечает одинаково.
Правильный ответ: n — 5, arr[0] — 99. int передаётся по значению — метод получает копию. Массив — reference type, передаётся ссылка. Изменение элемента через эту ссылку видно снаружи. Но если бы метод делал a = new int[]{ 0 } — снаружи ничего не изменилось бы: перезаписана была бы локальная копия ссылки.
Вопрос
struct Point { public int X; public int Y; }
Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1;
p2.X = 99;
Console.WriteLine(p1.X); // ?
Неправильный ответ: new ассоциируется с кучей. Кандидат сомневается — вдруг struct ведёт себя как class?
Правильный ответ: 1. struct — value type всегда, new для структур только вызывает конструктор, не аллоцирует в куче. Присваивание копирует все поля. Это поведение принципиально отличается от class и часто удивляет тех, кто пришёл из Java.
Boxing: где прячутся аллокации
Вопрос
int i = 42;
object o = i; // boxing
int j = (int)o; // unboxing
Объясните механику. Почему это «дорого»?
Неправильный ответ: кандидат знает слова, но не детали. Интервьюер спросит: что именно происходит в памяти?
Правильный ответ: boxing — создание нового объекта в куче, копирование значения туда. Unboxing — копирование значения обратно из кучи. Стоимость: аллокация в куче + работа GC. В горячих путях (tight loop, коллекции без generics) — причина деградации. Именно поэтому появились List<T> вместо ArrayList и Span<T> вместо массивов объектов.
Вопрос
int a = 5;
object o1 = a;
object o2 = a;
Console.WriteLine(o1 == o2); // ?
Console.WriteLine(o1.Equals(o2)); // ?
Неправильный ответ: оба содержат 5 — большинство уверенно говорят «оба true».
Правильный ответ: o1 == o2 — false. Это два отдельных объекта в куче (два boxing). == для object сравнивает ссылки. o1.Equals(o2) — true: int.Equals сравнивает значения. Чтобы не попасть в эту ловушку в реальном коде — не сравнивайте упакованные value types через ==.
Вопрос
interface ILogger { void Log(); }
struct ConsoleLogger : ILogger {
public void Log() => Console.WriteLine("log");
}
ILogger logger = new ConsoleLogger(); // boxing?
Неправильный ответ: кандидат думает — «struct на стеке, просто присваивание».
Правильный ответ: да, boxing. Присваивание struct в интерфейсную переменную всегда упаковывает значение в кучу. Это скрытая аллокация — одна из причин, почему struct редко используют для реализации интерфейсов в performance-critical коде. Альтернатива — generics с constraint: void Process<T>(T logger) where T : ILogger — boxing не происходит.
Nullable: тонкости
Вопрос
int? x = null;
int? y = null;
Console.WriteLine(x == y); // ?
Console.WriteLine(x < y); // ?
Console.WriteLine(x >= y); // ?
Неправильный ответ: «null равен null» — кандидат говорит «все true».
Правильный ответ: x == y — true (специальный случай для == и !=). x < y — false. x >= y — false. Все операторы сравнения кроме ==/!= при участии null возвращают false. Нет понятия «null меньше null» — это lifted operators из SQL-семантики трёхзначной логики.
Вопрос
int? a = 5;
object o1 = a; // что в o1?
int? b = null;
object o2 = b;
Console.WriteLine(o2 == null); // ?
Неправильный ответ: кандидат ожидает, что Nullable<int> упакуется как есть.
Правильный ответ: o1 — обычный упакованный int, не Nullable<int>. Boxing nullable снимает обёртку. o2 — буквально null: boxing null-значения nullable даёт null, не объект. o2 == null — true. Следствие: o2.GetType() бросит NullReferenceException.
Вопрос
int? a = null;
int c = (int)a; // ?
int d = a.GetValueOrDefault(); // ?
int e = a ?? -1; // ?
Неправильный ответ: кандидат путает способы «достать» значение из nullable.
Правильный ответ: (int)a при null — InvalidOperationException (не NullReferenceException — частая путаница). a.GetValueOrDefault() — 0 (дефолт для int). a ?? -1 — -1. Для safe-извлечения предпочитайте ?? или GetValueOrDefault(defaultValue).
Переполнение
Вопрос
int max = int.MaxValue;
int result = max + 1;
Console.WriteLine(result); // ?
Неправильный ответ: кандидат говорит «исключение» или «очень большое число».
Правильный ответ: -2147483648 — int.MinValue. По умолчанию в C# арифметика работает в режиме unchecked: переполнение оборачивается по кругу без исключений. Тихая ошибка — хуже, чем исключение.
Вопрос
const int a = int.MaxValue;
const int b = a + 1; // скомпилируется?
Неправильный ответ: кандидат думает — «unchecked везде, значит скомпилируется и обернётся».
Правильный ответ: нет. Константные выражения вычисляются компилятором, и для них checked включён по умолчанию. Получите ошибку компиляции. Если нужно именно такое поведение — unchecked(a + 1) явно.
Вопрос
checked
{
byte b = 200;
b += 100; // ?
}
Неправильный ответ: кандидат уверен, что checked перехватит переполнение — но не все знают, что byte += byte неявно проходит через int.
Правильный ответ: OverflowException. 200 + 100 = 300, что не помещается в byte (max 255). checked это ловит. В unchecked результат был бы 44 (300 - 256).
Приведение типов
Вопрос
object o = "hello";
string s1 = (string)o;
string s2 = o as string;
object o2 = 42;
string s3 = (string)o2;
string s4 = o2 as string;
В чём разница между (T) и as? Что произойдёт в каждом случае?
Неправильный ответ: кандидат знает разницу теоретически, но путается в конкретных случаях.
Правильный ответ: s1 и s2 — "hello". s3 — InvalidCastException в рантайме. s4 — null без исключения. as возвращает null при несовместимости, (T) бросает исключение. as работает только с reference types и nullable value types — 42 as int? валидно, 42 as int — ошибка компиляции.
Вопрос
double d = 9.99;
int i = (int)d;
Console.WriteLine(i); // Что будет выведено в консоль?
Неправильный ответ: кандидат говорит «10».
Правильный ответ: 9. Явный каст double → int усекает дробную часть (truncate), не округляет. Math.Round(d) даст 10.0, (int)Math.Round(d) даст 10. Разница критична в финансовых расчётах.
Вопрос
int i = 16_777_217;
float f = i;
Console.WriteLine(i == (int)f); // ? Что будет выведено в консоль?
Неправильный ответ: кандидат думает, что float точно хранит любой int.
Правильный ответ: false. float — 32-битный с ~7 значащими цифрами. 16_777_217 теряет точность при конвертации в float и становится 16_777_216. Неявное расширяющее приведение int → float не всегда точное. Для точных вычислений — double (15-16 знаков) или decimal (28-29 знаков, но медленнее).
Вопрос с pattern matching
object[] items = { 1, "hello", 3.14, null };
foreach (var item in items)
{
if (item is int n && n > 0)
Console.WriteLine($"positive int: {n}");
else if (item is string { Length: > 3 } s)
Console.WriteLine($"long string: {s}");
}
Что выведет? Что здесь происходит?
Неправильный ответ: Middle часто не знает property pattern ({ Length: > 3 }), появившийся в C# 8.
Правильный ответ:
positive int: 1
long string: hello
is int n && n > 0 — declaration pattern с guard. is string { Length: > 3 } s — property pattern: проверяет тип, свойство и связывает переменную одновременно. 3.14 и null не подходят ни под одно условие.
Строки
Вопрос - что будет выведено в консоль:
string a = "hello";
string b = "hello";
string c = new string(new char[] { 'h','e','l','l','o' });
Console.WriteLine(object.ReferenceEquals(a, b)); // ?
Console.WriteLine(object.ReferenceEquals(a, c)); // ?
Неправильный ответ: кандидат либо говорит «строки всегда новые объекты», либо «строки всегда интернируются».
Правильный ответ: a == b — true: строковые литералы интернируются, компилятор размещает одну копию в пуле. a == c — false: new string(...) создаёт новый объект, минуя интернирование. string.Intern(c) вернул бы ту же ссылку, что a.
Вопрос - можно ли изменить элемент по индексу в примере:
string s = "hello";
s[0] = 'H'; // ?
Неправильный ответ: string — reference type, кандидат думает — можно менять.
Правильный ответ: ошибка компиляции. string иммутабельна — индексатор доступен только для чтения. Любая «модификация» создаёт новый объект: s = s.Replace('h', 'H') — новая строка в куче. Для частых изменений — StringBuilder, который мутирует внутренний буфер без лишних аллокаций.
Шпаргалка
| Вопрос | Типичная ошибка Middle | Правда |
|---|---|---|
| Где хранится value type? | «В стеке» | Там, где объявлен — поле класса живёт в куче |
struct через интерфейс | Без аллокации | Boxing, аллокация в куче |
o1 == o2 для двух boxing одного int | true | false, разные объекты |
int? null → boxing | Объект-обёртка | Результат — буквально null |
(int)9.99 | 10 (округление) | 9 (усечение) |
int.MaxValue + 1 | Исключение | Тихое переполнение, -2147483648 |
const int b = int.MaxValue + 1 | Компилируется | Ошибка компиляции — checked для констант |
int → float всегда точно | Да | Нет, float теряет точность на больших числах |
Следующий шаг: C# на собеседовании: ООП — наследование, полиморфизм, интерфейсы — virtual vs abstract, new vs override и почему интерфейсы в C# 8 стали сложнее.