7 питань з .NET-тестів минулих років: the good, the bad, the ugly

Кожного року перед командою Binary Studio постає неймовірно важке завдання - зрозуміти, хто з 3000+ кандидатів гідний пройти далі, до другого етапу відбору. За допомогою тесту ми відбираємо найбільш талановитих студентів, що матимуть змогу пройти весь курс Академії, тому нам так важливо зробити питання якісними.

По-перше, нам потрібно максимально якісно перевірити знання студента. Для цього необхідно, щоб питання покривало певну тему (наприклад, перевантаження методів чи оператор Елвіса) і дозволяло визначити, чи людина справді розуміє, чому потрібно вибрати саме той чи інший варіант. Питання не повинне бути занадто легким, тому що всі зможуть дати відповідь. При цьому воно не повинно бути й занадто важким, бо якщо на нього ніхто не відповість, вийде, що воно є даремним.

По-друге, нам потрібно боротися з тими, хто намагається схитрувати у той чи інший спосіб. Щоб цього уникнути, ми намагаємося придумати питання таким чином, щоб пошук в інтернеті не дав явної відповіді, а код не можна було набрати в IDE та отримати правильний результат.

У зв’язку з цим ми щороку намагаємося переглянути питання: прибрати ті, що, на нашу думку, не виправдали себе, додати нові, що враховують зміни у технологіях та практиках. Цього року ми проаналізували тестові питання з минулих відборів до Академії, щоб побачити кореляцію між результатами тестів та успішністю студентів. Саме це й допомогло нам визначити питання, що не відповідають нашим критеріям, які я описав вище.

У цій статті я пропоную розглянути 7 питань, що допоможуть зрозуміти, чого слід очікувати від етапу тестування. Хоч деякі з них вже не будуть використовуватися, вони аналогічні тим, що залишаться, і підкажуть, на які моменти варто звернути увагу при підготовці до тестів.

Але для початку спробуйте пройти цей тест самостійно, без підказок ось за цим посиланням:

Пройти тест

№1. Оберіть правильні твердження:

Коли IDisposable.Dispose() викликається автоматично?

Варіанти відповідей:

  1. В using statement
  2. При роботі Garbage Collector під час очищення будь-якого об’єкта, якщо той реалізує IDisposable
  3. В циклі foreach, якщо ітератор реалізує IDisposable

94% студентів відповіли неправильно.

Ті, хто готувався до тесту, ймовірно, читали книжки або статті, де описуються основи C#. Тому смію припустити, що багато людей обрали перший варіант. І вони були праві, але лиш частково. Так, using statement справді використовується для класів, що потребують будь-якого очищення ресурсів, закриття з'єднань і т.д., але для того, щоб повністю відповісти на це питання, вам знадобляться більш глибокі знання C#.

Якщо ми подивимось на механізм роботи циклу foreach, то побачимо що “під капотом” він буде розгорнутий у звичайний цикл while, який буде виконуватись, використовуючи метод ітератора MoveNext(). Також, якщо ітератор реалізує IDisposable, такий цикл буде огорнутий в конструкцію try-finally, де у finally буде викликаний Dispose(). Що приводить нас до того, що третій варіант також правильний. Детальніше про ітератори можна подивитись на MSDN.

Що ж стосується другого варіанта відповіді, то Garbage Collector не знає, як поводитися з unmanaged ресурсами (трішки детальніше тут).

Виходячи зі статистики правильних відповідей, ми зробили висновок, що дане питання потребує занадто спеціалізованих знань, а тому немає сенсу його задавати студентам на першому етапі.

№2. Як називається секція коду, яка йде після блоків try-catch?

Варіанти відповідей:

  1. finalize
  2. finally
  3. final
  4. finish
  5. dispose

9% студентів відповіли неправильно.

Це питання є повною протилежністю першому. Подивившись на результати, ми зрозуміли, що воно є занадто легким та фактично даремним. Ключове слово finally, що використовується для очищення ресурсів блоку try, описане в перших розділах туторіалів та книжок по C#, тому ті, хто починав готуватися до тесту, повинні бути ознайомлені з цим. На щастя, це питання буде замінено більш релевантним, що допоможе вам краще проявити свої знання.

№3. Як оптимізувати наступний код?

var str = "this is " + "a first " + "string in " + "the application. " 
+ "It should " + "be rewritten!";

Варіанти відповідей:

  1. Використати конструктор ініціалізації типу string
  2. Використати клас StringWriter для створення стрічки
  3. Використати клас StringBuilder для створення стрічки
  4. Використати масив символів для створення стрічки
  5. Записати всі операції в один рядок.

33% студентів відповіли неправильно.

Для того, щоб відповісти на це питання, необхідно трішки “копнути” в механізм роботи стрічок. String - це тип за посиланням, що є імутабельним. Імутабельність означає, що ви не можете змінювати значення після того, як стрічка була створена. В даному прикладі, насправді, буде створена не одна стрічка, а шість(!). Спочатку створюється стрічка “this is ”, потім при конкатенації створюється друга стрічка “this is a first ”, і так далі. Для того, щоб оптимізувати цей код, нам потрібно позбутися створення зайвих стрічок.

  1. Використання конструктора ініціалізації типу string нам нічого не дасть, оскільки він і так викликається неявно.
  2. Клас StringWriter використовується для запису стрічок у файл
  3. Використання масиву символів нам також нічого не дасть, бо стрічка і без того є масивом символів.
  4. Кількість переносів ніяк не впливає на перфоманс, тому що при компіляції вони будуть видалені компілятором.

Для оптимізації подібних випадків нам потрібен клас StringBuilder. Він дозволить нам створити об’єкт типу StringBuilder, виконати Append() стільки разів, скільки нам необхідно, а потім виконати ToString(), що і дасть нам кінцевий результат. Детальніше з імутабельністю стрічок та класом StringBuilder можна ознайомитись тут.

Це приклад гарного питання, тому що воно справді перевіряє знання стрічок (ці знання згодом будут справді корисні при роботі над проектом), але водночас воно не потребує занадто глибоких знань.

№4 Що необхідно написати в тілі методу CastToCar для того, щоб він безпечно відпрацював (не викинув exception)?

public static Car CastToCar(object obj)        
{            
	// Code here        
}        
static void Main(string[] args)        
{            
	object obj = "Hello World!";            
	var result = CastToCar(obj);        
}

Варіанти відповідей:

  1. return obj;
  2. return (Car)obj;
  3. return obj(Car);
  4. return obj as Car;
  5. return Car as obj;

53% студентів відповіли неправильно.

Для того, щоб відповісти на це питання, вам потрібно мати знання з приведення типів. Давайте розберемо кожний з варіантів:

  1. return obj; не скомпілюється, тому що необхідне явне приведення типів.
  2. return (Car)obj; скомпілюється, але код не можна вважати безпечним, бо у нас немає інформації про те, що міститься у класі Car. У випадку, якщо клас Car не є обгорткою над класом String, буде викинутий exception.
  3. return obj(Car); не скомпілюється, тому що порушено синтаксис мови.
  4. return obj as Car; є правильною відповіддю. Використання ключового слова as дозволяє нам спробувати привести obj до типу Car, а в разі невдачі замість exception буде повернений null. Детальніше про ключове слово as можна почитати на MSDN.
  5. return Car as obj; не скомпілюється, тому що порушено синтаксис мови.

Це питання дозволяє перевірити і синтаксис мови, і базові знання з приведення типів. Схожі питання можуть зустрічатися в майбутньому тесті.

№5. Чому буде дорівнювати результат виконання наступного коду?

class A
{
	private int i = initA();        
	public static int initA()
	{                
		return 2;        
	}        
	public A()
	{                
		Print();        
	}        
	public virtual void Print()
	{                
		Console.WriteLine(i);        
	}
}
class B: A
{        
	private int i = initB();        
	public static int initB()
	{                
		return 8;        
	}        
	public override void Print()
	{                
		Console.WriteLine(i);        
		}
}
class Program
{        
	static void Main(string[] args)
	{                
		A a = new B();                
		a.Print();                
		Console.ReadLine();        
	}
}

Варіанти відповідей:

  1. 8 8
  2. 2 8
  3. 8 2
  4. 8
  5. 2

89% студентів відповіли неправильно.

Для правильної відповіді на це питання необхідно мати знання про механізм роботи virtual / override методів та наслідування. Ось тут описано, що при виконанні virtual методу CLR перевіряє наявність override методу та виконує його у разі, якщо він є. В іншому випадку - виконує базовий virtual метод. Кожен клас має неявний конструктор без параметрів. Клас-нащадок неявно викликає parameterless конструктор батьківського класу.

Відповідно, необхідно звернути увагу на те, що змінній a з типом A присвоюється об’єкт типу B. Класс A містить конструктор, в якому відбувається виклик методу Print(). Коли ми виконуємо new B(), відбувається неявний виклик конструктора без параметрів класу B, який у свою чергу викликає конструктор без параметрів класу A. У конструкторі класу A відбувається виклик методу Print(). При виконанні CLR бачить, що у virtual методу Print() є override версія цього методу в класі B (пам’ятаєте, ми присвоювали саме new B()), що й виведе нам 8. Потім виконується a.Print() за тим самим принципом, і ми знову отримуємо 8. Виходить, що перший варіант є правильною відповіддю.

Це питання погане і для студентів, і для нас, команди Binary Studio. По-перше, воно є достатньо заплутаним та потребує уваги до кожної дрібниці. По-друге, це питання, відповідь на яке можна легко отримати, виконавши його в IDE. Тому ми його більше не використовуємо.

№6. Оберіть правильне твердження

public delegate R D<out R>();

Варіанти відповідей:

  1. Делегат є коваріантним.
  2. Делегат є контраваріантним.
  3. Код не скомпілюється.

60% студентів відповіли неправильно.

  • Ключове слово out використовується для того, щоб дозволити методам делегату повертати похідні типи. Відповідно, якщо type parameter використовується тільки для типу вихідного значення та не використовується для параметрів методів, це називається коваріантністю.
  • Ключове слово in використовується для того, щоб дозволити методам делегату приймати типи, похідні від type parameter. Якщо type parameter використовується тільки для параметрів методів та не використовується для типу вихідного значення, це називається контраваріантністю.

У нашому випадку використовується коваріантність, оскільки вказано ключове слово out. Це означає, що тип результату виконання такого делегату може бути похідним. Наприклад, у наступній частині коду ми можемо присвоїти похідний клас Mercedes базовому типу Car.

public delegate R D<out R>();

public static void Main()
{
	D<Car> car = Car.GetCar;
	D<Mercedes> mercedes = Mercedes.GetMercedes;
	
	car = mercedes;
	
	car();
}

public class Car
{
	public static Car GetCar()
	{
		return new Car();
	}
}

public class Mercedes : Car
{
	public static Mercedes GetMercedes()
	{
		return new Mercedes();
	}
}

Почитати про generic делегати можна стисло у книжці C# Pocket Reference або детальніше у цій та цій статтях.

№7. Яка з наступних структур даних найбільш підходить для відображення кількості студентів по групах?

Варіанти відповідей:

  1. Dictionary<string, int>
  2. List<string>
  3. Enum
  4. Dictionary<int, string>
  5. String[]

39% студентів відповіли неправильно.

Для початку уявімо, як виглядає такий набір даних:
Група А - 20 студентів
Група Б - 25 студентів
Група В - 15 студентів

Як бачимо, в нас є два параметри. Перший - назва групи, другий - кількість студентів. Погодьтеся, що оскільки студенти розподілені по групах, то назва групи тут буде основним значенням, тобто ключем. Кількість студентів у групі буде значенням ключа.

Давайте розглянемо кожен варіант з кінця:

  • 5. Чому нам не підходить string[]? Тому що в нас двовимірний набір даних, відповідно, одновимірний масив - не те, що нам потрібно.
  • 4. Dictionary<⁠int, string> вже є двомірною структурою. Але вище ми вказали, що основним значенням у нас є назва групи. А ця структура нам пропонує зробити ключем кількість студентів, що є не найкращим варіантом.
  • 3.  Enum використовується для статичних даних / констант (наприклад, Color зі значеннями Red, Green, Blue), відповідно, Enum погано розширюваний та не підходить для нашого завдання.
  • 2. List<⁠string>, знову ж таки, є одновимірною структурою, тому ми не можемо його використовувати для наших цілей.
  • 1. Dictionary<⁠string, int> є найбільш відповідним варіантом, тому що на відміну від Dictionary<⁠int, string>, у цьому варіанті ключем є назва групи, а значенням - кількість студентів у ній.

Вміння застосувати потрібну структуру для будь-якої задачі зекономить вам нерви в подальшій роботі, тому що при неправильному виборі це може спричинити багато незручностей, таких як складності при обробці даних та ускладнені конструкції.

Підсумки

Декілька років тому я був практично на вашому місці. Я дізнався про Академію та старанно готувався до тесту, поглинаючи тонни інформації. Я намагався знайти будь-які коментарі в соц. мережах, які б дали мені підказку про те, що мене чекає, до чого потрібно бути готовим. В той час я, будучи студентом, віддав би тарілку грєчки багато за можливість отримати хоч якусь додаткову інформацію.

Сподіваюсь, що ця стаття прибере хвилювання і страх перед невідомим та допоможе з більшою впевненістю натиснути на кнопку “Пройти тест”.

Бажаю вам успіху, зустрінемось у другому етапі!