← Academy Blog

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

Стартував марафон підготовки до тестування 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 у другому реченні вказано, що компілятор визначає і присвоює тип, який найбільше підходить :)

  • [Java] Яким буде результат виконання даного коду?
    package com.learning;
    
    public class Main {
      public static void main(String[] args) {
        try {
          // business logic
        }
        catch (ArithmeticException e) {
          // error handler logic
        }
        catch (Exception e) {
          // error handler logic
        }
        catch (RuntimeException e) {
          // error handler logic
        }
      }
    }

    a) Код скомпілюється без помилок b) Помилка компіляції через те, що класу ArithmeticException не існує c) Помилка компіляції через те, що клас Exception перехоплюється раніше, ніж RuntimeException d) Помилка компіляції через те, що блок try {} не містить коду

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

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

    Спочатку варто згадати, що стандартний клас-виключення ArithmeticException існує :)

    Для того, щоб правильно відповісти, необхідно знати базову ієрархію класів винятків і нюанс використання цієї ієрархії в послідовності блоків "catch". У нашому випадку ієрархія виглядає таким чином (...-> означає успадковується від):

    ArithmeticException -> RuntimeException -> Exception

    Сама ж помилка компіляції все-таки буде, оскільки ієрархія блоків "catch" в коді вибудувана неправильно. Правильним є варіант с). Така логіка забезпечує те, що виключення будуть оброблятися в послідовності від нащадка до предку (from child to parent), що дозволяє описати алгоритм обробки більш прямолінійно, рухаючись як би від приватного до більш загального. В цілому це також зробить код більш читабельним.

    Додатково:

    https://airbrake.io/blog/java-exception-handling/the-java-exception-class-hierarchy

    https://codegym.cc/groups/posts/exceptions-in-java

  • [JS] Який з наведених варіантів є вірним рішенням наступної задачі?

    Є масив array, заповнений довільними числами. Необхідно наповнити масив newArray числами з array, які не рівні 0.

    let array = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55];
    
    let newArray = [];

    a)

    for (let i = 0; i < array.length; i++) {
      if (array[i]) {
        newArray.push(array[i]);
      }
    }

    b)

    array.forEach((index, element) => {
      if (element) {
        newArray.push(element);
      }
    });

    c)

    newArray = array.map((element) => {
      if (element) {
        return element;
      }
    });
    1. Жоден

    2. Всі


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

    Якщо вам це питання здається легким і ви вибрали будь який інший варіант окрім варіанту a) - рекомендую розібрати це питання разом з нами. Якщо ви вибрали правильний варіант - вітаю, ви молодець, але розбір все одно буде корисно глянути:)

    Отож, одразу почнемо з варіантів відповідей:

    а) В даному варіанті для перебору масиву використовується звичайний цикл for. Тобто для кожного з елементів масиву array виконується перевірка if (array[i]), яка являє собою неявне приведення числового типу до логічного. Згадаємо, що усі значення числового типу, окрім 0, при приведенні до логічного типу повернуть true. Тому всі ненульові елементи успішно пройдуть перевірку, і далі для кожного з них виконається newArray.push(array[i]). Метод масиву push додає один або більше елементів в кінець масиву - якраз те, що нам потрібно!

    Отже варіант а) є вірним. Але давайте розберемо, чому не підходять інші варіанти.

    b) Тут реалізація рішення схожа на варіант а), за винятком того, що для перебору елементів array замість циклу for використовується метод масиву forEach. Метод forEach приймає як параметр функцію, яка буде викликана для кожного елемента масиву один раз. Ця функція буде викликана з трьома параметрами: function callback(currentValue, index, array),

    де currentValue - значення поточного елемента масиву,
    index - індекс поточного елемента масиву,
    array - масив, для якого було викликано forEach.

    Ось тут і прихована помилка, яку уважні вже помітили! У варіанті b) за значення елементу масиву береться другий параметр функції, коли насправді ж другий параметр відповідає індексу елемента. В результаті ми отримаємо щось на кшталт:
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    Саме тому цей варіант не спрацює коректно і є невірним.

    с) В третьому варіанті використовується метод map, який як і forEach, приймає як параметр функцію-callback та викликає її один раз для кожного елемента масиву. При цьому він створює новий масив зі значень, які поверне ця функція-callback. Отже, якщо значення елементу array не дорівнює 0, то він пройде перевірку if (element) і функція поверне значення даного елемента, яке запишеться у масив newArray. Але якщо елемент дорівнює 0, то наш callback поверне undefined і масив newArray буде містити undefined окрім числових значень, а це не задовольняє умову:
    [undefined, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    Тому варіант с) невірний.

    Зрозуміло, що останні два варіанти також невірні. Отже, правильна відповідь a).

  • [PHP] Що виведе даний код?
    function test(): void {
      $array = [1, 2, 3];
      foreach($array as &$number) {
        echo $number . ' ';
      }
      echo PHP_EOL;
      foreach($array as $number) {
        echo $number . ' ';
      }
    }
    test();

    a)

    1 2 3
    1 2 3

    b)

    1 2 3
    1 2 2

    c)

    1 2 3
    3 3 3

    d) Нічого, відбудеться помилка


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

    Помилок в коді немає, отже варіант d відпадає. Запустивши програму ми побачимо, що правильна відповідь - b.

    1 2 3
    1 2 2

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

    В основі такої поведінки лежать дві особливості PHP: область видимості змінних і посилання.

    У PHP поділяють дві області видимості змінних: локальну і глобальну. Кожна функція створює свою область видимості. Однак локальну область видимості створюють тільки функції - цикли і оператори розгалуження не створюють свої області видимості, і тому змінні, які оголошуються всередині циклу, доступні і поза ним. Розглянемо наступний приклад:

    $array = [1, 2, 3];
    
    foreach($array as $number){
      echo $number . ' ';
    }
    
    echo PHP_EOL;
    echo $number;

    В результаті його виконання буде виведено

    1 2 3
    3

    Тепер ми розуміємо, що змінна $number в першому і другому циклі foreach - одна і та ж сама.

    Після цього в гру вступає досить екзотична і рідко використовувана можливість PHP - посилання.

    Посилання в PHP дозволяють звертатися до однієї і тієї ж змінної під різними іменами, наприклад

    $a = 42;
    $b = &$a;
    $b = 21;
    
    echo $a . ' ' . $b;
    //prints 21 21

    Тепер, коли у нас є всі шматочки пазла, зберемо все воєдино.

    1. Під час першої ітерації створюється змінна $number, якої ще немає в області видимості, в яку за посиланням записуються елементи з масиву $array, тобто на першій ітерації $number вказує на перший елемент [1, 2, 3], на другій ітерації на другий елемент - [1, 2, 3], на третій елемент на третій ітерації відповідно - [1, 2, 3].

    2. Після того, як цикл закінчився, змінна $number знаходиться в області видимості функції і вказує на останній елемент масиву: [1, 2,3]

    3. Починається виконання другого циклу. Змінна $number вже є в області видимості, тому вона перевикористовується.

    4. Коли інтерпретатор PHP виконує foreach, він присвоює значення змінної $number:

    на першій ітерації $number = $array[0], отже масив набуває вигляду [1, 2, 1];
    на другий ітерації $number = $array [1], отже масив набуває вигляду [1, 2, 2];
    на третій ітерації $ number = $ array [2], отже масив не змінюється [1, 2, 2];

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

    Follow up

    Що виведе даний код:

    $arr = [1, 2, 3];
    
    function test(array $array): void{
      foreach($array as &$number){
        echo $number . ' ';
      }
      echo PHP_EOL;
    
      foreach($array as $number){
        echo $number . ' ';
      }
      echo PHP_EOL;
    }
    
    test($arr);
    
    foreach($arr as $num){
      echo $num . ' ';
    }

    Якщо ви відповіли

    1 2 3
    1 2 2
    1 2 2

    то ви не вгадали. Правильна відповідь:

    1 2 3
    1 2 2
    1 2 3

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

    Більш докладно прочитати про посилання і області видимості змінних можна тут:

    References Explained - Manual

    Variable scope - Manual

  • [QA] Була реалізована нова фіча: "Публікація постів у стрічці Facebook". У ході реалізації був задіяний код інтеграції з іншими соцмережами. Виберіть, що можна вважати регресійним тестуванням у даній ситуації.

    a) Публікація постів у стрічці Facebook
    b) Публікація постів в Twitter
    c) Публікація постів в Instagram
    d) Коментування поста
    e) Зчитування тексту поста
    f) Монетизація лайків

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

    Для відповіді на це запитання спочатку треба розібратися з тим, що таке регресійне тестування. Регресійний тип тестування пов'язаний зі змінами у програмі, для виявлення нових дефектів в незмінних областях програмного забезпечення. Під це визначення підпадают два варіанти - b) Публікація постів в Twitter та c) в Instagram. Перший варіант не підходить тому, що це новий функціонал.

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

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


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

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

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

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

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

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

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

  • [Java] Приклад якого патерну проектування ООП представлений в коді?
    package com.learning;
    
    abstract class Smartphone {
    
      protected final String name;
    
      public Smartphone(String name) {
        this.name = name;
      }
    
      public String getName() {
        return name;
      }
    }
    
    class Iphone extends Smartphone {
      public Iphone() {
        super("Iphone");
      }
    }
    
    class Nokia extends Smartphone {
      public Nokia() {
        super("Nokia");
      }
    }
    
    interface SmartphoneProducer {
      Smartphone produce();
    }
    
    final class ChinaProducer implements SmartphoneProducer {
      public Smartphone produce() {
        return new Iphone();
      }
    }
    
    final class NonExistentProducer implements SmartphoneProducer {
      public Smartphone produce() {
        return new Nokia();
      }
    }

    a) Декоратор
    b) Стратегія
    c) Фабричний метод
    d) Абстрактна фабрика

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

    Питання є чисто теоретичним. Жодних помилок компіляції та інших каверз тут на нас не чекає (круто ж :)). Необхідно відразу зробити невеликий дисклеймер, що відповісти правильно на це питання допоможе або кмітливість рівня 80, або ваші знання теорії і прикладів патернів проектування, які знаходяться безпосередньо у вашій пам'яті. Практика показує, що покладатися все ж варто на 2-й варіант.

    Самі патерни, як і будь-які інші сучасні, загальноприйняті поняття та підходи в розробці (існує також термін "best practices"), усталені і протестовані протягом чималого часу, з'явилися на світ, звичайно ж, не випадково. Програмісти, щодня виконуючи завдання з автоматизації і спрощення якихось бізнес-процесів, часто стикалися з досить схожим колом завдань день за днем, тиждень за тижнем і т.д. Звідси і почали з'являтися різні прийоми і методики, що описують рішення якоїсь конкретної задачі, а також практики для спрощення повсякденного життя розробника і, відповідно, економії часу на написання коду і уникнення потреби винаходити так звані "велосипеди". Надалі ці підходи і практики були описані в книгах, статтях, відеокурсах, і неважливо, чи стосується це написання коду, розгортання серверів для веб додатків, оптимізації структури та роботи з базами даних, чи навіть навчання свого власного штучного інтелекту.

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

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

    Тому раджу вам все ж ознайомитися з класифікацією патернів і декількома базовими, які найчастіше зустрічаються в повсякденному житті:

    • Singleton
    • Factory method
    • Abstract Factory
    • Decorator
    • Adapter
    • Builder
    • Strategy

    І, звісно, відповідь на запитання - варіант d).

    Додатково вам допоможуть:

    https://refactoring.guru/

    https://github.com/RefactoringGuru/design-patterns-java

    https://github.com/iluwatar/java-design-patterns

    https://github.com/kamranahmedse/design-patterns-for-humans (приклади на PHP, але все досить доступно)

  • [JS] Необхідно отримати масив зі значеннями об’єкта. Які методи неправильні?

    a)

    for (let key of someData) {
      someList.push(someData[key]);
    }

    b)

    someList = Object.keys(someData).map((key) => {
      return [someData[key]];
    });

    c)

    someList = Object.values(someData);

    d)

    for (let key in someData) {
      someList.push(someData[key]);
    }

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

    Давай по порядку розберемо кожну із функцій. Для зразку використовуватимемо об'єкт

    const someData = { a: 1, b: 2, c: 3 };

    Object.keys - повертає масив із назвами полів (ключами або властивостями) об'єкта. Тобто результатом Object.keys(someData) буде масив ['a', 'b', 'c']

    Object.values натомість повертає значення всіх полів. Результатом Object.values(someData) буде масив [1, 2, 3]

    for ... in - використовується для обходу в циклі усіх властивостей об'єкта. Для

    прикладу наведений нижче код виведе у консолі усі ключі об'єкта та їх значення

    for (const key of someData) {
      console.log(`${key} - ${someData[key]}`);
    }
    
    // у консолі
    // a - 1
    // b - 2
    // c - 3

    for ... of - використовується для обходу в циклі усіх елементів ітерабельних об'єктів (масивів, рядків, NodeList, Set, Map та ін.). З об'єктами дана конструкція дасть помилку. Приклад:

    const someArray = [1, 2, 3];
    
    for (const item of someArray) {
      console.log(item);
    }
    
    // у консолі
    // 1
    // 2
    // 3

    Тому відразу відкидаємо варіант, який використовує for ... of. Всі решта варіантів дійсно присвоюють someList масив. Проте поглянемо уважно на варіант b): return [someData[key]] - виявляється, тут зайва пара квадратних дужок. В результаті для нашого someData отримаємо двовимірний масив [[1], [2], [3]]. Отож неправильними є відповіді a) та b).

  • [PHP] Яку дату буде містити об'єкт $a після виконання наступного фрагмента коду?
    $a = new \DateTimeImmutable('2020-04-25 09:00:00');
    
    $a->modify('next day midnight');

    a) 2020-04-25 09:00:00
    b) 2020-04-26 00:00:00
    c) в PHP немає \DateTimeImmutable
    d) рядок модифікації має неправильний формат


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

    Відразу хочу обмовитися, що в PHP є клас \DateTimeImmutable. Отже, варіант c точно не правильний. Викреслюємо його.

    Варіант d теж є неправильним. Тут використовується Відносний формат дати. Це досить зручний і потужний інструмент. Рекомендую ознайомитись, це однозначно стане вам в пригоді в майбутньому.

    Залишилося 2 відповіді a і b. Щоб правильно відповісти, який з варіантів правильний, треба знати, що ж таке імутабельність і чим відрізняється звичайний \DateTime від \DateTimeImmutable. Імутабельним (незмінним, immutable) називається об'єкт, стан якого не може бути змінено після створення. Результатом будь-якої модифікації такого об'єкта завжди буде новий об'єкт, при цьому старий об'єкт не зміниться. Так як об'єкт $a у нас імутабельний, а ми знаємо, що імутабельний об'єкт не може змінити свого стану, то відповідь a) - 2020-04-25 09:00:00 є правильною (до речі це дата початку першого відбіркового етапу, так що не забудь зареєструватись тут).

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

    class DatePeriod
    {
    
      private \DateTime $start;
      private \DateTime $end;
    
      public function __construct(\DateTime $start, \DateTime $end)
      {
        if ($start >= $end) {
          throw new \InvalidArgumentException('Start date can\'t be greater than end date');
        }
    
        $this->start = $start;
        $this->end = $end;
      }
    
      public function getStartDate(): DateTime
      {
        return $this->start;
      }
    
      public function getEndDate(): DateTime
      {
        return $this->end;
      }
    
      public function getDuration(): int
      {
        return $this->end->getTimestamp() - $this->start->getTimestamp();
      }
    }

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

    $period = new DatePeriod(
      new \DateTime(‘now’),
      new \DateTime(+10 days’)
    );
    
    $period->getStartDate()->modify(+20 days’);

    Через те, що об'єкт start date передається за посиланням (насправді об'єкт містить ідентифікатор об'єкта, але для простоти будемо вважати, що він передається за посиланням), в цьому випадку він буде змінений також і всередині об'єкта $period, що автоматично призведе до невалідності об'єкта $period, адже ми припускаємо, що $start ніколи не може бути більше ніж $end. Це так званий side effect. Щоб уникнути подібної ситуації, варто задуматися про використання імутабельних об'єктів.

  • [QA] Ти документуєш спосіб трансформації даних з існуючої системи для того, щоб вони стали більш сумісними з новою системою. Як називається тип вимог, що ти створюєш?

    a) Нефункціональні вимоги
    b) Transition requirements
    c) Функціональні вимоги
    d) Solution requirements

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

    На перший погляд, може здатися, вимоги щодо перетворення даних можуть відноситься до вимог для нового рішення (Solution requirements), адже саме вони визначають умови для нього. Але такі вимоги не розкривають технічних деталей. Тому правильною відповіддю буде b) - Transition requirements, вони покривають перетворення даних (можливостей та інші) з таких, що існують в системі, на нові. Більше на тему вимог рекомендую подивитись на каналі Карла Вігерса - https://www.youtube.com/watch?v=u2GD4-7tHqc&list=PLA1dXT4tBFfcRj7WmtSbIMlhKHWWUuktk

  • [.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.

  • [Java] Які типи аргументів буде приймати метод?
    package com.learning;
    
    public class Main {
      public static <T extends Vacation> void method(T param) {
        // ...
      }
    }
    
    class Vacation {}
    
    class Quarantine extends Vacation {}

    a) Object, Vacation
    b) Тільки нащадки Vacation
    c) Vacation і його нащадки
    d) Будь-які типи

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

    Традиційно на початку варто визначити, що перевіряється в даному питанні. Які підтеми зачіпаються? У цьому випадку точно зрозуміло, що це Generics.

    Що ж собою являє цей механізм? Узагальнені типи (вони ж Generics) є дуже потужною особливістю інструментарію деяких мов програмування. Багато статично типізованих мов (Java, Kotlin, C++, C#) імплементують цю фічу. Якщо говорити дуже коротко, вони існують для того, щоб мати можливість описувати функціональність і алгоритми обробки даних, які не залежать від конкретного типу цих даних. Адже є чималий ряд ситуацій, коли логіка алгоритму є універсальною незалежно від того, чи працюємо ми з числами, рядками, об'єктами, колекціями об'єктів або чисел і т.д. Хорошим прикладом будуть мабуть стандартні структури даних List, Set, Map та ін.

    Найцікавіша частина в питанні полягає в фрагменті коду "". Щоб правильно на нього відповісти, необхідно точно знати, що таке "wildcards" та особливість поведінки ключового слова "extends" в цій конструкції. Згідно зі специфікацією мови Java правильним буде варіант с), який говорить про те, що або сам клас Vacation, або його спадкоємці можуть передаватися в якості узагальненого типу T.

    Також радимо звернути увагу і вивчити нюанси схожого використання узагальнених типів, наприклад "<? super Vacation>".

    Додатково можна подивитися тут

    https://javarush.ru/groups/posts/2324-wildcards-v-generics

    https://www.baeldung.com/java-generics

  • [JS] Який код потрібно використати, щоб в консоль були виведені результати асинхронної функції asyncFoo()?
    const asyncFoo = () => fetch(`https://google.com`);
    
    const foo = async () => {
      let data;
    
      /* код тут */
    
      console.log(data);
    };

    a) data = async asyncFoo();
    b) data = () => asyncFoo();
    c) data = asyncFoo();
    d) data = await asyncFoo();
    e) data = asyncFoo;


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

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

    Повернемося до нашого завдання. В нас є асинхронна функція foo, всередині якої нам потрібно отримати дані з функції asyncFoo. Як бачимо, asyncFoo повертає результат виконання методу fetch, який в свою чергу виконує мережевий запит за URL адресою, вказаною першим аргументом, і повертає Promise. Отже, після виконання, функція asyncFoo також поверне Promise.

    Тепер, якщо ми просто виконаємо функцію data = asyncFoo(), то в консоль буде виведено Promise в стані очікування - не те, що ми хотіли побачити. Нам потрібно дочекатися виконання Promise-у, щоб отримати дані із запиту. Для цього використовується оператор await, він призупиняє виконання асинхронної функції допоки Promise не буде виконаний або відхилений. Отже правильна відповідь d)

    Якщо ви переконані в тому що правильний інший варіант, розглянемо їх також:

    a) В цьому варіанті виникне помилка через неправильне використання ідентифікатора async.
    b) Такий варіант теж не вірний тому, що в змінній data буде міститися функція, результат котрої поверне Promise.
    c) Це не вірний варіант, бо в data буде Promise в стані очікування.
    d) Вірний варіант.
    e) Цей варіант невірний, адже в data присвоюється сама функція asyncFoo, а не результат її виконання

  • [PHP] Як треба змінити клас A, щоб серіалізований рядок не містив поля 'collection'?
    <?php
    
    final class A
    {
        private string $name;
        private array $collection = [];
    
        public function __construct(string $name)
        {
            $this->name = $name;
        }
    
        public function addToCollection(\DateTimeInterface $element)
        {
            $this->collection[] = $element;
        }
    }
    
    $a = new A('name');
    $a->addToCollection(new DateTime());
    $a->addToCollection(new DateTime('tomorrow'));
    
    $serialized = serialize($a);

    a) Реалізувати магічні методи **sleep() и **wakeup()
    b) Реалізувати інтерфейс Serializable
    c) Реалізувати ArrayAccess
    d) Реалізувати магічні методи **get() та **set()
    e) Реалізувати магічний метод __toString()


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

    Для того, щоб контролювати, які поля містять серіалізований рядок треба або реалізувати магічний метод __sleep(), або реалізувати інтерфейс Serializable. Слід зауважити, що згідно з документацією, класи, що реалізують цей інтерфейс, більше не підтримують __sleep() і __wakeup(). Метод serialize викликається всякий раз, коли потрібна серіалізація екземпляру класу. Цей метод не викликає __destruct() і не має ніяких побічних дій, окрім тих, які запрограмовані всередині нього. Коли дані десеріалізуються, клас відомий і відповідний метод unserialize() викликається як конструктор замість виклику __construct(). Якщо вам необхідно викликати стандартний конструктор, ви можете це зробити в цьому методі.

    Розглянемо приклад реалізації методу __sleep

    public function __sleep()
    {
        return ['name'];
    }

    Якщо порівняти результати серіалізації класу Container перед та після реалізації методу __sleep(), ми побачимо:

    • Перед:
    O:1:"A":2:{s:7:"Aname";s:6:"name A";s:13:"Acollection";a:2:{i:0;O:8:
    "DateTime":3:{s:4:"date";s:26:"2020-04-02 03:11:52.004918";s:13:
    "timezone_type";i:3;s:8:"timezone";s:16:"America/New_York";}i:1;O:8:"DateTime":
    3:{s:4:"date";s:26:"2020-04-03 00:00:00.000000";s:13:"timezone_type";
    i:3;s:8:"timezone";s:16:"America/New_York";}}}
    • Після:
    O:1:"A":1:{s:7:"Aname";s:6:"name A";}

    Як ми бачимо, рядок більше не містить інформацію про властивість collection.

    Того ж самого результату можна досягти імплементуючи інтерфейс Serializable як наведено нижче:

    public function serialize()
    {
      return serialize([
        'name' => $this->name,
      ]);
    
    }
    
    public function unserialize($data)
    {
      $data = unserialize($data);
    
      $this->name = $data['name'];
    
    }

    Отже, як ви вже могли здогадатись варіанти a та b є вірними.

    Даний підхід дозволяє передавати поточний стан об‘єкта, не передаваючи при цьому з‘єднання з базою даних, внутрішній кеш та іншу зайву інформацію, тим самим економлячи об’єм повідомлення, яке буде передаватися. Це може стати у нагоді у системах із розподіленими обчисленнями

  • [QA] У банківській системі існує наступна модель кредитування: при кредиті в 5000 гривень - кредит надається без відсотків, при наступному рефінансуванні зі збільшенням кредиту на суму до 2500 гривень - під 7%, при наступному рефінансуванні зі збільшенням на суму до 18000 гривень - під 23%, при рефінансуванні зі збільшенням на ще більшу суму - відсоток буде 60%. Визначте, який з наборів даних підходить під один клас еквівалентності.

    a) 4000 ₴ ; 6700 ₴ ; 20000 ₴ ;
    b) 5000 ₴ ; 6200 ₴; 7501 ₴;
    c) 7500 ₴; 18000 ₴; 25500 ₴;
    d) 8000 ₴; 20000 ₴; 25500 ₴;

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

    За умовами завдання можемо скласти наступні класи еквівалентності:

    1й клас: 0 - 5000 під 0%
    2й клас: 5001 - 7500 під 7%
    3й клас: 7501 - 25500 під 23%
    4й клас: 25501 - і все, що вище під 60%

    Виходить, що правильною відповіддю буде d), оскільки цей набір даних підпадає під лише один - третій клас.

  • [.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).

  • [Java] Яке значення матиме змінна message?
    package com.learning;
    
    import java.util.Queue;
    import java.util.LinkedList;
    
    public class Main {
      public static void main(String[] args) {
        Queue<String> messages = new LinkedList();
    
        messages.add("Message 1");
        messages.add("Message 2");
        messages.add("Message 3");
        messages.add("Message 4");
        messages.add("Message 5");
    
        RemoveQueueMessage lambda = msgPool -> msgPool.remove();
    
        String message = lambda.remove(messages);
    
        message.toLowerCase();
      }
    }
    
    @FunctionalInterface
    interface RemoveQueueMessage {
      String remove(Queue<String> messages);
    }

    a) Message 5
    b) message 5
    c) Message 1
    d) message 1
    e) Compilation error

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

    В цьому питанні перевіряється розуміння базових структур даних, а також особливості лямбда-виразів в Java.

    Спочатку нам слід визначити, чи виникне помилка компіляції. Для цього треба знати ієрархію основних класів структур даних в так званому Java collections framework (основні структури як List, Queue, Set, Map, Collection), а також особливість визначення лямбда-виразів в Java 8.

    Спробуємо розібратися :)

    Класс LinkedList імплементує інтерфейс Deque, який своєю чергою наслідується від інтерфейсу Queue. Відповідно, рядок “Queue messages = new LinkedList();” ніяких помилок не викличе.

    Наступний потенційно небезпечний крок - це оголошення об’єкту типу “RemoveQueueMessage”. Тіло інтерфейсу описане без помилок, але цікавою для нас є анотація “@FunctionalInterface”. Вона вказує на те, що оголошений нами лямбда-вираз типу “RemoveQueueMessage” може містити лише один метод, і в нашому разі це “remove”. Саме тіло лямбда-виразу описане рядком “RemoveQueueMessage lambda = msgPool -> msgPool.remove();” відповідно до специфікації і не містить помилок. Виклик цього виразу в рядку “lambda.remove(messages);” здійснюється з правильними параметрами. Можемо зробити висновок, що помилки компіляції не буде.

    Все, що залишилося - визначитися з правильною відповіддю. В змінну message запишеться значення методу “msgPool.remove()”, і цим значенням є “Message 1” відповідно до принципу FIFO, на якому грунтується структура даних Queue.

    Це значення не зміниться і не переведеться у нижній регістр в останньому рядку. Варто пам’ятати, що деякі об’єкти (String, Int та ін.) є незмінними (immutable). Тобто, будь-яка зміна створює новий об’єкт такого ж типу, але з новим значенням. Якщо уважно проаналізуємо код, то помітимо, що змінна message просто викликає метод “message.toLowerCase()”, але її значення не переприсвоюється, і отже не буде змінене.

    Тепер ми можемо точно визначити правильну відповідь, і нею є варіант c.

    Додатково можна прочитати тут:

    https://habr.com/ru/post/237043/

    https://javarush.ru/groups/posts/845-lambda-vihrazhenija-na-primerakh

    https://www.baeldung.com/java-8-lambda-expressions-tips

    https://www.tutorialspoint.com/data_structures_algorithms/dsa_queue.htm

  • [JS] Які значення будуть в змінних після виконання коду?
    let a = [1, 3, 2, 4];
    let b = a.sort();

    a) a = [1, 3, 2, 4]; b = [1, 2, 3, 4];
    b) a = [1, 2, 3, 4]; b = [1, 2, 3, 4];
    c) a = [1, 2, 3, 4]; b = 4;
    d) a = [1, 2, 3, 4]; b = [1, 3, 2, 4];


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

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

    Метод працює наступним чином: за замовчуванням елементи приводяться до рядка та порівнюються значення їх UTF-16 послідовності. Наприклад, при виконанні наступного коду [9, 80].sort() результатом буде масив [80, 9] . В числовій послідовності 9 менше за 80, але числа конвертуються в строки та порівнюється їх значення в Unicode послідовності в котрій 80 менше за 9. В нашому прикладі масив a містить всі значення як одноцифрові числа тому результат співпадає з відсортованим чисельним результатом.

    Розберемо наданий код

    1. let a = [1, 3, 2, 4]; ініціалізуємо змінну та присвоюємо їй масив.
    2. let b = a.sort(); ініціалізуєму змінну b та присвоюємо їй значення результату виконання сортування масиву a. Тобто масив a вже також містить відсортовані елементи.

    А тепер розглянемо варіанти відповідей

    1. а) - невірна відповідь тому, що метод sort змінює вихідний масив, а не створює новий, тобто змінна a повинна мати значення відсортованого масиву.
    2. b) - вірна, після виконання коду змінні a та b мають значення відсортованого масиву.
    3. c) - невірна тому, що змінна b повинна мати значення відсортованого масиву
    4. d) - теж невірна тому, що метод sort повертає відсортований масив, а не вихідний.

    Отже, правильна відповідь b)

  • [PHP] Який буде результат виконання наступного блоку коду?
    class Foo
    {
      public static function handle(&$bar)
      {
        if (strlen($bar) > 5) {
          unset($bar);
          $bar = "World!";
          return;
        }
          $bar = "There!";
        }
    }
    
    $bar = 'Hello ';
    echo $bar;
    Foo::handle($bar);
    echo $bar;

    a) Hello There!
    b) Hello World!
    c) Hello
    d) Hello Hello


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

    Отже, спробуємо розглянути варіант відповіді №1, де результатом виконання коду є рядок "Hello There!". Для того, щоб отримати вказаний результат, довжина рядка, змінної $bar, має бути менше або дорівнювати 5 символам. Що, безумовно, не так. Відповідно, 1-й варіант не є вірним.

    Другий варіант, на перший погляд, виглядає більш правдоподібним, ніж попередній. Довжина рядка дійсно більше 5 символів. Але уважний кодер обов'язково помітить один нюанс: $bar передається в функцію unset(), яка, згідно з документацією, робить видалення змінної. "Ну і що?!, - вигукне все той же уважний кодер, - адже відразу ж після цього змінна з тим же ім'ям $bar ініціалізується новим значенням!". І виявиться неправий через те, що змінна $bar в метод handle() передавалася за посиланням і була видалена. Виходить, що початкова змінна успішно знищена. А нова змінна з тим же ім'ям вже створена в контексті методу і з нього не повертається. Виходить, що варіант "Hello World!" теж не правильний.

    Ну все, 3-й варіант "Hello" вже точно вірний! Перша частина відобразилася, а після цього змінна видалена в методі handle() класу Foo. Значить, відображати більше нічого. А ось і ні! Згідно з офіційною документацією PHP (Manual): "Якщо змінна, яка передається за посиланням, видаляється всередині функції, то буде видалена лише локальна змінна. Змінна в області видимості виклику функції збереже те ж саме значення, що і до виклику unset()."

    Таким чином правильним виявляється останній варіант: "Hello Hello".

  • [QA] Припустимо, що прийшов час релізу, і тобі треба виконати великий набір тест кейсів за дуже короткий час. Від чого б ти відштовхувався, щоб забезпечити якість релізу?

    a) Оцінка твого project manager’a / teamlead’a
    b) Пріоритет тест кейсів
    c) Уподобання клієнтів продукту
    d) Пройдеш усі тестові сценарії, навіть якщо треба овертаймити

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

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

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

    Отже, правильна відповідь – b) Пріоритет тест кейсів та c) Уподобання клієнтів продукту.

  • [.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) якраз викличе помилку, тому що ми не вказали тип змінної, яку створюємо.

  • [Java] Яким буде результат виконання даного коду?
    package com.learning;
    
    public class Main {
      public static void main(String[] args) {
        Logger.console("Hello!");
      }
    }
    
    interface Logger {
    
      default void file(String str) {
        // file logging logic
      }
    
      static void console(String msg) {
        System.out.println(msg);
      }
    }

    a) Помилка компіляції, тому що метод інтерфейсу не може містити ключового слова default.
    b) Буде виведений рядок "Hello!".
    c) Помилка компіляції, тому що метод інтерфейсу не може бути статичним.
    d) Помилка компіляції, тому що не можна звертатися до статичного методу через інтерфейс Logger. Спочатку потрібно описати клас, який імплементує цей інтерфейс, і звертатися вже до нього.

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

    У цьому питанні перевіряється знання синтаксису і нюансів версії Java 8 і вище. Правильним є варіант "b" - виведеться рядок "Hello!". Спробуємо розібратися, чому саме цей варіант правильний, а для цього проаналізуємо інші варіанти і спробуємо зрозуміти, чому ж саме вони не підходять.

    Варіант "а" говорить про те, що ключове слово default викличе помилку компіляції. Це неправильно, адже починаючи з версії Java 8 в специфікацію мови була додана можливість описати реалізацію методу інтерфейсу за замовчуванням, і для цього використовується це ключове слово. Нова функціональність дає можливість описати логіку методу всередині інтерфейсу і класи, які в свою чергу будуть імплементувати цей інтерфейс, можуть не перевизначати цей метод.

    У варіанті “c” стверджується, що помилку компіляції викличе ключове слово "static" перед методом. Цей варіант також є помилковим. Починаючи з 8-ї версії дозволяється оголошувати статичні методи всередині інтерфейсу і використовувати їх звертаючись безпосередньо до імені цього інтерфейсу. Зважаючи на це, варіант "d" також є некоректний.

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

    Додатково кілька прикладів тут:

  • [JS] Що буде виведено в консоль під час виконання функції?
    function () {
      const numbers = [1, 2, 3];
      numbers.reduce((sum, number) => {
        console.log(sum, number);
        sum += number;
        return sum;
      });
    }

    a)

    0 1
    1 2
    3 3

    b)

    1 2
    3 3

    c)

    undefined 1
    1 2
    3 3

    d)

    1 2
    3 3
    6 undefined

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

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

    arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

    Де accumulator - значення, яке ми повертаємо при ітерації, currentValue - поточне значення при ітерації, індекс масиву та початкове значення. В нашому випадку акумулятором буде сума, а поточним значенням при ітерації будуть значення масиву (по черзі 1, 2, 3).

    Отже, оскільки початкове значення для функції не задано (початкове значення суми), то при першому проході в консолі ми повинні побачити undefined 1. Далі відповідно 1 2 та 3 3.

    Якщо хтось подумав так само, то це не правильна відповідь. Якщо не передати initialValue в метод reduce, то він в якості нього візьме перший елемент масиву array. А отже, ітерації будуть всього дві, починаючи з другого елементу (з першого, якщо рахувати з нуля) і результат консолі виведений всього двічі. Правильна відповідь b).

  • [PHP] Що буде виведено на екран в результаті виконання наступного коду?
    class Dog {
      static $whoami = 'dog';
      static $sound = 'barks';
    
      static function makeSounds() {
        echo self::makeSound() . ', ';
        echo static::makeSound() . PHP_EOL;
      }
    
      static function makeSound() {
        echo static::$whoami . ' ' . static::$sound;
      }
    }
    
    class Puppy extends Dog {
      static $whoami = 'puppy';
      static $sound = 'howls';
    
      static function makeSound(){
        echo static::$whoami . ' whines';
      }
    }
    
    Dog::makeSounds();
    Puppy::makeSounds();
    

    a)

    dog barks, dog whines
    puppy howls, puppy barks

    b)

    dog barks, dog barks
    puppy howls, puppy whines

    c)

    dog barks, dog barks
    puppy whines, puppy howls

    d)

    dog howls, dog barks
    puppy barks, puppy whines

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

    В даному прикладі використовується «Late Static Bindings» яке означає, що ключове слово static пов'язує метод або властивість (до якого відбувається звернення через static: :) не з тим класом, в якому було визначено використовує його метод, а з тим, в якому цей метод був викликаний під час виконання.

    Першим рядком відповіді, як ви вже напевно здогадалися, буде «dog barks, dog barks».

    Але ось що менш очевидно, так це другий рядок відповіді - «puppy howls, puppy whines».

    Щоб зрозуміти, чому так відбувається, давайте подивимося на метод makeSounds() в класі Dog.

    У методі makeSounds() спочатку викликається self::makeSound(). self:: завжди вказує на контекст того класу, в якому відбувається звернення через нього (в даному випадку це клас Dog). Тому self::makeSound() завжди буде призводити до версії методу makeSound() з класу Dog. Це справедливо і в тому випадку, коли відбувається виклик Puppy::makeSounds(), тому що в класі Puppy немає власного методу makeSounds(), і тому викликається метод makeSounds() з класу Dog. В результаті, при виклику Puppy::makeSounds() виконується self::makeSound(), що призводить до появи тексту «puppy howls».

    Потім в методі makeSounds() класу Dog викликається static::makeSound (). static:: обробляється інакше, ніж self::. При зверненні через static:: використовується версія методу, яка визначена в контексті класу який викликав цей метод під час виконання (в даному випадку, це клас Puppy). Отже, виклик static::makeSound() призводить до виконання версії методу makeSound() з класу Puppy. Саме з цієї причини при виклику з Puppy::makeSounds() методу static::makeSound() виводиться текст «puppy whines». Отже правильна відповідь - b

  • [QA] Дано систему бронювання і покупки залізничних квитків. Квиток може бути доступним, заброньованим, оплаченим повернутим або використаним. Якою технікою тест-дизайну краще скористатись у даному контексті?

    a) Класи еквівалентності
    b) Аналіз граничних значень
    c) Таблиця/діаграма переходів станів
    d) Таблиця рішень

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

    В даному питанні слід зосередитися на бізнес процесі. Оскільки нам відомо, що квиток має різні стани, то логічно, що ці стани були викликані якоюсь дією. Ідеальним варіантом відповіді є таблиця переходів станів, яка дозволяє розробити численні комбінації для досягнення станів. Кожен крок у такій таблиці описується чотирма полями - поточний стан, подія / умова, дія і новий стан. Мета цієї техніки полягає в тому, щоб не тільки скласти список "скриптових" сценаріїв поведінки, але також знайти невизначені ситуації в системі.

  • [.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) Один.

  • [Java] Чому дорівнюватиме значення змінної "a" в результаті виконання наступного коду при початковому значенні a = 10?
    int a = 10;
    try {
      try {
        if (a < 20) {
          throw new Exception();
        }
      } catch (Exception ex) {
        a += 10;
        throw new RuntimeException();
      }
    } catch (Throwable th) {
      a += 5;
    } finally {
      a += 5;
    }

    a) 25
    b) 20
    c) 15
    d) 30


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

    Це питання стосується механізму обробки винятків в Java. Спробуємо проаналізувати логіку роботи цього коду.

    Як ми бачимо, мається вкладений блок try {} catch () {}, і наша основна мета - визначити, в якій саме послідовності виконуватимуться інструкції, а отже яке фінальне значення отримає змінна "a".

    Варто згадати, що в Java логіка вкладених try {} catch () {} не має в собі ніякої магії. Інструкції будуть виконуватися в порядку опису програми або ж "зверху вниз".

    Спочатку відпрацює блок if (a < 20) всередині вкладеного блоку "try". Умова є істинною, адже значення "a" в цей момент - число 10. Далі буде виключення типу "Exception", яке одразу обробляється блоком "catch (Exception ex)", який і є замикаючим для вкладеного "try". Всередині нього значення змінної "a" стане рівним 20. Після цього буде виключення типу "RuntimeException".

    Чи буде цей виняток оброблений далі? Так, адже в нас є зовнішній блок "catch (Throwable th)", в якому вказано, що ми ловимо виняток типу "Throwable". Тип "Throwable" - це клас, який є базовим для всіх класів винятків (java.lang.Exception), а також класів помилок (java.lang.Error). Відповідно, ми можемо зловити і обробити будь-яке виключення / помилку додатка, якщо вкажемо цей тип (RuntimeException успадковує Exception).

    Спочатку також потрібно розібратись, чи виконається "catch (Throwable th)" перед блоком "finally".

    Зв'язка try {} catch () {} finally {} влаштована таким чином, що блок "finally" буде виконаний завжди, незважаючи на те, чи був виконаний блок "catch" раніше. Важливим моментом є те, що "finally" завжди виконується після блоку "catch", якщо той присутній в конструкції (конструкція try {} finally {} теж вважається припустимою), і якщо всередині блоку "try {}" викинуто виключення, яке може перехопити відповідний блок "catch" (і в нашому випадку це так). Сміливо робимо висновок, що, потрапляючи всередину зовнішнього блоку "catch (Throwable th)", змінна "а" матиме нове значення "a + = 5; // 25". Наступним буде виконаний блок "finally", після якого змінна "а" вже матиме значення 30, що є правильною відповіддю.

    Додатково можна почитати тут https://www.geeksforgeeks.org/exceptions-in-java/ або https://javarush.ru/groups/posts/isklyucheniya-java.

  • [JS] Що поверне функція?
    function someFunction() {
      const numbers = [1, 2, 3];
      if (typeof numbers === 'array') {
        return true;
      }
    }

    a) null
    b) undefined
    c) true
    d) false


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

    Все просто - достатньо відкрити консоль Google Chrome і глянути результат. Або трохи подумати і розібрати функцію по рядкам:

    1. const numbers = [1, 2, 3]; - ініціалізація масиву. Тут все просто і без помилок. Йдемо далі.
    2. if (typeof numbers === 'array') - умова, в якій ми порівнюємо тип масиву. Поверне false, оскільки typeof numbers дорівнює “Object”, що в свою чергу не дорівнює “Array”. Якщо ти не знаєш, чому typeof поверне не “Array”, то радимо глянути на статтю в нашому блозі - в ній один з наших коучів розбирає подібну задачу.
    3. { return true; } - Оскільки умова ніколи не виконається, то цей рядок ми пропускаємо.
    4. І оскільки наша функція явно нічого не повертає, то вона поверне неявно undefined.

    Тобто:

      function () {
        const numbers = [1, 2, 3];
        if (typeof numbers === 'array') {
          return true;
        }
          return undefined;
        }

    Очевидно, що так функцію ніхто не стане писати - якщо функція перевіряє, чи numbers являє собою масив, то ми б отримали цей массив аргументом функції та явно повертали значення true або false.

    function сheckArray(numbers) {
      return Array.isArray(numbers);
    }
    або
    function сheckArray(numbers) {
      return numbers instanceof Array;
    }

    Але в якості тестового питання це хороший приклад, щоб перевірити ваші знання.

  • [PHP] Що виведе наведений нижче код?
    function isEqual(float $a, float $b) {
      return $a === $b;
    }
    
    function getEqualKeyValuesCount(array $array) {
      return count(array_filter($array, 'isEqual', ARRAY_FILTER_USE_BOTH));
    }
    
    $array = [];
    $step = 0.01;
    
    for ($i = 2.99; $i >= 0; $i -= $step) {
      $array[$i] = $i;
    }
    
    echo getEqualKeyValuesCount($array);
    

    a) 0
    b) 3
    c) 299
    d) 300


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

    Очевидно, що цикл for буде виконуватись 300 ітерацій від 2.99 до 0 включно. Отже, варіант c одразу відпадає.

    Однак, варіант d - 300 також є неправильним. Справа в тому, що ключем масиву в PHP може бути лише ціле число або строка. Якщо використовувати в якості ключа float, то його значення буде попередньо сконвертованно в integer, тобто буде відкинута дрібна частина, як при використанні функції floor. Детальніше: Arrays.

    Існує 3 цілих числа від 0 до 2.99: 0, 1, 2. Однак, варіант b також невірний.

    Зверніть увагу на функцію isEqual - вона містить грубу помилку, через яку завжди буде повертати false. Таке порівняння float не є коректним, так як цей тип даних має обмежену точність. Щоб переконатися в цьому, спробуємо вивести будь-яке з чисел в масиві $array.

    echo $array[1]; // 1
    // echo і var_dump не допоможуть виявити помилку.
    echo number_format($array[1], 50); // 1.00000000000002042810365310288034379482269287109375
    // зовсім інша справа. Це значення явно відрізняється від
    echo number_format(1, 50); // 1.00000000000000000000000000000000000000000000000000

    Щоб виправити баг, потрібно переписати функцію isEqual таким чином:

    function isEqual(float $a, float $b) {
      $epsilon = 0.00001;
      return abs($a - $b) < $epsilon;
    };

    По стандарту IEEE-754, який реалізовують більшість сучасних мов програмування (в тому числі і PHP), число 1 можливо точно представити в пам'яті. А ось число 0.01, яке зберігається в змінній $step, в двійковій системі представити точно неможливо. Бо воно являє собою нескінченний періодичний дріб 0.00 (00001010001111010111). Тому воно було представлено приблизно, а після того як ми послідовно здійснювали операції з цим числом, помилка накопичувалась.

    До речі, через накопичення помилки в цьому прикладі зробити значення $epsilon рівним константі PHP_FLOAT_EPSILON, не допомогло б виправити баг. Вибір кроку або початкового значення циклу дробовим числом - погана ідея.

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

    Ще один приклад:

    function isAbsoluteZero($temperature) {
      return $temperature === -273.15;
    };
    
    $step = 0.01;
    $temperature = 0;
    
    while (!isAbsoluteZero($temperature)) {
      $temperature -= $step;
    }
    
    if ($temperature === -273.15) {
      echo 'Absolute zero!';
    } else if ($temperature > -273.15) {
      echo 'Not so cold';
    } else {
      echo 'Unreal cold!';
    }

    Незважаючи на те, що в if-else блоці передбачено виведення одного з трьох рядків для будь-якого значення $temperature, на екран не буде виведено нічого. Програма зависне через нескінченний цикл while. Значення температури ніколи точно не збігається зі значенням -273.15.

  • [QA] Дано поле для введення ім'я користувача, у якого є обмеження: кількість символів повинна бути не менше 1 і не більше 15. Виберіть, які тестові дані перевіряють граничні значення за кількістю.

    a) -порожньо-
    b) a
    c) Ян
    d) Іван
    e) Іван Петрович
    f) Іван Васильович
    g) Іван Афанасійович


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

    Для виконання цього завдання потрібно згадати про аналіз граничних значень - перевірка поведінки системи на "межі" вхідних даних. У нашому випадку це 1 і 15. У теорії, до меж потрібно додавати 1 і -1, що дасть в результаті 6-ть перевірок, але це є надмірним. Тому, достатньо перевірити невалідні - 0, 16, та валідні - 1, 15. Таким чином правильними відподями будуть варіанти a, b, f, g

  • [.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”

  • [Java] Який результат виконання наведеного коду?
    package com.learning;
    
    public class Main {
    
      public static void main(String[] args) {
        BaseLogger l1 = new BaseLogger();
        ChildLogger l3 = new ChildLogger();
        BaseLogger l2 = new ChildLogger();
        consoleLog(l1);
        consoleLog(l2);
        consoleLog(l3);
    
      }
    
      public static void consoleLog(BaseLogger logger) {
        logger.log();
      }
    }
    
    class BaseLogger {
      public void log() {
        System.out.println("Base");
      }
    }
    
    class ChildLogger extends BaseLogger {
      @Override
      final public void log() {
        System.out.println("Child");
      }
    }

    a) RuntimeException b)

      Base
      Child
      Base

    c) Помилка компіляції
    d)

      Base
      Child
      Child

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

    У цьому питанні акцент — на використання фундаментальних принципів ООП, зокрема — наслідування і поліморфізму, а також синтаксис перевизначення методів. Помилки компіляції та виключень не буде, оскільки код написаний відповідно до усіх правил. Правильний варіант: d — відображення рядків у послідовності:

    Base
    Child
    Child

    Спочатку буде виведено стрічку "Base", оскільки змінній l1 присвоюється екземпляр об’єкта BaseLogger. У цьому випадку клас ні від кого не наслідується, логіка цілком прямолінійна. Клас "ChildLogger" — нащадок "BaseLogger", і у ньому визначений наперед метод "log", котрий, у свою чергу, виведе в консоль стрічку "Child".

    Найцікавішим є рядок BaseLogger l2 = new ChildLogger();, що додає певної невизначеності.

    Статичний метод “consoleLog” приймає аргумент типу BaseLogger, як і будь-який його похідний клас-нащадок відповідно. Однією з ключових особливостей поліморфізму є можливість підставити потрібну реалізацію під час виконання (Runtime). Спираючись на цю особливість, можна стверджувати, що у рядку "consoleLog(l2);" також буде виведено стрічку "Child".

    Непоганий приклад також наведено тут: https://www.geeksforgeeks.org/polymorphism-in-java/.

  • [JS] Які функції у наведеному фрагменті коду можна назвати чистими (pure function)?
    let a1 = 10;
    let a2 = 5;
    const obj1 = {
      prop1: 2,
      prop2: 4,
    };
    
    function func1(arg1, arg2) {
      return arg1 + arg2;
    }
    
    function func2(arg1, arg2) {
      return arg1 + a2 * arg2;
    }
    
    function func3(arg1, arg2) {
      a1 = arg2;
      return arg1 + a1;
    }

    a) func1
    b) func2 и func3
    c) func1 и func2
    d) жодна

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

    Спершу розберемося, що таке “чиста функція”. Чистими функціями називають функції, які відповідають двом умовам:

    • При одних і тих самих аргументах функція завжди повертає одне і те саме значення
    • Не містить побічних ефектів (side effects)

    У першій умові йдеться про те, що функція завжди повертатиме однаковий результат, якщо вхідні аргументи також однакові. Наприклад, функція added завжди повертатиме суму вхідних аргументів.

    function adder(x, y) {
      return x + y;
    }
    
    adder(7, 5); //12
    adder(7, 5); //12
    adder(7, 5); //12

    Але якщо використати аргументи ззовні, то результат вже не буде одним і тим самим, навіть якщо аргументи функції одні й ті ж самі:

    let y = 5;
    
    function adder(x) {
      return x + y;
    }
    
    adder(7); //12
    y = 10;
    adder(7); //17

    Результат функції непередбачуваний, оскільки залежить від змінної y, і такі функції називають не чистими. Чисті функції не повинні покладатись на змінні за межами їх області видимості.

    Друга умова стверджує, що чиста функція не повинна ніяк впливати на змінні зовні чи змінювати стан додатку, тобто містити так звані side effects. До побічних ефектів можна віднести роботу з DOM, роботу з файловою системою або HTTP запити.

    Наприклад, функція adder хоча і повертає завжди один і той самий результат, та все одно окрім цього змінює стан ззовні (записує дані у файл):

    function adder(x) {
      let result = x + y;
      fs.writeFileSync('result.txt', result);
      return result;
    }
    
    adder(7, 5); //12
    adder(7, 5); //12

    Така поведінка не є очевидною для функції, яка повинна просто повернути суму двох чисел. Однак, якщо виникає потреба записати результат у файл, можна зробити так:

    function adder(x) {
      return x + y;
    }
    
    let result = adder(7, 5);
    fs.writeFileSync('result.txt', result);

    Наступний приклад також не буде чистою функцією, оскільки змінює об'єкт ззовні та окрім того нічого не повертає:

    var obj = {
      active: false,
    };
    
    function setActive(obj) {
      obj.active = true;
    }
    
    setActive(obj);

    Отже, повертаючись до питання:

    • функція func1 чиста.
    • функція func2 нечиста, оскільки використовує аргумент a2, запозичений ззовні.
    • функція func3 також не чиста, оскільки мутує аргумент a1.

    Правильна відповідь: а — func1

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

  • [PHP] Що відбудеться зі змінними $a і $b після виконання наведеного коду?
    function doSomething(array $array, \DateTime $object)
    {
      $object->modify('+5 days');
      $array[] = $object;
    }
    
    $a = [];
    $b = new \DateTime('2020-01-01');
    
    doSomething($a, $b);

    a) $a міститиме об’єкт $b, $b залишиться без змін
    b) $a і $b залишаться без змін
    c) $aзалишиться без змін, об’єкт $b буде змінено
    d) $a буде містити об’єкт $b, об’єкт $b зміниться

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

    Як ви напевно знаєте, у РНР змінні об’єктів не містять самого об’єкта як значення. Така змінна містить лише ідентифікатор об’єкта, котрий дозволяє знайти конкретний об’єкт при зверненні до нього. Коли об’єкт передається як аргумент функції, повертається або присвоюється іншій змінній, то ці різні змінні містять копію ідентифікатора, що вказує на один і той самий об’єкт. Таким чином варіанти a і b, вочевидь, неправильні. Більш детальну інформацію про ООП і роботу з об’єктами можна знайти тут — Classes and Objects — Manual

    З масивами (у РНР масиви — хеш-таблиці), все відбувається дещо інакше. Річ у тім, що при передачі масиву як аргумента функції або при присвоюванні масиву іншій змінній завжди відбувається копіювання значення Arrays — Manual
    Це означає, що змінна $array у функції doSomething містить свою локальну копію масиву $a, і змінну $a не буде змінено. Правильний варіант відповіді — c.

    Розгляньмо ще такий приклад:

    $a = new \DateTime('2020-01-01');
    $arrayA = [$a];
    $arrayB = $arrayA;
    $a->modify('+5 days');

    У цьому прикладі $arrayB є копією масиву $arrayA. У свою чергу, $arrayA (як і $arrayB) у якості свого єдиного елемента містить ідентифікатор об’єкта $a. Це означає, що при зміні об’єкта $a фактично оновиться і значення елементів масивів $arrayA і $arrayB.

  • [QA] У специфікації вказано, що форма реєстрації нової соціальної мережі мусить витримувати навантаження у 10 одночасних з'єднань. В продакшені навантаження складало понад 100 одночасних підключень. Який QA процес допоміг би уникнути цієї проблеми?

    a) валідація
    b) верифікація
    c) оцінка ризиків
    d) тестування навантаження

    Розгорнути правильну відповідь з поясненням
    У вимогах чітко вказано, що система повинна витримувати навантаження у 10 паралельних підключень, і перевірка цієї вимоги є верифікацією (перевірка відповідності додатку прописаним вимогам). Можливо, існує думка, що тестування навантаження допомогло б виявити проблему ще до її появи, але таке тестування грунтувалося б на специфікації. Тому про 100 одночасних підключень ніхто б не думав. Тож правильна відповідь — валідація, перевірка відповідності додатку вимогам, що маються на увазі (всі решта, крім прописаних). Для цього необхідно думати поза документацією і розуміти домен продукту, що розробляється.