· 14 мин 👁 1.4k Средний

Собеседование C#: типы данных и приведение типов

Вопросы уровня Middle про value/reference types, boxing, nullable, checked и приведение — с неправильный ответми и разборами.

csharpсобеседованиетипыboxingnullable
Содержание

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]); // ?

Неправильный ответ: оба выглядят как «передача аргумента в метод» — кандидат отвечает одинаково.

Правильный ответ: n5, 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 == o2false. Это два отдельных объекта в куче (два 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 == ytrue (специальный случай для == и !=). x < yfalse. x >= yfalse. Все операторы сравнения кроме ==/!= при участии 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 == nulltrue. Следствие: o2.GetType() бросит NullReferenceException.


Вопрос

int? a = null;
int c = (int)a;      // ?
int d = a.GetValueOrDefault(); // ?
int e = a ?? -1;     // ?

Неправильный ответ: кандидат путает способы «достать» значение из nullable.

Правильный ответ: (int)a при nullInvalidOperationException (не NullReferenceException — частая путаница). a.GetValueOrDefault()0 (дефолт для int). a ?? -1-1. Для safe-извлечения предпочитайте ?? или GetValueOrDefault(defaultValue).


Переполнение

Вопрос

int max = int.MaxValue;
int result = max + 1;
Console.WriteLine(result); // ?

Неправильный ответ: кандидат говорит «исключение» или «очень большое число».

Правильный ответ: -2147483648int.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". s3InvalidCastException в рантайме. s4null без исключения. 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 == btrue: строковые литералы интернируются, компилятор размещает одну копию в пуле. a == cfalse: 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 одного inttruefalse, разные объекты
int? null → boxingОбъект-обёрткаРезультат — буквально null
(int)9.9910 (округление)9 (усечение)
int.MaxValue + 1ИсключениеТихое переполнение, -2147483648
const int b = int.MaxValue + 1КомпилируетсяОшибка компиляции — checked для констант
int → float всегда точноДаНет, float теряет точность на больших числах

Следующий шаг: C# на собеседовании: ООП — наследование, полиморфизм, интерфейсы — virtual vs abstract, new vs override и почему интерфейсы в C# 8 стали сложнее.