← Academy Blog

Марафон підготовки 2020

Стартував марафон підготовки до тестів - .NET трек - Binary Studio Academy 2020! Щотижневі питання з відповідями та пояснення чекають на нашому блозі.

  • [.NET] Коли присвоюється тип даних змінній, що оголошена за допомогою var?

    a) Runtime
    b) Interpret time
    c) Compile time
    d) Dynamic linking time
    e) Application Initialization time


    Розгорнути правильну відповідь з поясненням

    Відразу ж можемо прибрати зі списку можливих відповідей варіант b), оскільки C# є компільованою мовою, а отже у нього немає етапу інтерпретації.

    Також варто згадати, що ключове слово var не означає, що ми можемо змінювати тип змінної в процесі виконання програми (хоч це і співзвучно з "variant"). Натомість, ключове слово var використовується для створення змінної, не визначаючи її тип явно.

    Не варто плутати var і dynamic. Об'єкт з типом dynamic може мати будь-яке значення, обходить статичну перевірку типів і вважається, що він підтримує будь-які операції на етапі компіляції. Конкретний тип змінної присвоюється в runtime, в процесі виконання програми.

    Dynamic Linking - це процес зв'язування збірок (Dynamic Link Libraries), за допомогою якого різні процеси можуть використовувати одну і ту ж функціональність, не створюючи їх копії. Процес не стосується типів всередині таких збірок. Детальніше про DLL можна дізнатися тут.

    Application initialization - це процес запуску програми. До цього моменту всі статичні типи вже визначені, а середовище виконання буде працювати тільки з динамічними типами і рефлексією.

    Отож, правильним варіантом є c).

    П.С.: в офіційній документації Microsoft в розділі var у другому реченні вказано, що компілятор визначає і присвоює тип, який найбільше підходить :)

  • [.NET] Яке твердження щодо структур є правильним?

    a) В структурах не можна створювати властивості, лише поля.
    b) Структура може мати явний конструктор без параметрів.
    c) Структура може бути створена без ключового слова new.
    d) Структура може успадковуватись від інших структур.
    e) Структура може реалізовувати лише один інтерфейс.


    Розгорнути правильну відповідь з поясненням

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

    Структура - це тип даних, що може містити дані (поля, властивості) і логіку (методи). Але на відміну від класу, який є типом за посиланням і зберігається в купі, структура є типом за значенням, тому зберігається в стеку. Окрім цього, у неї є ще деякі особливості.

    Наприклад, ми не можемо явно оголосити конструктор без параметрів. Але у структури вже є неявний конструктор, який ініціалізує всі члени класу значеннями за замовчуванням.

    Також структура не може успадковувати інші структури і класи, проте може реалізовувати одночасно декілька інтерфейсів. Сама ж структура за замовчуванням є sealed, що означає, що ми не можемо її успадковувати.

    Тому правильним варіантом є c). Структуру дійсно можна створити, не використовуючи ключове слово new, але важливо пам'ятати, що всі члени класу повинні бути доступними і проініціалізованими перед першим використанням екземпляру структури.

    Якщо ж ви хочете дізнатися докладніше про інші відмінності структур від класів, радимо подивитися про це в офіційній документації Microsoft

  • [.NET] Який рядок потрібно розкоментувати для того, щоб отримати Hello World?
    public abstract class ParentClass {
        public virtual void Hello() {
            Console.Write("Hello");
        }
    }
    
    public class ChildClass: ParentClass {
        public override void Hello() {
            //base.Hello(); // 1
            //this.Hello(); // 2
            Console.WriteLine(" World");
        }
    }
    
    static void Main(string[] args) {
        ParentClass item = new ChildClass();
        item.Hello();
        Console.ReadLine();
    }

    a) 1
    b) 2
    c) Hello World і так виводиться
    d) Неможливо отримати Hello World


    Розгорнути правильну відповідь з поясненням

    Для того, щоб дати правильну відповідь, необхідно знати, як працює успадкування в C#, в чому різниця між base і this, а також яку функцію виконує конструкція virtual/override. Давайте розберемо по порядку.

    Успадкування - це один із принципів ООП, згідно з яким клас може "переймати" члени іншого, базового класу.

    Наприклад:

    public class ITCompany
    {
      public void CreateSoftware ()
      {
        Console.WriteLine ( "Software created");
      }
    }
    
    public class BinaryStudio : ITCompany
    {
    }

    Тут клас BinaryStudio успадковує функціональність класу ITCompany, у якого є метод CreateSoftware. Отож, тепер ми можемо викликати метод CreateSoftware і у класу BinaryStudio.

    public static void Main ()
    {
      var binaryStudio = new BinaryStudio ();
      binaryStudio.CreateSoftware ();
    }

    В консоль виведеться "Software created".

    Ключове слово base використовується для доступу до членів базового класу, а this- для доступу до членів поточного класу.

    Вже на цьому етапі ми можемо відсіяти варіант b) 2, оскільки після аналізу коду можна зрозуміти, що насправді метод Hello класу ChildClass викликає сам себе, створюючи рекурсію, що призведе до виключення.

    І тут ми вже близькі до розв'язки нашого питання. Механізм virtual / override.

    Ми можемо позначити метод в базовому класі ключовим словом virtual, для того, щоб дозволити класам-спадкоємцям перевизначати його логіку. При цьому сам метод, що перевизначає логіку, ми позначаємо ключовим словом override.

    Також варто звернути увагу, що змінна, в якої ми викликаємо метод Hello, має тип базового класу ParentClass, незважаючи на те, що ми присвоїли об'єкт типу ChildClass.

    Тепер давайте розберемо на прикладі коду з питання, чим відрізнятимуться результати з використанням virtual / override і без.

    У разі, якщо ми не використовуємо перевизначення логіки, буде викликаний метод Hello у класу ParentClass, оскільки наша змінна має саме цей тип (якби тип змінної був ChildClass, то тоді викликався б метод цього класу).

    Якщо ж ми перевизначили логіку, то CLR буде шукати нову реалізацію в класах-спадкоємцях. Тобто, хоч тип змінної це ParentClass, його метод Hello позначений ключовим словом virtual. Тому при виконанні програми буде викликаний метод класу ChildClass. Після цього можемо розкоментувати рядок 1 і за допомогою base викликати метод базового класу. У такий спосіб отримаємо рядок "Hello World".

    Отже, правильна відповідь - варіант a) 1.

  • [.NET] (C# 7.0) Оберіть правильний варіант перевірки типу.

    a)

    var score = 10;
    
    if (score as int d) Write(d);

    b)

    var score = 10;
    
    if (score is int d) Write(d);

    c)

    var score = 10;
    
    if (score is d) Write(d);

    d) жоден з наведених варіантів не є правильним.


    Розгорнути правильну відповідь з поясненням

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

    • as - використовується для явного приведення в посилальний тип або nullable тип значень. Якщо ж приведення неможливе, повертається null.

    Наприклад:

    object x = "Binary Studio Academy";
    var res = x as string;

    Приведення пройде успішно, і res буде дорівнювати рядку "Binary Studio Academy".

    object x = 120;
    var res = x as string;

    Однак, якщо ми замінимо рядок на число, то res буде дорівнювати null через неможливість явного приведення числа в рядок.

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

    • is - використовується для перевірки відповідності типів.

    Наприклад:

    var a = new Car ();
    var b = new Person ();
    Console.WriteLine (a is Car);
    Console.WriteLine (b is Car);

    Починаючи з C# 7.0, завдяки pattern matching ми можемо оголошувати нову змінну для результату прямо в виразі (book is Document doc).

    Беручи до уваги все вищезгадане, можна зауважити, що

    • варіант a) нам не підходить, оскільки нам потрібно перевірити тип, а не привести його;
    • варіант c) неправильний, оскільки синтаксис потребує наявність типу з яким порівнюємо, а не змінну.

    Тому правильним варіантом є варіант b).

  • [.NET] (C # 7.0) Який з наведених варіантів виклику функції TryParse спричинить помилку компіляції?

    a) int.TryParse("10", out var _);
    b) int.TryParse("10", out _);
    c) int.TryParse("10", out count);
    d) int.TryParse("10", out int count);

    Розгорнути правильну відповідь з поясненням

    Для початку, давайте розглянемо як можна передавати аргументи в методи.

    1. За значенням. Значення копіюється, а отже, якщо ми виконуємо якісь дії над змінною всередині методу, то сам аргумент зовні залишається незмінним.

    Наприклад:

    var count = 10;
    Calculate(count);
    Console.WriteLine(count);
    
    public static void Calculate(int count)
    {
      count++;
    }

    Що б ми не зробили всередині методу Calculate, count все одно буде дорівнювати 10, оскільки всередині програма буде працювати з копією цієї змінної.

    1. За посиланням за допомогою ключового слова ref. Замість копіювання значення, ми передаємо посилання, тобто всі дії всередині методу будуть відображатися на значенні аргументу, який ми передали.

    Наприклад:

    var count = 10;
    Calculate(ref count);
    Console.WriteLine(count);
    
    public static void Calculate(ref int count)
    {
      count++;
    }

    У цьому випадку метод Calculate буде працювати з посиланням на нашу змінну count, а не з копією значення, тому результат буде дорівнювати 11.

    1. За посиланням за допомогою ключового слова out. Все так само, як і з ref, за винятком того, що для ref потрібно, щоб змінна була проініціалізована до передачі у метод, а у випадку з out - ми ініціалізуємо її всередині. Також, починаючи з C # 7.0 ми можемо користуватися перевагами pattern matching і оголошувати out змінні прямо у списку аргументів під час виклику методу (наприклад, int.TryParse("10", out int count)). До речі у нас є класна стаття яка описує Pattern Matching вздовж і впоперек.

    C# 7.0 також додав можливість використання так званих "порожніх" змінних. Навіщо вони потрібні? У комерційній розробці часто буває ситуація коли доводиться працювати з чужим кодом, але при цьому не можна його змінювати через зворотню сумісність. І буває так, що потрібно щось передати в якості аргументу, однак нам не потрібен результат. У цьому випадку ми можемо використовувати Discards.

    Якщо зібрати всі ці знання воєдино, вийде, що варіанти відповідей a) і b) не спричинять помилку компіляції, оскільки ми дійсно можемо використовувати символ _, якщо нам не потрібен результат. Також в цьому випадку компілятор дозволяє нам не ставити тип змінної. А ось варіант c) якраз викличе помилку, тому що ми не вказали тип змінної, яку створюємо.

  • [.NET] Скільки запитів до бази даних буде виконано в результаті виконання наступного коду?
    class Program
        {
            private static readonly StudentsContext database = new StudentsContext();
    
            private static IQueryable<Student> GetStudents() => database.Students;
    
            private static IQueryable<Subject> GetSubjects() =>
                from subject in database.Subjects where subject.Name select subject;
    
            static void Main(string[] args)
            {
                var count = from s in GetStudents()
                    join sbj in GetSubjects() on s.SubjectId equals sbj.SubjectId
                    group s.Name by sbj
                    into g
                    select new
                    {
                        Count = g.Sum(s => s)
                    }
                    .ToList();
            }
        }

    a) Жодного
    b) Один
    c) Два
    d) Три

    Розгорнути правильну відповідь з поясненням

    Запити в LINQ діляться на два основні типи: ті, які виконуються негайно (Immediate), і ті, що виконуються в ході програми (Deferred / Lazy).

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

    Для того, щоб виконати запит який повертає одиничне значення, нам необхідно застосувати один з aggregate (Sum(), Count(), etc) або element (First(), Last()) операторів.

    Для того, щоб виконати запит який повертає колекцію, ми можемо викликати ToList(), ToDictionary() або ToArray().

    Докладніше про Immediate / Deferred Query Execution можна знайти тут, а повний список операторів - тут.

    У нашому випадку запит буде виконаний при виклику методу ToList(), так як всі інші методи (крім Sum) належать до Deferred Execution. Не дивлячись на те, що метод Sum() відноситься до Immediate Execution операторів, запит не буде виконаний одразу, так як після використання методу GroupBy() ми маємо доступ до SQL функцій агрегацій і Sum буде виконана на стороні SQL сервера. Тому правильною відповіддю є варіант b) Один.

  • [.NET] Який результат виконання поданого методу?
    public string GetString()
    {
      var a = "Binary Academy";
      var b = a;
      a.Replace("Binary", "Music");
    
      return b;
    }

    a) Exception
    b) "Music Academy"
    c) "Binary Academy"
    d) ""

    Розгорнути правильну відповідь з поясненням

    Щоб відповісти на це питання, необхідно знати, що таке імутабельність. Імутабельність — це незмінність об’єкта. Інакше кажучи, ви не можете змінювати об’єкт після того, як його було створено. Натомість повертається новий об’єкт із новими значеннями.

    Стрічки в .NET незмінні, тому при виконанні операцій над ними методи повертають нову стрічку. У нашому випадку метод Replace поверне нову стрічку “Music Academy”, але результат нікуди не буде присвоєно, а сама змінна а залишиться неторканою через імутабельність. Однак, навіть якби ми присвоїли результат назад в а, створився б новий рядок (а отже, нове посилання), але b при цьому не зміниться, оскільки буде так само зберігати лінк на початковий об’єкт.

    Тому правильна відповідь — варіант c. “Binary Academy”