PHP 中的数组是作为值复制还是作为对新变量的引用,以及何时传递给函数?

Posted

技术标签:

【中文标题】PHP 中的数组是作为值复制还是作为对新变量的引用,以及何时传递给函数?【英文标题】:Are arrays in PHP copied as value or as reference to new variables, and when passed to functions? 【发布时间】:2011-01-03 02:33:48 【问题描述】:

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

2) 将数组赋值给变量时,新变量是对原始数组的引用,还是新的副本? 这样做怎么样:

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

$b 是对$a 的引用吗?

【问题讨论】:

另见When-does-foreach-copy @MarlonJerezIsla:看起来只有在函数内部修改数组时才会克隆数组。仍然来自其他语言,看起来很奇怪。 【参考方案1】:

TL;DR

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

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

请记住 - php 会在您写入的那一刻 将值复制到非 & 数组参数中。这就是copy-on-write 的意思。我很想向您展示这种行为的 C 源代码,但它在那里很可怕。最好使用xdebug_debug_zval()。

帕斯卡·马丁是对的。 Kosta Kontos 更是如此。

回答

视情况而定。

加长版

我想我是在为自己写下来。我应该有一个博客什么的……

每当人们谈论引用(或指针,就此而言)时,他们通常会以一个标志性结束(看看这个thread!)。 PHP 作为一种古老的语言,我认为我应该增加混乱(即使这是上述答案的总结)。因为,虽然两个人可能同时是对的,但你最好只是将他们的头脑拼凑成一个答案。

首先,你应该知道如果你不以非黑即白的方式回答,你就不是一个书呆子。事情比“是/否”更复杂。

正如您将看到的,整个按值/按引用的事情与您在方法/函数范围内使用该数组的具体操作非常相关:读取它还是修改它?

PHP 是怎么说的? (又名“明智的改变”)

manual 这么说(强调我的):

默认情况下,函数参数按值传递(这样如果 函数内参数的值被改变,它没有得到 在函数之外更改)。允许函数修改其 参数,它们必须通过引用传递

有一个论据 函数总是通过引用传递,在 函数定义中的参数名称

据我所知,当大型、认真、诚实的程序员谈论引用时,他们通常会谈论改变引用的价值。这正是手册所说的内容:hey, if you want to CHANGE the value in a function, consider that PHP's doing "pass-by-value"

还有一个他们没有提到的情况:如果我不改变任何东西怎么办 - 只是阅读? 如果您将数组传递给没有显式标记引用的方法,并且我们不在函数范围内更改该数组怎么办?例如:

<?php
function readAndDoStuffWithAnArray($array) 

    return $array[0] + $array[1] + $array[2];


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

echo readAndDoStuffWithAnArray($x);

请继续阅读,我的同路人。

PHP 实际上做了什么? (又名“内存方面”)

同样的大而认真的程序员,当他们变得更加认真时,他们会谈论关于引用的“内存优化”。 PHP 也是如此。因为PHP is a dynamic, loosely typed language, that uses copy-on-write and reference counting,那就是why。

将 HUGE 数组传递给各种函数并让 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 变量,它就是一个引用,memory-wise。你只是没有看到它。这就是为什么 PHP mentions user-land 在谈论 &amp;$someVar 时引用,以区分内部和显式(使用 & 符号)。

事实

所以,when an array is passed as an argument to a method or function is it passed by reference?

我想出了 三个(是的,三个)案例: a) 方法/函数只读取数组参数 b) 方法/函数修改数组参数 c) 方法/函数数组参数被显式标记为引用(带有 & 符号)


首先,让我们看看该数组实际占用了多少内存(运行here):

<?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! See for yourself。这是 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);

同样,see for yourself,但对我来说,这非常接近 1331840。所以在这种情况下,数组 实际上被复制到 $arr

c) 方法/函数数组参数被显式标记为引用(带有 & 符号)

现在让我们看看对显式引用的写操作需要多少内存(运行here)-注意函数签名中的&符号:

<?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 个!因此,这消耗的内存大约与从非 & 参数读取一样多。

【讨论】:

在调试内存泄漏时为我节省了几个小时! Kosta Kontos:这是一个非常重要的问题,您应该将其标记为已接受的答案。也就是说,@nevvermind:很棒的文章,但请包括一个*** TL;DR 部分。 @nevvermind:我不是 groopy 的首字母缩写词,主要区别在于结论通常出现在文章的末尾,而 TL;DR 出现在那些只需要简短的人的第一行回答而不是进行冗长的分析。你的研究很好,这不是批评,只是我的 00.02 美元。 你是对的。我把结论放在最上面。但我仍然希望人们不要再懒惰地阅读整本书,在得出任何结论之前。滚动对我们来说太容易改变事物的顺序了。 我猜 PHP 在几年后变得更加高效,因为您的键盘示例给出的数字要低得多:)【参考方案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)

【讨论】:

【参考方案3】:

默认

    基元按值传递。不像 Java,字符串在 PHP 中是原始的 基元数组按值传递 对象通过引用传递

    对象数组通过值(数组)传递,但每个对象都通过引用传递。

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

注意:作为一种优化,每个值都作为引用传递,直到它在函数内被修改。如果它被修改并且值是通过引用传递的,那么它被复制并且副本被修改。

【讨论】:

这个答案应该被 +1 到顶部。它包含一个其他答案没有提到的晦涩难懂的问题:“4 - 对象数组按值(数组)传递,但每个对象都按引用传递。”因为那个,我一直在挠头! @magallanes great 也应该被我评为第一,你澄清了我遇到的对象数组的麻烦。有没有办法只在两个数组变量(原始和副本)之一中修改数组中的对象?【参考方案4】:

关于你的第一个问题,数组是通过引用传递的,除非它在你调用的方法/函数中被修改。如果您尝试在方法/函数中修改数组,则首先制作它的副本,然后仅修改副本。这看起来好像数组是按值传递的,但实际上并非如此。

例如,在第一种情况下,即使您没有将函数定义为通过引用接受 $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

仅供参考 - 这称为“延迟复制”或“写时复制”。

【讨论】:

这是一条超级有趣的信息!看起来是真的;但我找不到任何支持这一事实的官方文件。我们还需要知道哪些版本的 PHP 支持这种惰性复制概念。谁有更多信息? 更新,找了一些官方文档,还是要找PHP哪个版本支持惰性复制(手册里叫copy on write):php.net/manual/en/internals2.variables.intro.php 这纯粹是 PHP 虚拟机的实现决定,而不是语言的一部分 - 它实际上对程序员不可见。 Copy-on-write 出于性能原因当然是推荐的,但是从程序员的角度来看,复制每个数组的实现没有区别,因此我们可以说语言语义指定了按值传递。 @Superfly 当我想知道我是否可以在不耗尽内存的情况下通过一堆函数传递我的 100MB 数组时,这肯定会有所不同!您可能是对的,尽管将语义称为按值传递是正确的,但抛开这些术语上的争论不谈,这里提到的“实现细节”对于现实世界中的 PHP 程序员来说确实很重要。 这还有一个怪癖,这使得在考虑性能时意识到写时复制变得更加重要。您可能认为通过引用传递数组 saves 内存与按值传递相比(如果您不知道写时复制),但它实际上可以具有 相反 效果!如果数组随后按值传递(通过您自己的或第 3 方代码),那么 PHP 必须 制作完整副本,否则它无法再跟踪引用计数!更多信息:***.com/questions/21974581/…【参考方案5】:

在 PHP 中,数组默认按值传递给函数,除非你明确地通过引用传递它们,如下面的 sn-p 所示:

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

【讨论】:

【参考方案6】:

这个帖子有点老了,但我刚刚遇到了一些东西:

试试这个代码:

$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'] 对象,第二个输出日期保持不变。如果我只是将它设置为一个字符串,它也不会影响输出。

【讨论】:

数组被复制,但不是副本。这意味着像数字和字符串这样的原始值被复制到 $param 中,但是对于对象,引用被复制而不是被克隆的对象。 $arr 持有对 $date 的引用,复制的数组 $params 也是如此。所以当你在 $params['date'] 上调用一个改变它的值的函数时,你也在改变 $arr['date'] 和 $date。当您将 $params['date'] 设置为字符串时,您只是将 $params 对 $date 的引用替换为其他内容。【参考方案7】:

对于您问题的第二部分,请参阅array page of the manual,其中指出 (引用)

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

还有给定的例子:

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

因为,这一次,数组是“通过引用”传递的。

不要犹豫,阅读手册的References Explained 部分:它应该回答您的一些问题;-)

【讨论】:

像 $a = &$this->a 这样的东西怎么样。 $a 现在是对 &this->a 的引用吗? 当您使用 &amp; 时,是的,它应该 -- 请参阅 php.net/manual/en/… 天哪,我不敢相信这是我遇到的问题......如果这是一个教训,请务必阅读即将发布的手册 嗨 Pascal,我发现 Kosta Kontos 的回答似乎更准确。我做了一个简单的快速测试来确认他的发现gist.github.com/anonymous/aaf845ae354578b74906你也可以评论他的发现吗? 这也是我遇到的问题:认为嵌套数组有些奇怪,但实际上这正是 PHP 中数组赋值的工作方式。【参考方案8】:

当一个数组被传递给 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);

【讨论】:

以上是关于PHP 中的数组是作为值复制还是作为对新变量的引用,以及何时传递给函数?的主要内容,如果未能解决你的问题,请参考以下文章

值的引用传递数组作为函数参数

我如何将数组中的值作为变量引用?

C++ 对象作为返回值:复制还是引用?

为啥从函数返回数组作为参数时,我会从函数中的数组中获取随机值? [复制]

c++,类的对象作为形参时一定会调用复制构造函数吗?

如何从 javascript 变量发送表单值并在 php 中作为整数接收? [复制]