← Academy Blog

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

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

  • [.NET] Який результат виконання програми?
    static void Main(string[] args)
    {
      Console.Write(GetValue());
    }
    
    public static string GetValue()
    {
      try
      {
        throw new Exception("oops");
      }
      catch (Exception)
      {
        Console.Write("catch ");
        return "return ";
      }
      finally
      {
        Console.Write("finally ");
      }
    }

    a) catch return finally
    b) catch finally return
    c) finally catch return
    d) catch return

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

    Правильна відповідь: b) catch finally return

    Щоб відповісти на це питання, давайте спочатку заглибимось у виключення і як їх обробляти на платформі .NET. Виключення - це помилка, яка генерується при виконанні програми внаслідок непередбачуваної поведінки. Якщо таку помилку не обробити, то наша програма завершить своє виконання. Саме для цього мова C# надає розробникам таку конструкцію, як try-catch-finally блоки. Де try блок - це та ділянка коду, яка на нашу думку є ненадійною і в нас є план дій, як усунути негативні наслідки в разі виникнення непередбачуваної ситуації в ній. Цей план дій описується в catch блоках, при чому їх може бути як декілька, так і взагалі не бути об’явленим. Декілька catch блоків описують для того, аби можна було більш точно обробити помилку, наприклад помилки різних типів (ArgumentException, InvalidOperationException і т.д), або накласти додаткову умову через вираз when, який вказується зразу після catch виразу. І останній блок - це опціональний блок finally, який виконується зразу після обробки виключення у блоці catch, і він може бути вказаним тільки один раз. Зазвичай finally блок застосовується для звільнення ресурсів, таких як закриття файлу.

    Давайте спробуємо відтворити порядок дій програми, взявши до уваги все, що було описано вище. Спочатку викликається метод GetValue, результат виконання якого буде виведено в консоль за допомогою Console.Write. При виконанні GetValue метода одразу ж генерується виключення, але оскільки код, що генерує виключення, обернений в try блок, то виконання переходить в catch, де виводиться стрічка “catch ” і повертається “return ”. Але не забуваймо, що в нас також є finally, який виконується зразу після catch блоку. Тому результат повернення зберігається і буде повернуто до місця виклику лише після того, як відпрацює finally, тобто коли виведеться стрічка “finally ”. Склавши всі ці кроки докупи, отримаємо “catch finally return”.

  • [Java] Що буде виведено на екран після виконання цього коду?
    public class Test {
      public static void main(String[] args) {
        int x = 1;
        Integer y = new Integer(x);
        int [] z = {x};func(x, y, z);System.out.print(x);
        System.out.print(y);
        System.out.println(z[0]);
      }static void func (int x, Integer y, int[] z) {
        x++;
        y++;
        z[0]++;
      }
    }
    

    a) 122
    b) 111
    c) 112
    d) 11undefined


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

    Це питання покриває дві теми: Value та Reference типи, та autoboxing-autounboxing. ​ Почнемо з відкидання найбільш очевидного варіанту: в Java немає undefined, при звертанні до неіснуючого елементу массиву буде викинуто IndexOutOfRangeException, тому варіант e) відпадає. ​ ​ Тепер розглянемо наступний варіант на вибування - d). Оскільки int - це value type, то при виклику метода буде передано копію значення, зміна якої ніяк не вплине на оригінальне значення. ​ Також знаючи, що масив - reference тип, можна відкинути варіант b). Оскільки при передачі reference параметрів передається не копія значення, а посилання на купу, де знаходиться значення, зміна значень параметра в методі призводить до зміни оригінального значення. ​ Залишаються два варіанти - a і c. Оскільки типи-обгортки примітивів є об'єктами, відповідно вони є reference типами, можна було б зробити висновок, що варіант a) - правильний. Але слід пригадати, що типи-обгортки примітивів є іммутабельними. Розглянемо невеликий приклад:

    package com.binary_studio_academy;public class Main{
      public static void main(String[] args) {
        Integer a = 0;
        Integer b = a;
        a++;
        System.out.println(a + " " + b);
      }
    }

  • [JS] Оберіть варіант, у якому при зверненні до методу helper не виведеться помилка. (itemHandler.addItem(2)):

    a)

    const itemHandler = {
      addItem: function (arg) {
        let a = this.helper(arg);
      },
      helper: (prop) => {
        return prop;
      },
    };

    b)

    const itemHandler = {
      addItem: (arg) => {
        let a = this.helper(arg);
      },
      helper: (prop) => {
        return prop;
      },
    };

    c) Обидва правильні

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

    Чергове питання з рубрики "захоплюючий" javascript. У цьому питанні перевіряється, чи знаєш ти, що таке контекст функції і як працюють arrow functions. Це питання часто виникає при роботі з React, коли ти намагаєшся передати метод через властивості компонента, і не можеш зрозуміти, чому в методі компонента стан іншого компонента 🤷🏿 Але не хвилюйтеся, зараз ми з цим розберемося 😉

    Коли ми оголошуємо метод/функцію через function, то при її виклику буде використовуватися контекст (this), в якому вона викликається. Але варто нам записати функцію в інший об'єкт, this відразу зміниться. Щоб це зрозуміти, давайте проведемо експеримент:

    // оголошуємо функцію
    function f() {
      return this;
    }
    
    // записуємо ії в об’єкт
    const a = { f: f };
    
    // перезаписуємо ії з об’єкта
    const b = {};
    b.f = a.f;
    
    // викликаємо їі з методу об’єкта
    const c = {
      f: function () {
        return f();
      },
    };
    
    // перевіряємо
    console.log(f() === globalThis); // true
    console.log(a.f() === a); // true
    console.log(b.f() === b); // true
    console.log(c.f() === globalThis); // true

    У першому варіанті повернеться globalThis - це глобальний контекст (у браузері це window). Іншими словами, якщо функція викликається не як метод об'єкта, то this буде глобальний. Але варто нам записати її в якийсь об'єкт, то this зміниться. Крім того в JS у функції є 3 методи, які дозволяють змінити контекст насильно:

    1. f.apply(context, [...args]) - викличе функцію з контекстом context, значення аргументів функції будуть відповідати елементам масиву args
    2. f.call(context, ...args) - викличе функцію з контекстом context, аргументи (args) передаються послідовно
    3. f.bind(context, ...args) - змінить контекст і поверне нову функцію, без виклику

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

    const a = {};
    const d = {};
    d.f = f.bind(a);
    console.log(d.f() === a); // true

    Arrow function у свою чергу успадковує контекст з місця свого визначення. Причому, якщо ми визначимо arrow function у інший arrow function, то js буде підніматися вгору, поки не зустріне перший-ліпший контекст 🤯 Давайте подивимося, що буде якщо f визначити через arrow function:

    const f = () => this;
    
    const a = { f: f };
    
    const b = {};
    b.f = a.f;
    
    const c = {
      f: function () {
        return f();
      },
    };
    
    console.log(f() === globalThis); // true
    console.log(a.f() === globalThis); // true
    console.log(b.f() === globalThis); // true
    console.log(c.f() === globalThis); // true

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

    const contextA = {
      f1: function () {
        const foo = () => this;
        return foo(); // викликаємо
      },
      f2: function () {
        const bar = () => this;
        return bar; // не викликаємо
      },
    };
    const contextB = {};
    contextB.f1 = contextA.f1; // (1)
    contextB.f2 = contextA.f2(); // (2)
    
    console.log(contextA.f1() === contextA); // true
    console.log(contextA.f2()() === contextA); // true
    console.log(contextB.f1() === contextB); // true
    console.log(contextB.f2() === contextA); // true

    Зверніть увагу, що коли ми поміняли контекст методу f1 з contextA на contextB (1), то він автоматично змінився для внутрішньої foo, тому що контекст визначення foo змінився.

    А в методі f2 ми визначили функцію bar в контексті contextA та привласнили її contextB (2), при цьому контекст не змінився, тому що місце визначення залишилося в contextA.

    Отже, щоб бути впевненим у тому, що контекст функції буде той, який ми очікуємо, незалежно від того, де ми будемо її викликати, ми можемо використовувати або bind(), або arrow function.

    Фуух, це було складно, але я думаю, тепер ви готові відповісти на питання 🙂 У варіанті b) ми визначаємо функцію в глобальному контексті, а не в контексті об'єкта, а оскільки в globalThis ми не визначили метод helper(), то буде помилка this.helper is not a function. Значить, варіант b) неправильний, відповідно і відповідь c) - теж. Залишається тільки a), ну а ми й без цього вже знаємо, що якщо метод визначений через function, то контекст буде той, де ми цей метод викличемо, в даному випадку на об'єкті itemHandler.

    Щоб краще підготуватися до вступного тесту, рекомендую ознайомитися з функціями за наступними посиланнями:

    https://javascript.info/function-expressions
    https://javascript.info/arrow-functions-basics
    https://javascript.info/javascript-specials
    https://javascript.info/advanced-functions

  • [QA] Які з перелічених методик тестування відносяться до динамічних?

    a) Тестування специфікації
    b) Читання source code
    c) Приймальне тестування
    d) Тестування GUI

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

    Для початку пригадаємо, що таке динамічне та статичне тестування. Динамічне тестування - це тип тестування, який перевіряє функціональність застосунку під час виконання коду. Фактично це означає перевірку застосунку під час його роботи. Статичне тестування навпаки не передбачає, що програмний код буде запущено. Зазвичай статичне тестування проводиться на первинних стадіях розробки ПО, тобто не передбачає імплементації повного функціоналу. Під визначення статичного тестування підходять відповіді a) Тестування специфікації та b) Читання source code, оскільки тестування цих типів може бути проведено без запуску коду. Тому правильна відповідь c) Приймальне тестування та d) Тестування GUI, оскільки виконати тестування цих типів неможливо без запуску застосунку. Приймальне тестування виконується для перевірки виконання програмним забезпеченням задач, які були поставлені, та відповідності вимогам. Тестування GUI - це перевірка графічного інтерфейсу.

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

    a) 1
    b) 2
    c) 3
    d) Hello World! і так виводиться

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

    Правильна відповідь: c) 3

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

    Перейдемо до завдання. В першому рядку метода Main створюється екземпляр класу ChildClass, і одразу ж виконується неявне приведення його до базового класу BaseClass, тобто виконується upcasting. Хоча в нас посилання є BaseClass типу, але все одно воно вказує на ту ділянку в пам’яті, де було створено екземпляр класу ChildClass. Тому при виклику функції Hello в другому рядку викликається перевизначена версія методу з класу ChildClass, тобто варіанти 1 і 4 вже відпадають. Якщо розкоментувати рядок 2, то відбудеться вічна рекурсія, яка завершиться помилкою переповненого стеку. Залишається варіант 3, який і є правильним. В цьому випадку спочатку виконається реалізація базового класу, що виводить стрічку “Hello”, і після цього виведеться " World!" з наявної реалізації.

  • [Java] Який результат виконання цього коду?
    package com.learning;public class Main {
      public static void main(String[] args) {
        BaseLogger l1 = new BaseLogger();
        ChildLogger l2 = new ChildLogger();
        BaseLogger l3 = 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) BaseChildBase
    c) Помилка компіляції
    d) BaseChildChild


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

    Для розв'язання задачі скористаємось методом виключення:

    • Код успішно скомпілюється, тому варіант 3 відпадає;
    • Runtime виключень також немає, тому варіант 1 - теж не правильний;
    • Залишається або BaseChildBase, або BaseChildChild. Оскільки при створенні змінної l3 ми створюємо екземпляр класу ChildLogger, у якому метод log перевизначено, у консоль буде виведено BaseChildChild. Отже, правильна відповідь 4.

    Це питання покриває реалізацію двох аспектів ООП у Java - subtype polymorphism та наслідування. Розглянемо більш цікавий приклад використання наслідування:

    package com.learning;
    ​
    ​
    public class Main {
      public static void main(String[] args) {
        BaseValueCalculator l1 = new BaseValueCalculator();
        BaseValueCalculator l2 = new AdvancedValueCalculator();System.out.println(l1.calculate().value());
        System.out.println(l2.calculate().value());
      }
    }class BaseValue{
      protected final String val;
      public BaseValue(String val){
        this.val = val;
      }
      public String value(){
        return this.val;
      }
    }class CapitalizedValue extends BaseValue{
      public CapitalizedValue(String val){
        super(val);
      }
      @Override
      public String value(){
        return this.val.toUpperCase();
      }
    }class BaseValueCalculator {
      public BaseValue calculate() {
        return new BaseValue("I'm simple calculator!");
      }
    }class AdvancedValueCalculator extends BaseValueCalculator {
      @Override
      final public CapitalizedValue calculate() {
        return new CapitalizedValue("I'm advanced calculator!");
      }
    }

    Який результат виконання цього коду?

    1. RuntimeException
    2. I'm simple calculator!I'M ADVANCED CALCULATOR!
    3. Помилка компіляції
    4. I'm simple calculator!I'm advanced calculator!

    Головним питанням у цьому прикладі є те, чи цей код скомпілюється. Return type методів у Java [коваріантний](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science), отже, його можна перевизначити у класах нащадках. Розглянемо щє один приклад, цього разу спробуємо погратись з типами параметрів:

    package com.learning;
    ​
    ​
    public class Main {
      public static void main(String[] args) {
        BaseLogger l1 = new BaseLogger();
        ChildLogger l2 = new ChildLogger();
        BaseValue message = new BaseValue("lsp is a myth");
        l1.log(message);
        l2.log(message);
      }
    }class BaseLogger {
      public void log(BaseValue value) {
        System.out.println(value.value());
      }
    }class ChildLogger extends BaseLogger {
      final public void log(CapitalizedValue value) {
        System.out.println(String.format("<<<%s>>>", value.value()));
      }
    }class BaseValue {
      protected final String val;public BaseValue(String val) {
        this.val = val;
      }public String value() {
        return this.val;
      }
    }class CapitalizedValue extends BaseValue {
      public CapitalizedValue(String val) {
        super(val);
      }@Override
      public String value() {
        return this.val.toUpperCase();
      }
    }

    ​ Який результат виконання даного коду?

    1. RuntimeException
    2. lsp is a myth <<<lsp is a myth>>>
    3. Помилка компіляції
    4. lsp is a myth lsp is a myth

    ​ Якщо ви відповіли “помилка компіляції”, то вітаю вас: ви помилились. Правильна відповідь - 4, привітаємо всіх, хто так відповів. На відміну від return значень, параметри методу є інваріантними. У прикладі ми спробували зробити параметр нащадка коваріантним, але навіть якщо ми використаємо контраваріантний тип параметру(BaseLogger приймав би CapitalizedValue, а ChildLogger - BaseValue), все одно перевизначення методу log не відбулось би. Тому у ChildLogger відбувається не overriding, а overloading методу log, і при виклику методу log з параметром типу BaseValue буде викликано реалізацію методу log батьківського класу, а при виклику з параметром типу CapitalizedValue буде викликано перевантажену реалізацію з класу ChildLogger. На практиці, хоч анотація @Override не є обов'язковою, її слід завжди використовувати для запобігання неочікуваним перевантаженням замість перевизначень. ​

    На цьому на сьогодні все, дякуємо за увагу. Якщо хочеш більше дізнатись про наслідування та варіантність у Java, тицяй на посилання знизу. Побачимось в Академії ;) ​

  • [JS] Чи можна замінити вираз 1 виразом 2?
    const someBooleanValue = isNaN(+someVariable);
    const someBooleanValue = +someVariable == NaN;

    a) Так, у будь-якому випадку
    b) Ні
    c) Так, якщо someVariable в обох виразах буде числом

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

    У цьому питанні приховані одразу два підводні камені JS: приведення типів та властивості NaN.

    Знак + перед змінною приведе значення до числового типу, тобто +someVariable еквівалентно Number(someVariable). Наприклад, якщо привести до числа наступний рядок 34, то ми отримаємо число:

    +'34' + 6; //  40

    Але якщо додати рядок до змінної, то незважаючи на значення, результатом буде рядок, тобто typeof ('' + someVariable) === 'string'. Наприклад:

    '' + '34' + 6; // “346”

    Виходячи з цього, ми можемо перевірити результат виконання, підставляючи значення різних типів:

    console.log(+true); // 1
    console.log(+'2'); // 2
    console.log(+2); // 2
    console.log(+null); // 0
    console.log(+{}); // NaN
    console.log(+[]); // 0
    console.log(+undefined); // NaN
    console.log(+'a'); // NaN

    Як бачите, у випадках, коли JS не може перетворити значення у число, ми отримуємо NaN (Not a Number). До речі, чи знаєте ви, чому пустий масив дорівнює 0? Пишіть свою відповідь у коментарях 😉

    Знаючи це, ми можемо відсіяти відповідь с) , бо someVariable не обов’язково зберігати число. Але щоб відповісти правильно, ми маємо знати головну властивість NaN: NaN не дорівнює жодному зі значень, включаючи NaN, тобто:

    console.log(NaN === NaN); // false

    Тобто, вираз 2 не буде коректним, якщо +someVariable поверне NaN.

    Для того, щоб перевірити, чи дорівнює змінна NaN, треба використовувати функцію isNaN(someVariable) або ж Number.isNaN(someVariable). Тому вираз 1 є єдиним правильним способом перевірити, чи значення є числом, і це означає, що вони не еквівалентні між собою. Тому правильна відповідь b) Ні.

    До речі, якщо в масиві одне з значень буде NaN, то indexOf(NaN) поверне -1, а от includes(NaN) поверне true:

    let arr = [2, 4, NaN, 12];
    arr.indexOf(NaN); // -1
    arr.includes(NaN); // true

    Більш детально з NaN можете ознайомитись за посиланням.

  • [QA] Що з наведеного нижче буде правильним показником прогресу тестування?

    a) Кількість непомічених дефектів
    b) Кількість тестів, які ще не виконані
    c) Загальна кількість дефектів у продуктіі
    d) Зусилля, необхідні для виправлення всіх дефектів

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

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