Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Функції й замикання»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

Обидві створюють об'єкт Closure, але по-різному працюють із зовнішніми змінними.

Анонімна функція (function () use (...)) бачить лише ті зовнішні змінні, які явно перелічені в use, і захоплює їх за значенням у момент створення:

$rate = 1.2;
$withTax = function (int $price) use ($rate): float {
    return $price * $rate;
};

Щоб змінювати зовнішню змінну, - захоплення за посиланням: use (&$total).

Стрілкова функція (fn, PHP 7.4) автоматично захоплює всі зовнішні змінні, які використовує, за значенням:

$withTax = fn (int $price): float => $price * $rate;

Відмінності:

  • Лише один вираз. Тіло стрілкової функції - один вираз, результат якого повертається. Кілька інструкцій - тільки анонімна функція.
  • Не можна змінити зовнішню змінну. fn () => $count++ змінить копію, а не $count ззовні. Для накопичення - анонімна функція з use (&$count).
  • use не потрібен - менше шуму в коді.

Де що використовують:

  • fn - короткі колбеки: array_map(fn ($u) => $u->email, $users), ->filter(fn ($o) => $o->isPaid()), ->when($search, fn ($q) => $q->where(...)).
  • function - колбеки з кількома кроками, розгалуженнями чи зміною зовнішнього стану.

Обидві автоматично прив'язують $this усередині класу. Щоб уникнути цього (і зайвого утримання об'єкта в пам'яті), - static fn чи static function.

Докладніше в документації: Стрілкові функції

Значення за замовчуванням роблять параметр необов'язковим:

function paginate(array $items, int $perPage = 15, int $page = 1): array
{
    return array_slice($items, ($page - 1) * $perPage, $perPage);
}

paginate($items);            // 15 на сторінку, перша сторінка
paginate($items, 50);
paginate($items, page: 3);   // іменований аргумент пропускає $perPage

Правила:

  • Значення за замовчуванням - константний вираз: літерал, константа, new (з PHP 8.1), масив. Не виклик функції.
  • Необов'язкові параметри йдуть після обов'язкових. Обов'язковий параметр після необов'язкового - deprecated.
  • null за замовчуванням вимагає nullable-типу: ?string $name = null.

Variadic-параметр ...$name збирає всі решту аргументів у масив:

function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

sum(1, 2, 3);          // 6
sum(...[1, 2, 3]);     // розпаковка масиву в аргументи
  • Може бути лише останнім параметром.
  • Тип застосовується до кожного елемента: int ...$numbers.
  • Іменовані аргументи, що не відповідають жодному параметру, потрапляють у variadic з рядковими ключами.

Пастки:

  • Змінюване значення за замовчуванням через new (Options $o = new Options()) створює новий об'єкт при кожному виклику - на відміну від Python, тут проблеми спільного стану немає.
  • Багато необов'язкових параметрів - запах. Якщо їх п'ять, краще об'єкт параметрів (DTO) чи кілька окремих методів.
  • Булевий прапорець за замовчуванням (bool $force = false) робить виклики незрозумілими - іменований аргумент рятує: delete(force: true).

Докладніше в документації: Аргументи функцій

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

function addItem(array &$cart, string $item): void
{
    $cart[] = $item;
}

$cart = [];
addItem($cart, 'книга');
// $cart === ['книга']

Де це в стандартній бібліотеці: sort(), array_push(), preg_match() (масив збігів), array_walk() - вони змінюють передану змінну.

Чому у власному коді це часто погана ідея:

  • Прихований побічний ефект. З виклику addItem($cart, 'книга') не видно, що $cart зміниться. Читач має знати сигнатуру.
  • Важче міркувати й тестувати: функція не просто повертає результат, а змінює стан ззовні.
  • Не дає економії пам'яті. Масиви копіюються ліниво (copy-on-write), тож передача за значенням не копіює дані, доки їх не змінять.
  • Не можна передати вираз: addItem([], 'x') чи addItem(getCart(), 'x') - помилка, потрібна саме змінна.

Краща альтернатива - повертати нове значення:

function withItem(array $cart, string $item): array
{
    return [...$cart, $item];
}

$cart = withItem($cart, 'книга');

Або - об'єкт зі станом і методом: $cart->add('книга') - зміна очевидна з виклику.

Об'єкти й так не копіюються: функція, що отримала об'єкт, працює з тим самим об'єктом. & для об'єкта потрібен лише тоді, коли функція має замінити змінну іншим об'єктом.

Коли посилання виправдані: зміна великих вкладених структур на місці в гарячому коді та рідкісні API на кшталт «повернути кілька значень» - хоча тут читабельніше повернути масив чи об'єкт.

Докладніше в документації: Передача аргументів за посиланням