← Academy Blog

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

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

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

  • [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. Щоб уникнути подібної ситуації, варто задуматися про використання імутабельних об'єктів.

  • [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 є вірними.

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

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

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

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

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