PHP中的数组在赋值或传递给函数时,是按值还是按引用复制的?

314

1)当将数组作为参数传递给一个方法或函数时,它是按引用传递还是按值传递?

2)将数组分配给一个变量时,新变量是指向原始数组的引用,还是一个新的副本?
进行以下操作呢:

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

$b 是不是指向 $a 的引用?


另请参阅When-does-foreach-copy - nawfal
5
看起来只有在函数内部修改数组时,数组才会被克隆。但对于其他语言的人来说,这似乎很奇怪。 - user276648
8个回答

336

关于第二个问题,请参见手册中的数组页面,其中指出:(引用)

数组赋值总是涉及值复制。 使用引用操作符通过引用复制数组。

给出了以下示例:

<?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
?>


针对第一个问题,最好的方法是尝试一下;-)

考虑下面这个代码示例:

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

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

它将会给出以下输出:

array
  0 => int 10
  1 => int 20

这表明函数没有修改作为参数传递的“外部”数组:它是作为副本而不是引用传递的。

如果您想按引用传递它,您将需要修改函数,像这样:

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

然后输出将变为:

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

因为这次数组是“按引用传递”的,所以……


不要犹豫阅读手册中的引用解释部分:它应该可以回答你的一些问题;-)


像 $a = &$this->a 这样的东西怎么样?现在 $a 是对 &this->a 的引用吗? - Frank
1
由于您正在使用,所以应该 - 请参见http://php.net/manual/en/language.references.whatdo.php#language.references.whatdo.assign - Pascal MARTIN
1
天哪,我简直不敢相信这就是我的问题所在... 这应该是一个教训,永远要阅读官方手册。 - Heavy_Bullets
2
嗨,Pascal,我发现Kosta Kontos的答案似乎更准确。我进行了一个简单的快速测试来确认他的发现https://gist.github.com/anonymous/aaf845ae354578b74906 你也能对他的发现发表评论吗? - Cheok Yan Cheng
1
这也是我遇到的问题:我以为是关于嵌套数组的一些奇怪问题,但实际上只是 PHP 中数组赋值的工作方式。 - Jeremy List

158

关于您的第一个问题,如果在调用的方法/函数内修改数组,则会传递引用。如果尝试在该方法/函数内修改数组,则首先会创建副本,然后只修改副本。这使得数组似乎是按值传递,但实际上不是。

例如,在此第一种情况下,即使您没有定义函数通过引用接受$my_array(通过在参数定义中使用&字符),它仍然会被传递引用(即:您不会浪费内存进行不必要的复制)。

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
}

然而,如果您修改数组,则首先会制作其副本(这将使用更多的内存,但不会影响您的原始数组)。

function handle_array($my_array) {

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

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

顺便提一下 - 这被称为“懒复制”或“写时复制”。


9
这是一条非常有趣的信息!看起来它是真实的,但我找不到任何官方文档支持这个事实。我们还需要知道哪些版本的PHP支持这种惰性复制的概念。有人有更多信息吗? - Mario Awad
8
更新:找到一些官方文档,仍需确定哪个版本的PHP支持惰性复制(在手册中称为“写时复制”):http://php.net/manual/en/internals2.variables.intro.php - Mario Awad
14
这纯粹是PHP虚拟机的一个实现决定,而不是语言的一部分 - 程序员实际上看不到它。出于性能原因,复制时写是明确建议的,但一个每次都复制数组的实现在程序员的角度来看与传值相同,因此我们可以说语言语义规定了按值传递。 - Superfly
26
@Superfly,当我想知道我是否可以通过数十个函数的堆栈而不会耗尽内存来传递我的100MB数组时,这肯定是有所区别的!你可能是对的,仍然称其为传值语义是正确的,但撇开这些术语上的争论,这里提到的“实现细节”确实关系到PHP程序员在现实世界中的工作。 - Mark Amery
3
这里还有一个小技巧,让我们对写时复制(copy-on-write)的了解更加重要。如果你不知道写时复制,你可能会认为通过引用传递数组比按值传递可以节省内存,但实际上这会产生相反的效果!如果数组随后被自己或第三方代码按值传递,PHP就必须进行完全复制,否则它将无法跟踪引用计数!更多信息:https://stackoverflow.com/questions/21974581/why-are-php-references-a-bad-idea-how-do-zvals-and-copy-on-write-come-into-it - Dan King
显示剩余4条评论

101

简而言之

a) 方法/函数仅读取数组参数 => 隐式(内部)引用
b) 方法/函数修改数组参数 =>
c) 方法/函数数组参数明确标记为引用(带有&符号) => 显式(用户级)引用

或者这样:
- 非&符号数组参数: 通过引用传递; 写入操作会更改新的数组副本,该副本在第一次写入时创建;
- &符号数组参数: 通过引用传递; 写入操作会更改原始数组。

请记住 - PHP在您写入非&符号数组参数时立即进行值复制。这就是copy-on-write的含义。我很想向您展示此行为的C源代码,但里面太可怕了。最好使用xdebug_debug_zval()

Pascal MARTIN是正确的。Kosta Kontos更加正确。

答案

这要看情况。

详细版

我想我是在为自己写下这些。我应该有一个博客之类的东西...

每当人们谈论引用(或指针),他们通常会陷入争辩中(只需看看这个thread!)。
PHP是一种古老的语言,我想我应该增加混乱(尽管这是上述答案的摘要)。因为,尽管两个人可能同时正确,但最好将他们的头合并成一个答案。

首先,你应该知道如果你不以黑白方式回答,你就不是一个学究。事情比“是/否”更加复杂。

正如你将看到的,整个按值/按引用的问题与你在方法/函数范围内对该数组做了什么密切相关:读取它还是修改它?

PHP说什么?(也称“变化方面”)

手册中这样说(重点是我的):

默认情况下,函数参数是按值传递的(因此,如果函数内部参数的值更改,它不会在函数外部更改)。要允许函数修改其参数,它们必须通过引用传递。

要求函数始终通过引用传递参数,请在函数定义中的参数名称前加上&符号。

据我所知,当大型、严肃、诚实可靠的程序员谈论引用时,他们通常谈论更改该引用的值。这正是手册所讲述的:嘿,如果你想在函数中改变一个值,请考虑PHP正在执行“按值传递”

虽然他们没有提到另一种情况: 如果我不做出任何更改 - 只是阅读呢?
如果您将一个数组传递给一个没有明确标记引用的方法,并且我们在函数范围内不更改该数组,会发生什么?例如:

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

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

echo readAndDoStuffWithAnArray($x);

请继续阅读,我的旅行者同伴。

PHP 到底是做什么的?(也就是“内存方面”)

同样那些专业的程序员们,当他们变得更加严谨时,他们会谈论关于引用的“内存优化”。PHP 也是如此。因为PHP 是一种动态、弱类型语言,使用写时复制和引用计数,这就是为什么

将巨大的数组传递给各种函数,并让 PHP 对它们进行复制(毕竟这就是“按值传递”的作用),这并不理想:

<?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);

好的,如果这个实际上是按值传递,那么我们将会失去3mb+的RAM,因为有两个副本的数组,对吧?

错了,只要我们不改变$ arr变量,那就是一个引用,从内存角度来看。你只是看不到它。这就是为什么PHP在谈论&$someVar时mentions用户空间引用,以区分内部和显式(带有&符号)引用。

事实

所以,当一个数组作为参数传递给方法或函数时,它是按引用传递的吗?

我想到了三种情况:
a) 方法/函数仅读取数组参数
b) 方法/函数修改数组参数
c) 方法/函数数组参数被明确标记为引用(使用&符号)


首先,让我们看一下这个数组实际占用了多少内存(在此处运行):
<?php
$start_memory = memory_get_usage();
$x = array_fill(0, 10000, 1);
echo memory_get_usage() - $start_memory; // 1331840

那么多字节。太好了。

a)该方法/函数仅读取数组参数

现在让我们编写一个函数,它将数组作为参数仅读取,然后我们将看到读取逻辑所占用的内存量:

<?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);

我猜你能猜到吗?我得到了80!看看自己。这是PHP手册遗漏的部分。如果$arr参数实际上是按值传递的,你会看到类似于1331840字节的东西。看起来$arr的行为像一个引用,不是吗?那是因为它是一个内部引用。

b)该方法/函数修改了数组参数

现在,让我们写入该参数,而不是从中读取:

<?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);

再次,亲自查看,但对我来说,那已经非常接近1331840了。因此,在这种情况下,数组实际上被复制到$arr中。

c) 方法/函数的数组参数被明确标记为引用(使用&符号)

现在让我们看一下显式引用的写操作需要多少内存(在此处运行)-请注意函数签名中的&:

<?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);

我猜你最多只能得到200!因此,这大约占用了与从非&参数读取相同的内存。

1
我猜PHP现在更加高效,因为你的codepad示例给出了更低的数字 :) - drzaus

25

默认情况下:

  1. 基本数据类型按值传递。与Java不同,PHP中的字符串是基本数据类型。
  2. 基本数据类型的数组按值传递。
  3. 对象按引用传递。
  4. 对象数组按值(数组本身)传递,但每个对象都按引用传递。

<?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" } } 

注意:作为优化,每个值在函数内部修改之前都被传递为引用。如果它被修改并且该值是通过引用传递的,则会复制该值并修改副本。


5
这个回答应该被点赞到顶部。它包含其他答案没有提到的一个晦涩难懂的问题:“4-对象数组是按值传递的(数组本身),但每个对象都是按引用传递的。” 因为这个问题,我一直在思考! - augustin
@magallanes 对我来说也应该首先评价为优秀,您为我解决了一个关于对象数组的问题。是否有一种方法可以仅在两个数组变量中的一个中修改数组中的对象(原始和副本)? - fede72bari

6

当在PHP中将数组传递给方法或函数时,如果您没有显式地通过引用传递它,它将被按值传递,如下所示:

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);

在你的第二个问题中,$b 不是对 $a 的引用,而是 $a 的一个副本。就像第一个例子一样,你可以通过以下方式引用 $a
$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);

2

在 PHP 中,默认情况下,数组是按值传递给函数的,除非您显式地通过引用传递它们,如以下代码段所示:

$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

这是输出结果:
array(3) {
  [0]=>
  int(11)
  [1]=>
  int(22)
  [2]=>
  int(33)
}
array(3) {
  [0]=>
  int(66)
  [1]=>
  int(22)
  [2]=>
  int(33)
}

2
为了补充其中一个回答,多维数组的子数组也会按值传递,除非显式地按引用传递。"最初的回答"
<?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

最终结果为:
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)
}

1
这个帖子有点老,但是我刚刚找到了一些东西: 尝试使用这段代码:
$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

请注意,$params参数中没有&符号,但它仍然改变了$arr ['date']的值。这与此处的所有其他解释以及我之前的想法并不完全匹配。
如果我克隆$params ['date']对象,则第二个输出的日期保持不变。如果我只将其设置为字符串,则也不会影响输出。

3
该数组已被复制,但这不是一份_深层_复制。这意味着像数字和字符串这样的基本值会被复制到$param中,但对于对象而言,引用会被复制而不是对象被克隆。$arr持有$date的引用,复制的数组$params也是如此。因此,当您调用$params ['date']上的函数以更改其值时,您也会更改$arr ['date']和$date。当您将$params['date']设置为字符串时,您只是用其他内容替换了$params对$date的引用。 - ejegg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接