<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · JUNIOR

Як масиви передаються в функції і коли спрацьовує копіювання?

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

Передати масив на 200 тисяч рядків у функцію - це дорого?
PHP передає масиви за значенням чи за посиланням?
Чому обʼєкт усередині функції змінився, а масив залишився старим, хоча `&` я ніде не писав?
Чому після другого `foreach` останній елемент масиву продублювався?
arrays copy-on-write references foreach memory objects

Формально масив у PHP передається за значенням, але фізичного копіювання під час виклику не відбувається. Усередині масив - це структура zend_array із власним лічильником посилань. Присвоєння $b = $a, передача аргументом і повернення з функції збільшують цей лічильник, а самі дані лишаються в одному екземплярі. Механізм називають copy-on-write: PHP відкладає копію до моменту, коли хтось із власників спробує щось записати. Тоді відбувається separation - для того, хто пише, створюється власна структура, і далі дві змінні живуть незалежно.

Практичний наслідок видно в першому блоці коду. Виклик countRows($rows) на масиві зі ста тисяч елементів не додає жодного байта, бо функція тільки читає. А appendRow($rows), який дописує один елемент, коштує повного дубля масиву в памʼяті: у момент запису на дані дивилися і зовнішня змінна, і параметр. Звідси робоче правило: боятися треба не передачі великих масивів, а модифікації всередині функції. Якщо обробляти доводиться щось справді велике, вихід не в &$rows, а в тому, щоб не тримати весь набір у памʼяті: генератор із yield, курсор до бази, читання файла порціями.

Амперсанд у foreach - окрема історія, на якій ловлять найчастіше. Запис foreach ($items as &$value) робить $value посиланням на поточний елемент, і після завершення циклу це посилання не зникає: змінна далі вказує на останній елемент масиву. Наступний звичайний foreach ($items as $value) починає присвоювати в неї значення одне за одним, послідовно затираючи хвіст, і масив ['A','B','C'] тихо стає ['A','B','B']. Помилка не дає ні попередження, ні винятку. Лікується вона одним рядком unset($value) одразу після циклу. Є й глибший ефект: масив, елементи якого стали посиланнями, втрачає швидке копіювання, і при «копіюванні» такого масиву елементи-посилання лишаються спільними для обох копій.

З обʼєктами модель інша, і сформульована вона рівно однією фразою: змінна тримає не сам обʼєкт, а його ідентифікатор. Передача обʼєкта копіює цей ідентифікатор, копія вказує на той самий екземпляр, ніякого copy-on-write тут немає. Тому addItem($cart) у прикладі дописує елемент у масив, який реально побачить зовнішній код, хоча & у сигнатурі не стоїть. Межа проходить по присвоєнню самої змінної: $cart = new Cart() усередині функції зовні не видно, бо ви замінили локальну копію ідентифікатора, а не сам обʼєкт. Щоб зовнішня змінна побачила підміну, потрібен явний &$cart у параметрі.

Вибір між масивом і обʼєктом уже не про швидкість передачі: у PHP 7+ обидва варіанти коштують інкремента лічильника. Питання в тому, яку семантику ви хочете дати виклику. Масив-аргумент дає ізоляцію: функція не зіпсує вхідні дані, поки не запише в них, а якщо запише, зміну доведеться повернути результатом. Обʼєкт-аргумент дає спільний стан і мовчазні побічні ефекти, тому такі функції краще називати дієсловом (addItem, applyDiscount), а не getSomething. Якщо ж кортеж даних передається між шарами й змінюватися не має, надійніше загорнути його в readonly-клас: тоді про copy-on-write і про випадкові записи думати вже не доведеться.

declare(strict_types=1);

function countRows(array $rows): int
{
    return count($rows);            // лише читаємо: копії не буде взагалі
}

function appendRow(array $rows): array
{
    $rows[] = ['id' => 0];          // перший запис - тут народжується копія
    return $rows;
}

$rows = array_fill(0, 100_000, ['id' => 1]);

$before = memory_get_usage();
countRows($rows);
echo memory_get_usage() - $before;  // близько нуля: передали лічильник, не дані

$before = memory_get_usage();
appendRow($rows);                   // а тут памʼять стрибне на розмір масиву
echo memory_get_usage() - $before;

// foreach з & лишає $value посиланням на останній елемент
$letters = ['a', 'b', 'c'];
foreach ($letters as &$value) {
    $value = strtoupper($value);
}
unset($value);                      // без цього наступний цикл зіпсує хвіст

foreach ($letters as $value) {
    // якби не unset вище, тут вийшло б ['A', 'B', 'B']
}
print_r($letters);                  // ['A', 'B', 'C']

final class Cart
{
    /** @var list<string> */
    public array $items = [];
}

function addItem(Cart $cart): void
{
    $cart->items[] = 'книга';       // той самий обʼєкт, зміну видно назовні
}

$cart = new Cart();
addItem($cart);
echo count($cart->items);           // 1
Що «за значенням» не означає «одразу копіюється»: до першого запису функція працює з тією самою внутрішньою структурою, і виклик коштує інкремента лічильника.
Що копія (separation) виникає в момент запису, коли на масив дивиться більше ніж один власник, і саме тоді памʼять стрибає вдвічі.
Що `foreach ($arr as &$v)` робить кожен елемент посиланням, а `$v` після циклу лишається посиланням на останній елемент - звідси класичний зіпсований хвіст масиву.
Що змінна з обʼєктом тримає ідентифікатор, тому `$obj->prop = 1` усередині функції видно назовні, а `$obj = new Foo()` - ні.
Що функції на кшталт `sort()`, `shuffle()`, `array_push()`, `array_splice()` оголошені з `&` у сигнатурі, тому приймають лише змінну і змінюють її на місці.
Казати «масив передається за посиланням, бо інакше PHP копіював би мегабайти»: модель протилежна, це значення з відкладеним копіюванням.
Ставити `&$array` у сигнатурі «для швидкості»: на читанні це нічого не пришвидшує, а на записі змушує копіювати рівно так само, коли масив ще десь використовується.
Забувати `unset($value)` після `foreach ($items as &$value)` і потім не розуміти, чому наступний цикл перезаписав останній елемент.
Вважати, що `clone $obj` копіює й масиви, і вкладені обʼєкти: масиви-властивості поводяться як звичайні значення з copy-on-write, а вкладені обʼєкти лишаються спільними.
Писати `sort(array_values($data))` і отримувати фатальну помилку: аргумент за посиланням має бути змінною, а не результатом виклику.
Міряти памʼять `memory_get_usage()` після `count($rows)` і робити висновок, що великі масиви взагалі безкоштовні: копія просто відкладена до першого запису.
ПОРАДА

Формулювання, яке закриває питання: «масив - значення з copy-on-write: передача коштує інкремента лічильника, а копія народжується на першому записі; обʼєкт copy-on-write не має, бо змінна тримає його ідентифікатор». Далі одразу згадайте `unset()` після `foreach` з амперсандом: це показує, що ви на цьому вже обпікалися.

оновлено 6 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Масив - значення з copy-on-write: передача аргументом лише інкрементує лічильник власників внутрішньої структури. Реальне копіювання (separation) відбувається в момент першого запису, коли власників більше одного.