Mid📖Теория8 min

Ссылки

Присваивание по ссылке, передача по ссылке, подводные камни и copy-on-write

Ссылки в PHP

Ссылки в PHP -- это не указатели (как в C/C++). Ссылка -- это алиас (псевдоним) для переменной. Две переменные-ссылки указывают на одно и то же значение, и изменение одной отражается на другой.

Что такое ссылки

<?php
declare(strict_types=1);

$a = 'Hello';
$b = &$a; // $b is now an alias for $a

echo $a; // Hello
echo $b; // Hello

$b = 'World';
echo $a; // World — $a changed because $b is an alias!
echo $b; // World

Ссылки -- это не указатели на память. Вы не можете получить адрес переменной или выполнить арифметику указателей. PHP-ссылки просто делают две переменные синонимами одного значения.

Как это работает внутри

PHP использует внутренний механизм zval (Zend Value). Когда создаётся ссылка, обе переменные начинают указывать на один zval с установленным флагом IS_REFERENCE:

<?php
declare(strict_types=1);

$a = 42;
// Internally: $a -> zval { value: 42, refcount: 1 }

$b = &$a;
// Internally: $a -> reference { zval { value: 42 } }
//             $b -> reference { zval { value: 42 } }  (same reference wrapper)

$c = $a;
// $c gets a COPY of the value (42), not the reference
// $c -> zval { value: 42, refcount: 1 }

Присваивание по ссылке

Оператор =& создаёт ссылочную связь между переменными.

<?php
declare(strict_types=1);

// Basic reference assignment
$original = 'original value';
$reference = &$original;

$reference = 'modified';
echo $original; // modified

// Chain of references
$a = 1;
$b = &$a;
$c = &$b; // $c is also a reference to the same value as $a and $b

$c = 100;
echo $a; // 100
echo $b; // 100
echo $c; // 100

Ссылка НЕ копируется при обычном присваивании

<?php
declare(strict_types=1);

$a = 'hello';
$b = &$a;
$c = $b; // $c gets a COPY of $b's value, NOT a reference!

$c = 'changed';
echo $a; // hello — NOT changed, because $c is not a reference
echo $b; // hello
echo $c; // changed

Важно: $c = $b копирует значение $b, даже если $b является ссылкой. Для создания ссылки нужен явный &.

Передача по ссылке

Параметры функции можно передавать по ссылке, используя & в сигнатуре функции.

<?php
declare(strict_types=1);

// Pass by reference — modifies the original variable
function increment(int &$value): void
{
    $value++;
}

$counter = 0;
increment($counter);
echo $counter; // 1

increment($counter);
echo $counter; // 2

Практический пример: swap

<?php
declare(strict_types=1);

function swap(mixed &$a, mixed &$b): void
{
    $temp = $a;
    $a = $b;
    $b = $temp;
}

$x = 'first';
$y = 'second';
swap($x, $y);

echo $x; // second
echo $y; // first

Передача по ссылке с типами

<?php
declare(strict_types=1);

// Reference parameter with type hint
function doubleValues(array &$items): void
{
    foreach ($items as &$item) {
        $item *= 2;
    }
    unset($item); // CRITICAL: unset reference after foreach!
}

$numbers = [1, 2, 3, 4, 5];
doubleValues($numbers);
print_r($numbers); // [2, 4, 6, 8, 10]

Нельзя передать литерал по ссылке

<?php
declare(strict_types=1);

function modify(int &$value): void
{
    $value = 42;
}

// WRONG — cannot pass literal by reference
// modify(10); // Fatal error: Cannot pass parameter by reference

// CORRECT — pass variable
$val = 10;
modify($val);
echo $val; // 42

Возврат по ссылке

Функция может возвращать ссылку на переменную. Это используется редко, но иногда необходимо для реализации паттернов вроде fluent interface или доступа к внутренним данным.

<?php
declare(strict_types=1);

class Config
{
    private array $data = [
        'debug' => false,
        'cache' => true,
    ];

    // Return reference to internal data
    public function &get(string $key): mixed
    {
        if (!array_key_exists($key, $this->data)) {
            throw new InvalidArgumentException("Key not found: {$key}");
        }

        return $this->data[$key];
    }
}

$config = new Config();

// Get reference to internal value
$debug = &$config->get('debug');
$debug = true; // Modifies the internal data!

echo var_export($config->get('debug'), true); // true

Предупреждение: Возврат по ссылке нарушает инкапсуляцию -- внешний код может изменить внутреннее состояние объекта. Используйте крайне осторожно.

Синтаксис возврата по ссылке

<?php
declare(strict_types=1);

// Both & in function declaration AND & at call site
function &getGlobalCounter(): int
{
    static $counter = 0;
    return $counter;
}

// Must use =& to capture reference
$ref = &getGlobalCounter();
$ref++;
$ref++;
$ref++;

echo getGlobalCounter(); // 3

Ссылки и unset

unset() удаляет только алиас (имя переменной), а не само значение.

<?php
declare(strict_types=1);

$a = 'hello';
$b = &$a;

unset($a);
// $a is destroyed (the name is removed)
// But the VALUE still exists because $b references it

echo isset($a) ? 'yes' : 'no'; // no — $a no longer exists
echo $b; // hello — value is still alive through $b

Цепочка ссылок и unset

<?php
declare(strict_types=1);

$a = 'value';
$b = &$a;
$c = &$a;

// Three aliases for the same value: $a, $b, $c

unset($b);
// Only $b alias is removed. $a and $c still reference the value.

echo $a; // value
echo $c; // value

unset($a);
unset($c);
// Now value has no references and will be garbage collected

Ссылки в foreach -- ОПАСНОСТИ!

Это один из самых коварных подводных камней PHP. Если вы используете & в foreach и забываете сделать unset, последняя итерация оставляет ссылку живой.

Классический баг

<?php
declare(strict_types=1);

$items = ['a', 'b', 'c'];

// Iterate by reference
foreach ($items as &$item) {
    $item = strtoupper($item);
}
// After loop, $item is still a REFERENCE to $items[2]!

// Now a regular foreach:
foreach ($items as $item) {
    // Each iteration assigns to $item, which is still a reference to $items[2]!
    echo implode(', ', $items) . PHP_EOL;
}
// A, B, A    ← $items[2] became 'A' (value of $items[0])
// A, B, B    ← $items[2] became 'B' (value of $items[1])
// A, B, B    ← $items[2] became 'B' (value of $items[2], which is now 'B')

// RESULT: ['A', 'B', 'B'] instead of expected ['A', 'B', 'C']!

Правильное решение: ВСЕГДА unset после foreach по ссылке

<?php
declare(strict_types=1);

$items = ['a', 'b', 'c'];

foreach ($items as &$item) {
    $item = strtoupper($item);
}
unset($item); // CRITICAL! Break the reference

// Now safe to use $item again
foreach ($items as $item) {
    echo $item . ' '; // A B C — correct!
}

Золотое правило: После foreach ($arr as &$val) ВСЕГДА пишите unset($val) на следующей строке. Без исключений.

Ссылки и global

Ключевое слово global создаёт ссылку на глобальную переменную.

<?php
declare(strict_types=1);

$counter = 0;

function incrementGlobal(): void
{
    global $counter; // $counter inside function is a reference to global $counter
    $counter++;
}

incrementGlobal();
incrementGlobal();
echo $counter; // 2

// Same as:
function incrementGlobalExplicit(): void
{
    $GLOBALS['counter']++;
}

Подводный камень: unset глобальной ссылки

<?php
declare(strict_types=1);

$value = 'original';

function tryUnset(): void
{
    global $value;
    unset($value); // Only unsets the LOCAL alias, NOT the global variable!
}

tryUnset();
echo $value; // original — still exists!

Ссылки и $this

$this в PHP нельзя присвоить по ссылке. Это зарезервированная переменная.

<?php
declare(strict_types=1);

class Example
{
    public function test(): void
    {
        // $ref = &$this; // Fatal error: Cannot re-assign $this
        // $this = new self(); // Fatal error: Cannot re-assign $this
    }
}

Ссылки в массивах

Элементы массива могут быть ссылками на другие переменные.

<?php
declare(strict_types=1);

$name = 'Alice';
$data = [
    'name' => &$name,
    'static' => 'value',
];

$name = 'Bob';
echo $data['name']; // Bob — changed because it's a reference

// Creating references between array elements
$arr = [1, 2, 3];
$arr[3] = &$arr[0]; // $arr[3] is an alias for $arr[0]

$arr[3] = 100;
echo $arr[0]; // 100

Копирование массива с ссылками

<?php
declare(strict_types=1);

$original = ['a', 'b', 'c'];
$ref = &$original[1]; // $ref is alias for $original[1]

// Copy the array
$copy = $original;

// The reference is carried into the copy!
$ref = 'MODIFIED';
echo $original[1]; // MODIFIED
echo $copy[1];     // MODIFIED — because the reference was copied!

// To break all references, use array_values() or serialize/unserialize
$cleanCopy = array_values($original);
// Or: $cleanCopy = unserialize(serialize($original));

Важно: При копировании массива ($copy = $original) внутренние ссылки сохраняются. Если элемент массива был ссылкой, он останется ссылкой и в копии.

Copy-on-Write vs ссылки

PHP использует механизм Copy-on-Write (COW) -- данные копируются только тогда, когда одна из переменных изменяется. Ссылки ломают COW.

Copy-on-Write (без ссылок)

<?php
declare(strict_types=1);

$a = str_repeat('x', 1_000_000); // 1MB string
$b = $a; // NO copy! $a and $b share the same memory (COW)

// Memory: ~1MB (shared)
echo memory_get_usage() . PHP_EOL;

$b .= 'y'; // NOW a copy is made because $b was modified
// Memory: ~2MB (two separate copies)
echo memory_get_usage() . PHP_EOL;

Ссылки ломают COW

<?php
declare(strict_types=1);

$a = str_repeat('x', 1_000_000);
$b = &$a; // Reference — COW is DISABLED for this value

// Any third variable now MUST copy:
$c = $a; // FORCED copy because $a is part of a reference group!

// With reference: $c = $a copies immediately
// Without reference: $c = $a would share memory (COW)

Бенчмарк: ссылки vs copy-on-write

<?php
declare(strict_types=1);

// COW: passing large array WITHOUT reference — NO copy if not modified
function readOnly(array $data): int
{
    return count($data); // No modification = no copy
}

// Reference: passing by reference — prevents COW optimization
function readOnlyRef(array &$data): int
{
    return count($data); // Reference prevents COW
}

$bigArray = range(1, 100_000);

// Both are fast, but COW version is MORE memory efficient
// because PHP doesn't need to track reference semantics
$result1 = readOnly($bigArray);
$result2 = readOnlyRef($bigArray);

Запомни: Передача по ссылке НЕ является оптимизацией в PHP! Благодаря COW, передача по значению больших массивов/строк обходится бесплатно, если функция их не модифицирует. Ссылки наоборот мешают COW и могут увеличить потребление памяти.

Когда использовать ссылки

Хорошие случаи для использования ссылок

<?php
declare(strict_types=1);

// 1. Swap values
function swap(mixed &$a, mixed &$b): void
{
    [$a, $b] = [$b, $a];
}

// 2. Modify array in-place (e.g., deep normalization)
function normalizeTree(array &$tree): void
{
    foreach ($tree as &$node) {
        if (is_string($node)) {
            $node = trim(strtolower($node));
        } elseif (is_array($node)) {
            normalizeTree($node); // Recursive modification
        }
    }
    unset($node);
}

// 3. preg_match and similar functions (language design)
$text = 'Hello World 2025';
if (preg_match('/(\d{4})/', $text, $matches)) {
    echo $matches[1]; // 2025 — $matches filled by reference
}

// 4. array_walk with modification
$prices = [10.0, 20.0, 30.0];
array_walk($prices, function (float &$price): void {
    $price *= 1.10; // Add 10% tax
});
// $prices = [11.0, 22.0, 33.0]

Когда НЕ использовать ссылки

<?php
declare(strict_types=1);

// BAD: "optimization" — COW handles this better
function processData(array &$data): array  // Reference is WRONG here
{
    return array_map(fn($item) => $item * 2, $data);
}

// GOOD: Pass by value — COW prevents copying
function processDataCorrect(array $data): array
{
    return array_map(fn($item) => $item * 2, $data);
}

// BAD: Returning reference to avoid "copy" — unnecessary
function &getItems(): array  // Don't do this
{
    static $items = [1, 2, 3];
    return $items;
}

// BAD: Reference to scalar for "performance" — no gain
function calculate(float &$value): void  // Pointless reference
{
    $value = sqrt($value);
}

Типичные баги и подводные камни

Баг 1: Забытый unset после foreach

<?php
declare(strict_types=1);

$users = ['Alice', 'Bob', 'Charlie'];

foreach ($users as &$user) {
    $user = strtoupper($user);
}
// BUG: forgot unset($user)

$user = 'Diana'; // Overwrites $users[2]!
echo $users[2];  // Diana (was CHARLIE)

Баг 2: Ссылка на несуществующий элемент массива

<?php
declare(strict_types=1);

$data = [];

$ref = &$data['key']; // Creates $data['key'] with null value!
var_dump($data); // ['key' => null] — element was auto-created

// This is actually used intentionally sometimes:
$nested = [];
$ref = &$nested['a']['b']['c']; // Creates entire nested structure!
$ref = 'value';
print_r($nested); // ['a' => ['b' => ['c' => 'value']]]

Баг 3: Ссылка в массиве при сериализации

<?php
declare(strict_types=1);

$a = 'hello';
$arr = [&$a, &$a]; // Both elements reference same value

$json = json_encode($arr);
echo $json; // ["hello","hello"] — reference information is LOST

$decoded = json_decode($json, true);
$decoded[0] = 'modified';
echo $decoded[1]; // hello — no longer a reference!

Баг 4: Ссылка в вызове по значению

<?php
declare(strict_types=1);

function noRef(string $value): string
{
    return strtoupper($value);
}

$name = 'alice';
$ref = &$name;

// Calling noRef() with a reference variable — value is passed (COW copy)
$result = noRef($name);
echo $name; // alice — not modified
echo $result; // ALICE

Баг 5: Ссылки и list()/array destructuring

<?php
declare(strict_types=1);

$data = [1, 2, 3];

// Reference in destructuring (PHP 7.3+)
[&$first, &$second, &$third] = $data;

$first = 100;
echo $data[0]; // 100 — modified through reference!

// Be careful with this pattern
unset($first, $second, $third);

Итоги

Операция Синтаксис Когда использовать
Присваивание по ссылке $b = &$a Создание алиаса
Передача по ссылке function f(&$x) Модификация аргумента in-place
Возврат по ссылке function &f() Крайне редко (доступ к внутренним данным)
Unset ссылки unset($ref) Удаление алиаса (не значения)

Правила:

  1. По умолчанию не используйте ссылки -- COW делает передачу по значению эффективной
  2. Всегда unset() после foreach по ссылке
  3. Ссылки -- не указатели и не дают прироста производительности для чтения
  4. Ссылки ломают COW -- могут увеличить потребление памяти
  5. Используйте ссылки, когда нужно модифицировать переменную вызывающего кода

Запомни: PHP-ссылки -- это алиасы, а не указатели. Они НЕ ускоряют передачу данных (Copy-on-Write делает это лучше). Используй ссылки только когда нужно модифицировать значение in-place. ВСЕГДА делай unset() после foreach по ссылке -- это самый частый источник багов.


Проверь себя

5 из 12
🧪

Что произойдёт при копировании массива, содержащего ссылку: `$copy = $original;`?

🧪

Почему передача по ссылке НЕ является оптимизацией в PHP?

🧪

Как правильно разорвать все ссылки при копировании массива?

🧪

Что произойдёт: `$data = []; $ref = &$data['key'];`?

🧪

Что такое ссылка в PHP?