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