Czy tablice w PHP są kopiowane jako wartość lub jako odniesienie do nowych zmiennych, a po przekazaniu do funkcji?

259

1) Czy gdy tablica jest przekazywana jako argument do metody lub funkcji, jest przekazywana przez odwołanie lub wartość?

2) Czy podczas przypisywania tablicy do zmiennej nowa zmienna jest odniesieniem do oryginalnej tablicy, czy jest to nowa kopia?
Co z tym zrobić:

$a = array(1,2,3);
$b = $a;

Czy $bjest odniesienie do $a?

Szczery
źródło
Zobacz także When-does-foreach-copy
nawfal
3
@MarlonJerezIsla: wygląda na to, że tablica jest klonowana tylko wtedy, gdy zmodyfikujesz ją wewnątrz funkcji. Nadal pochodzi z innych języków, wydaje się to dziwne.
user276648,

Odpowiedzi:

276

Druga część twojego pytania znajduje się na stronie tablicy podręcznika , która stwierdza (cytowanie) :

Przypisanie tablicy zawsze obejmuje kopiowanie wartości. Użyj operatora odniesienia, aby skopiować tablicę przez odniesienie.

I podany przykład:

<?php
$arr1 = array(2, 3);
$arr2 = $arr1;
$arr2[] = 4; // $arr2 is changed,
             // $arr1 is still array(2, 3)

$arr3 = &$arr1;
$arr3[] = 4; // now $arr1 and $arr3 are the same
?>


W pierwszej części najlepszym sposobem, aby się upewnić, jest spróbować ;-)

Rozważ ten przykład kodu:

function my_func($a) {
    $a[] = 30;
}

$arr = array(10, 20);
my_func($arr);
var_dump($arr);

Daje to wynik:

array
  0 => int 10
  1 => int 20

Co oznacza, że ​​funkcja nie zmodyfikowała tablicy „zewnętrznej”, która została przekazana jako parametr: jest przekazywana jako kopia, a nie odwołanie.

Jeśli chcesz, aby przekazywana była przez referencję, musisz zmodyfikować funkcję w ten sposób:

function my_func(& $a) {
    $a[] = 30;
}

I wynik będzie:

array
  0 => int 10
  1 => int 20
  2 => int 30

Ponieważ tym razem tablica została przekazana „przez odniesienie”.


Nie wahaj się przeczytać sekcji Objaśnienia do podręcznika w podręczniku: powinien on odpowiedzieć na niektóre pytania ;-)

Pascal MARTIN
źródło
co z czymś takim jak $ a = & $ this-> a. Czy $ jest teraz odniesieniem do & this-> a?
Frank
1
Jak używasz &, tak, powinno - patrz php.net/manual/en/…
Pascal MARTIN
1
święta krowa, nie mogę uwierzyć, że to był problem, który miałem ... jeśli to powinna być lekcja, zawsze czytaj instrukcję obsługi
Heavy_Bullets
2
Cześć Pascal, znalazłem odpowiedź Kosta Kontos wydaje się bardziej dokładna. Wykonuję prosty szybki test, aby potwierdzić jego odkrycie gist.github.com/anonymous/aaf845ae354578b74906 Czy możesz również skomentować jego odkrycie?
Cheok Yan Cheng
1
To był problem, który miałem również: myślałem, że to dziwne w zagnieżdżonych tablicach, ale tak naprawdę przypisywanie tablic działa w PHP.
Jeremy List,
120

W odniesieniu do pierwszego pytania tablica jest przekazywana przez odniesienie, JEŚLI nie jest modyfikowana w wywoływanej metodzie / funkcji. Jeśli spróbujesz zmodyfikować tablicę w ramach metody / funkcji, najpierw tworzona jest jej kopia, a następnie modyfikowana jest tylko kopia. To sprawia, że ​​wydaje się, że tablica jest przekazywana przez wartość, podczas gdy w rzeczywistości tak nie jest.

Na przykład, w tym pierwszym przypadku, nawet jeśli nie definiujesz swojej funkcji, aby akceptowała $ my_array przez referencję (przez użycie znaku & w definicji parametru), nadal jest przekazywana przez referencję (tj .: nie marnujesz pamięci z niepotrzebną kopią).

function handle_array($my_array) {  

    // ... read from but do not modify $my_array
    print_r($my_array);

    // ... $my_array effectively passed by reference since no copy is made
}

Jeśli jednak zmodyfikujesz tablicę, najpierw tworzona jest jej kopia (która zużywa więcej pamięci, ale nie zmienia oryginalnej tablicy).

function handle_array($my_array) {

    // ... modify $my_array
    $my_array[] = "New value";

    // ... $my_array effectively passed by value since requires local copy
}

FYI - jest to znane jako „leniwa kopia” lub „kopiowanie przy zapisie”.

Kosta Kontos
źródło
8
To bardzo interesująca informacja! Wygląda na to, że to prawda; ale nie mogłem znaleźć żadnej oficjalnej dokumentacji potwierdzającej ten fakt. Musimy także wiedzieć, które wersje PHP obsługują tę leniwą kopię. Czy ktoś ma więcej informacji?
Mario Awad,
8
Zaktualizuj, znalazłem oficjalną dokumentację, wciąż muszę znaleźć, która wersja PHP obsługuje leniwą kopię (w instrukcji nazywają ją „kopiuj podczas zapisu”): php.net/manual/en/internals2.variables.intro.php
Mario Awad
7
Jest to wyłącznie decyzja implementacyjna maszyny wirtualnej PHP, a nie część języka - w rzeczywistości nie jest widoczna dla programisty. Kopiowanie przy zapisie jest z pewnością zalecane ze względu na wydajność, ale implementacja, która kopiuje każdą tablicę, nie ma żadnej różnicy z perspektywy programisty, więc możemy powiedzieć, że semantyka języka określa wartość pass-by-value.
Superfly
14
@ Superfly z pewnością robi różnicę, gdy chcę wiedzieć, czy mogę przekazać moją tablicę 100 MB przez stos dziesiątek funkcji bez wyczerpania pamięci! Być może masz rację, że mimo wszystko należy nazywać semantykę pass-by-value, ale pomijając takie spory dotyczące terminologii, wspomniany tutaj „szczegół implementacji” z pewnością ma znaczenie dla programistów PHP w świecie rzeczywistym.
Mark Amery
3
Jest jeszcze jedno dziwactwo, które sprawia, że ​​świadomość kopii na piśmie jest jeszcze ważniejsza, gdy myślisz o wydajności. Możesz myśleć, że przekazywanie tablic przez referencję oszczędza pamięć w porównaniu z przekazywaniem przez wartość (jeśli nie wiedziałeś o kopiowaniu przy zapisie), ale w rzeczywistości może mieć odwrotny skutek! Jeśli tablica jest następnie przekazywana przez wartość (kod własny lub kod innej firmy), PHP musi wykonać pełną kopię lub nie może już śledzić liczby referencji! Więcej tutaj: stackoverflow.com/questions/21974581/...
Dan King
80

TL; DR

a) metoda / funkcja odczytuje tylko argument tablicy => niejawne (wewnętrzne) odniesienie
b) metoda / funkcja modyfikuje argument tablicy => wartość
c) argument metody / funkcji tablicy jest wyraźnie oznaczony jako odwołanie (ze znakiem ampersand) => wyraźne odniesienie (user-land)

Lub to:
- parametr niezamienny i tablicowy : przekazany przez referencję; operacje zapisu zmieniają nową kopię tablicy, która jest tworzona przy pierwszym zapisie;
- parametr ampersand array : przekazany przez odniesienie; operacje zapisu zmieniają oryginalną tablicę.

Pamiętaj - PHP wykonuje kopiowanie wartości w momencie, gdy piszesz do parametru nie będącego znakiem ampersand. To copy-on-writeznaczy. Chciałbym pokazać Ci źródło tego zachowania, ale tam jest przerażające. Lepiej użyj xdebug_debug_zval () .

Pascal MARTIN miał rację. Kosta Kontos był jeszcze bardziej.

Odpowiedź

To zależy.

Długa wersja

Myślę, że zapisuję to dla siebie. Powinienem mieć blog czy coś ...

Ilekroć ludzie mówią o referencjach (lub wskaźnikach, jeśli o to chodzi), zwykle kończą się logomatyką (wystarczy spojrzeć na ten wątek !).
PHP jako czcigodny język, pomyślałem, że powinienem dodać do zamieszania (choć jest to streszczenie powyższych odpowiedzi). Ponieważ chociaż dwie osoby mogą mieć rację w tym samym czasie, lepiej po prostu połamać sobie głowy w jedną odpowiedź.

Po pierwsze, powinieneś wiedzieć, że nie jesteś pedantem, jeśli nie odpowiadasz w czarno-biały sposób . Sprawy są bardziej skomplikowane niż „tak / nie”.

Jak zobaczysz, cała rzecz według wartości / referencji jest bardzo związana z tym, co dokładnie robisz z tą tablicą w zakresie metody / funkcji: czy ją czy modyfikujesz?

Co mówi PHP? (alias „zmiany”)

Instrukcja mówi ten (mój nacisk):

Domyślnie argumenty funkcji są przekazywane przez wartość (tak, że jeśli wartość argumentu w funkcji zostanie zmieniona , nie zostanie zmieniona poza funkcją). Aby umożliwić funkcji modyfikowanie jej argumentów, muszą zostać przekazane przez odwołanie .

Aby argument funkcji był zawsze przekazywany przez odwołanie, wstaw znak ampersand (&) do nazwy argumentu w definicji funkcji

O ile mi wiadomo, kiedy duzi, poważni, uczciwi programiści mówią o referencjach, zwykle mówią o zmianie wartości tych referencji . I to jest dokładnie to, co mówi o ręcznych: hey, if you want to CHANGE the value in a function, consider that PHP's doing "pass-by-value".

Jest jednak inny przypadek, o którym nie wspominają: co jeśli nic nie zmienię - po prostu przeczytaj?
Co się stanie, jeśli przekażesz tablicę metodzie, która nie oznacza jawnie odwołania, a my nie zmienimy tej tablicy w zakresie funkcji? Na przykład:

<?php
function readAndDoStuffWithAnArray($array) 
{
    return $array[0] + $array[1] + $array[2];
}

$x = array(1, 2, 3);

echo readAndDoStuffWithAnArray($x);

Czytaj dalej, mój towarzyszu podróży.

Co właściwie robi PHP? (aka „pod względem pamięci”)

Ci sami duzi i poważni programiści, kiedy stają się jeszcze poważniejsi, mówią o „optymalizacjach pamięci” w odniesieniu do referencji. Podobnie PHP. Ponieważ PHP is a dynamic, loosely typed language, that uses copy-on-write and reference countingwłaśnie dlatego .

Nie byłoby idealne przekazywanie OGROMNYCH tablic do różnych funkcji, a PHP tworzenie ich kopii (w końcu to właśnie robi funkcja „pass-by-value”):

<?php

// filling an array with 10000 elements of int 1
// let's say it grabs 3 mb from your RAM
$x = array_fill(0, 10000, 1); 

// pass by value, right? RIGHT?
function readArray($arr) { // <-- a new symbol (variable) gets created here
    echo count($arr); // let's just read the array
}

readArray($x);

Cóż, teraz, gdyby to była wartość przekazywana, nie byłoby już około 3 MB pamięci RAM, ponieważ istnieją dwie kopie tej tablicy, prawda?

Źle. Dopóki nie zmienimy $arrzmiennej, jest to odniesienie pod względem pamięci . Po prostu tego nie widzisz. Właśnie dlatego PHP wspomina o odniesieniach do ziemi użytkownika podczas &$someVarrozróżniania wewnętrznych i jawnych (ze znakami ampersand).

Fakty

Więc, when an array is passed as an argument to a method or function is it passed by reference?

Wymyśliłem trzy (tak, trzy) przypadki:
a) metoda / funkcja odczytuje tylko argument tablicy;
b) metoda / funkcja modyfikuje argument tablicy;
c) argument metody / funkcji tablicy jest wyraźnie oznaczony jako odwołanie (za pomocą ampersand)


Po pierwsze, zobaczmy, ile pamięci faktycznie zjada tablica (uruchom tutaj ):

<?php
$start_memory = memory_get_usage();
$x = array_fill(0, 10000, 1);
echo memory_get_usage() - $start_memory; // 1331840

Tyle bajtów. Wspaniały.

a) metoda / funkcja odczytuje tylko argument tablicy

Stwórzmy teraz funkcję, która odczytuje tylko tę tablicę jako argument i zobaczymy, ile pamięci zajmuje logika odczytu:

<?php

function printUsedMemory($arr) 
{
    $start_memory = memory_get_usage();

    count($arr);       // read
    $x = $arr[0];      // read (+ minor assignment)
    $arr[0] - $arr[1]; // read

    echo memory_get_usage() - $start_memory; // let's see the memory used whilst reading
}

$x = array_fill(0, 10000, 1); // this is 1331840 bytes
printUsedMemory($x);

Chcesz zgadnąć? Dostaję 80! Przekonaj się sam . Jest to część, którą omija podręcznik PHP. Jeśli $arrparametr rzeczywiście był przekazywany przez wartość, zobaczysz coś podobnego do 1331840bajtów. Wygląda na to, że $arrzachowuje się jak odniesienie, prawda? To dlatego, że jest to odniesienie - wewnętrzne.

b) metoda / funkcja modyfikuje argument tablicy

Teraz, niech pisze na tym param, zamiast czytać z niego:

<?php

function printUsedMemory($arr)
{
    $start_memory = memory_get_usage();

    $arr[0] = 1; // WRITE!

    echo memory_get_usage() - $start_memory; // let's see the memory used whilst reading
}

$x = array_fill(0, 10000, 1);
printUsedMemory($x);

Ponownie, przekonaj się sam , ale dla mnie jest to bardzo bliskie bycia 1331840. W tym przypadku tablica jest kopiowana $arr.

c) argument tablicy metod / funkcji jest wyraźnie oznaczony jako odwołanie (ze znakiem ampersand)

Zobaczmy teraz, ile pamięci zajmuje operacja zapisu do jawnego odwołania (uruchom tutaj ) - zwróć uwagę na znak ampersand w podpisie funkcji:

<?php

function printUsedMemory(&$arr) // <----- explicit, user-land, pass-by-reference
{
    $start_memory = memory_get_usage();

    $arr[0] = 1; // WRITE!

    echo memory_get_usage() - $start_memory; // let's see the memory used whilst reading
}

$x = array_fill(0, 10000, 1);
printUsedMemory($x);

Założę się, że otrzymasz maksymalnie 200! Więc zjada to w przybliżeniu tyle samo pamięci, co odczyt z paramperów i ampers .

nevvermind
źródło
Zaoszczędził mi kilka godzin na debugowaniu wycieku pamięci!
Ragen Dazs
2
Kosta Kontos: To jest tak ważne pytanie, że powinieneś zaznaczyć to jako zaakceptowaną odpowiedź. To powiedziawszy, @nevvermind: Świetny esej, ale proszę załączyć górną sekcję TL; DR.
AVIDeveloper
1
@nevvermind: Nie jestem akronimem, główna różnica polega na tym, że wnioski pojawiają się zwykle na końcu artykułu, a TL; DR pojawia się jako pierwsza linia dla tych, którzy potrzebują krótkiej odpowiedzi zamiast długiej analizy . Twoje badania są dobre i to nie jest krytyka, tylko moje 00,02 USD.
AVIDeveloper,
1
Masz rację. Konkluzje umieściłem na górze. Ale nadal chciałbym, żeby ludzie przestali być leniwi w czytaniu całości, zanim dojdą do jakichkolwiek wniosków . Przewijanie jest dla nas zbyt łatwe, aby zawracać sobie głowę zmienianiem kolejności rzeczy.
nevvermind
1
Wydaje mi się, że PHP stało się bardziej wydajne lata później, ponieważ twoje przykłady kodów są znacznie niższe. :)
drzaus
14

Domyślnie

  1. Prymitywy są przekazywane według wartości. Jest mało prawdopodobne w Javie, łańcuch jest prymitywny w PHP
  2. Tablice prymitywów są przekazywane według wartości
  3. Obiekty są przekazywane przez odniesienie
  4. Tablice obiektów są przekazywane przez wartość (tablicę), ale każdy obiekt jest przekazywany przez odwołanie.

    <?php
    $obj=new stdClass();
    $obj->field='world';
    
    $original=array($obj);
    
    
    function example($hello) {
        $hello[0]->field='mundo'; // change will be applied in $original
        $hello[1]=new stdClass(); // change will not be applied in $original
        $
    }
    
    example($original);
    
    var_dump($original);
    // array(1) { [0]=> object(stdClass)#1 (1) { ["field"]=> string(5) "mundo" } } 

Uwaga: W ramach optymalizacji każda pojedyncza wartość jest przekazywana jako odniesienie, dopóki nie zostanie zmodyfikowana w funkcji. Jeśli zostanie zmodyfikowany, a wartość zostanie przekazana przez odwołanie, zostanie skopiowana, a kopia zmodyfikowana.

magallanes
źródło
4
Ta odpowiedź powinna być +1 na górze. Zawiera niejasną gotcha, o której inne odpowiedzi nie wspominają: „4 - Tablice obiektów są przekazywane przez wartość (tablicę), ale każdy obiekt jest przekazywany przez odniesienie”. Z tego powodu drapałem się po głowie!
sierpień
@magallanes great powinien być również oceniany jako pierwszy, wyjaśnisz mi problem z tablicą obiektów, którą miałem. Czy jest jakiś sposób zmodyfikowania obiektu w tablicy tylko w jednej z dwóch zmiennych tablicowych (oryginalnej i kopii)?
fede72bari
5

Kiedy tablica jest przekazywana do metody lub funkcji w PHP, jest przekazywana przez wartość, chyba że jawnie przekażesz ją przez referencję, na przykład:

function test(&$array) {
    $array['new'] = 'hey';
}

$a = $array(1,2,3);
// prints [0=>1,1=>2,2=>3]
var_dump($a);
test($a);
// prints [0=>1,1=>2,2=>3,'new'=>'hey']
var_dump($a);

W drugim pytaniu $bnie chodzi o odniesienie $a, ale o kopię $a.

Podobnie jak w pierwszym przykładzie, możesz odwoływać $asię, wykonując następujące czynności:

$a = array(1,2,3);
$b = &$a;
// prints [0=>1,1=>2,2=>3]
var_dump($b);
$b['new'] = 'hey';
// prints [0=>1,1=>2,2=>3,'new'=>'hey']
var_dump($a);
Corey Ballou
źródło
1

Ten wątek jest nieco starszy, ale tutaj właśnie się natknąłem:

Wypróbuj ten kod:

$date = new DateTime();
$arr = ['date' => $date];

echo $date->format('Ymd') . '<br>';
mytest($arr);
echo $date->format('Ymd') . '<br>';

function mytest($params = []) {
    if (isset($params['date'])) {
        $params['date']->add(new DateInterval('P1D'));
    }
}

http://codepad.viper-7.com/gwPYMw

Uwaga: parametr $ params nie ma wzmacniacza, a mimo to zmienia wartość $ arr [„data”]. To tak naprawdę nie pasuje do wszystkich innych wyjaśnień tutaj i tego, co myślałem do tej pory.

Jeśli sklonuję obiekt $ params ['data'], druga wygenerowana data pozostanie taka sama. Jeśli po prostu ustawię go na ciąg znaków, nie wpłynie to również na wynik.

robbash
źródło
3
Tablica jest kopiowana, ale nie jest to głęboka kopia. Oznacza to, że prymitywne wartości, takie jak liczby i łańcuchy, są kopiowane do $ param, ale w przypadku obiektów odwołanie jest kopiowane zamiast sklonowanego obiektu. $ arr zawiera odniesienie do $ date, podobnie jak skopiowana tablica $ params. Więc kiedy wywołujesz funkcję na $ params ['date'], która zmienia jej wartość, zmieniasz również $ arr ['date'] i $ date. Kiedy ustawisz $ params ['date'] na ciąg znaków, po prostu zamieniasz odniesienie $ params na $ date na coś innego.
ejegg
1

Aby rozszerzyć jedną z odpowiedzi, również podstrony tablic wielowymiarowych są przekazywane przez wartość, chyba że są przekazywane jawnie przez odwołanie.

<?php
$foo = array( array(1,2,3), 22, 33);

function hello($fooarg) {
  $fooarg[0][0] = 99;
}

function world(&$fooarg) {
  $fooarg[0][0] = 66;
}

hello($foo);
var_dump($foo); // (original array not modified) array passed-by-value

world($foo);
var_dump($foo); // (original array modified) array passed-by-reference

Wynik to:

array(3) {
  [0]=>
  array(3) {
    [0]=>
    int(1)
    [1]=>
    int(2)
    [2]=>
    int(3)
  }
  [1]=>
  int(22)
  [2]=>
  int(33)
}
array(3) {
  [0]=>
  array(3) {
    [0]=>
    int(66)
    [1]=>
    int(2)
    [2]=>
    int(3)
  }
  [1]=>
  int(22)
  [2]=>
  int(33)
}
K.Karamazen
źródło
0

W PHP tablice są domyślnie przekazywane do funkcji domyślnie, chyba że jawnie przekazujesz je przez odwołanie, jak pokazuje następujący fragment:

$foo = array(11, 22, 33);

function hello($fooarg) {
  $fooarg[0] = 99;
}

function world(&$fooarg) {
  $fooarg[0] = 66;
}

hello($foo);
var_dump($foo); // (original array not modified) array passed-by-value

world($foo);
var_dump($foo); // (original array modified) array passed-by-reference

Oto wynik:

array(3) {
  [0]=>
  int(11)
  [1]=>
  int(22)
  [2]=>
  int(33)
}
array(3) {
  [0]=>
  int(66)
  [1]=>
  int(22)
  [2]=>
  int(33)
}
John Sonderson
źródło