PHP 日本研討會 2024

變數作用域

變數的作用域是指它被定義的上下文。PHP 有函式作用域和全域作用域。在函式之外定義的任何變數都限制在全域作用域內。當包含一個檔案時,它包含的程式碼會繼承包含該程式碼的行的變數作用域。

範例 #1 全域變數作用域範例

<?php
$a
= 1;
include
'b.inc'; // 變數 $a 將在 b.inc 內可用
?>

在具名函式或匿名函式內建立的任何變數都限制在函式主體的作用域內。但是,箭頭函式會將父作用域的變數繫結,使其在主體內可用。如果在呼叫檔案的函式內發生檔案包含,則呼叫檔案中包含的變數將如同在呼叫函式中定義一樣可用。

範例 #2 區域變數作用域範例

<?php
$a
= 1; // 全域作用域

function test()
{
echo
$a; // 變數 $a 未定義,因為它指的是 $a 的區域版本
}
?>

上面的範例將產生一個未定義的變數 E_WARNING(或 PHP 8.0.0 之前的 E_NOTICE)。這是因為 echo 語句指的是 $a 變數的區域版本,並且在這個作用域內沒有賦予它一個值。請注意,這與 C 語言有點不同,因為 C 語言中的全域變數會自動提供給函式使用,除非被區域定義明確覆蓋。這可能會導致一些問題,因為人們可能會不小心變更全域變數。在 PHP 中,如果要在函式中使用全域變數,則必須在函式內宣告為全域。

global 關鍵字

global 關鍵字用於將全域作用域的變數繫結到區域作用域。該關鍵字可以與變數清單或單個變數一起使用。將建立一個參考相同名稱全域變數的區域變數。如果全域變數不存在,則將在全域作用域中建立該變數,並賦值為 null

範例 #3 使用 global

<?php
$a
= 1;
$b = 2;

function
Sum()
{
global
$a, $b;

$b = $a + $b;
}

Sum();
echo
$b;
?>

上面的範例將輸出

3

透過在函式內宣告 $a$b 為全域,對任一變數的所有參考都將參考全域版本。函式可以操作的全域變數數量沒有限制。

另一種存取全域作用域變數的方式是使用特殊的 PHP 定義的 $GLOBALS 陣列。上一個範例可以改寫為

範例 #4 使用 $GLOBALS 而不是 global

<?php
$a
= 1;
$b = 2;

function
Sum()
{
$GLOBALS['b'] = $GLOBALS['a'] + $GLOBALS['b'];
}

Sum();
echo
$b;
?>

$GLOBALS 陣列是一個關聯陣列,其中全域變數的名稱為鍵,該變數的內容為陣列元素的 value。請注意,$GLOBALS 存在於任何作用域中,這是因為 $GLOBALS 是一個超全域。以下是一個示範超全域功能的範例

範例 #5 示範超全域和作用域的範例

<?php
function test_superglobal()
{
echo
$_POST['name'];
}
?>

注意在函式外部使用 global 關鍵字不是錯誤。如果從函式內部包含該檔案,則可以使用它。

使用 static 變數

變數作用域的另一個重要特性是靜態變數。靜態變數僅存在於區域函式作用域中,但當程式執行離開此作用域時,它不會遺失其值。考慮以下範例

範例 #6 示範需要靜態變數的範例

<?php
function test()
{
$a = 0;
echo
$a;
$a++;
}
?>

這個函式非常無用,因為每次呼叫它時,它都會將 $a 設定為 0 並列印 0$a++ 遞增變數沒有任何作用,因為一旦函式結束,$a 變數就會消失。為了製作一個有用的計數函式,而不會遺失目前的計數,將 $a 變數宣告為靜態

範例 #7 靜態變數的使用範例

<?php
function test()
{
static
$a = 0;
echo
$a;
$a++;
}
?>

現在,變數 $a 只會在函式第一次被呼叫時初始化,且每次呼叫 test() 函式時,都會印出 $a 的值並將其遞增。

靜態變數也提供了一種處理遞迴函式的方法。以下簡單的函式使用靜態變數 $count 來判斷何時停止,以遞迴方式計數到 10。

範例 #8 搭配遞迴函式的靜態變數

<?php
function test()
{
static
$count = 0;

$count++;
echo
$count;
if (
$count < 10) {
test();
}
$count--;
}
?>

在 PHP 8.3.0 之前,靜態變數只能使用常數表達式初始化。自 PHP 8.3.0 起,也允許使用動態表達式(例如函式呼叫)。

範例 #9 宣告靜態變數

<?php
function foo(){
static
$int = 0; // 正確
static $int = 1+2; // 正確
static $int = sqrt(121); // 自 PHP 8.3.0 起正確

$int++;
echo
$int;
}
?>

自 PHP 8.1.0 起,當使用靜態變數的方法被繼承(但未被覆寫)時,繼承的方法現在將與父方法共享靜態變數。這表示方法中的靜態變數現在的行為與靜態屬性相同。

自 PHP 8.3.0 起,靜態變數可以使用任意表達式初始化。這表示例如可以使用方法呼叫來初始化靜態變數。

範例 #10 在繼承方法中使用靜態變數

<?php
class Foo {
public static function
counter() {
static
$counter = 0;
$counter++;
return
$counter;
}
}
class
Bar extends Foo {}
var_dump(Foo::counter()); // int(1)
var_dump(Foo::counter()); // int(2)
var_dump(Bar::counter()); // int(3),在 PHP 8.1.0 之前為 int(1)
var_dump(Bar::counter()); // int(4),在 PHP 8.1.0 之前為 int(2)
?>

使用 globalstatic 變數的參考

PHP 透過參考來實作變數的 staticglobal 修飾符。例如,使用 global 語句匯入函式範圍內的真正全域變數,實際上會建立對全域變數的參考。這可能會導致意外的行為,以下範例將說明這個問題。

<?php
function test_global_ref() {
global
$obj;
$new = new stdClass;
$obj = &$new;
}

function
test_global_noref() {
global
$obj;
$new = new stdClass;
$obj = $new;
}

test_global_ref();
var_dump($obj);
test_global_noref();
var_dump($obj);
?>

上面的範例將輸出

NULL
object(stdClass)#1 (0) {
}

類似的行為適用於 static 語句。參考不會靜態儲存。

<?php
function &get_instance_ref() {
static
$obj;

echo
'Static object: ';
var_dump($obj);
if (!isset(
$obj)) {
$new = new stdClass;
// 將參考賦值給靜態變數
$obj = &$new;
}
if (!isset(
$obj->property)) {
$obj->property = 1;
} else {
$obj->property++;
}
return
$obj;
}

function &
get_instance_noref() {
static
$obj;

echo
'Static object: ';
var_dump($obj);
if (!isset(
$obj)) {
$new = new stdClass;
// 將物件賦值給靜態變數
$obj = $new;
}
if (!isset(
$obj->property)) {
$obj->property = 1;
} else {
$obj->property++;
}
return
$obj;
}

$obj1 = get_instance_ref();
$still_obj1 = get_instance_ref();
echo
"\n";
$obj2 = get_instance_noref();
$still_obj2 = get_instance_noref();
?>

上面的範例將輸出

Static object: NULL
Static object: NULL

Static object: NULL
Static object: object(stdClass)#3 (1) {
  ["property"]=>
  int(1)
}

此範例說明當將參考賦值給靜態變數時,在第二次呼叫 &get_instance_ref() 函式時,不會記住該參考。

新增註解

使用者貢獻的註解 5 則註解

227
dodothedreamer at gmail dot com
13 年前
請注意,與 Java 和 C++ 不同,在迴圈或 if 等區塊內宣告的變數,也會在區塊外被識別和存取,因此
<?php
for($j=0; $j<3; $j++)
{
if(
$j == 1)
$a = 4;
}
echo
$a;
?>

會印出 4。
179
warhog at warhog dot net
18 年前
在類別方法中使用靜態範圍關鍵字時,會出現一些有趣的行為(使用 PHP5 測試)。

<?php

class sample_class
{
public function
func_having_static_var($x = NULL)
{
static
$var = 0;
if (
$x === NULL)
{ return
$var; }
$var = $x;
}
}

$a = new sample_class();
$b = new sample_class();

echo
$a->func_having_static_var()."\n";
echo
$b->func_having_static_var()."\n";
// this will output (as expected):
// 0
// 0

$a->func_having_static_var(3);

echo
$a->func_having_static_var()."\n";
echo
$b->func_having_static_var()."\n";
// this will output:
// 3
// 3
// maybe you expected:
// 3
// 0

?>

你可能會預期輸出 "3 0",因為你可能會認為 $a->func_having_static_var(3); 只會更改 $a 中函式的靜態變數 $var 的值。但正如其名稱所示,這些是類別方法。擁有一個物件只是一個屬性的集合,函式仍然保留在類別中。因此,如果你在函式內宣告一個變數為靜態變數,則它是對整個類別及其所有實例都是靜態的,而不是對每個物件都是靜態的。

發布這個可能沒有意義.. 因為如果你想要我所期望的行為,你可以簡單地使用物件本身的變數。

<?php
class sample_class
{ protected $var = 0;
function
func($x = NULL)
{
$this->var = $x; }
}
?>

我相信所有正常思考的人甚至都不會嘗試使用 static 關鍵字來實現此目的,對於那些嘗試的人(像我一樣),這個筆記可能會有幫助。
34
andrew at planetubh dot com
15 年前
我花了比預期更長的時間才弄清楚這一點,並認為其他人可能會覺得它有用。

我建立了一個函式 (safeinclude),我用它來包含檔案;它會在實際包含檔案之前進行處理(確定完整路徑、檢查它是否存在等等)。

問題:由於包含發生在函式內部,因此包含檔案中的所有變數都繼承了函式的變數作用域;由於包含的檔案可能需要或不需要在其他地方宣告的全域變數,這會產生問題。

大多數地方(包括這裡)似乎都透過以下方式解決此問題
<?php
// 在 include 之前宣告
global $myVar;
// 或在 include 檔案內宣告
$nowglobal = $GLOBALS['myVar'];
?>

但是,要使此方法在這種情況下起作用(在函式內包含標準 PHP 檔案,從另一個 PHP 腳本呼叫;其中存取可能存在的任何全域變數非常重要)...對於 'safeinclude' 包含的每個 PHP 檔案中的每個變數都使用上述方法是不切實際的,對於 "global $this" 方法靜態命名每個可能的變數也是不切實際的。(主要是因為程式碼是模組化的,而 'safeinclude' 旨在是通用的)

我的解決方案:因此,為了使所有全域變數可用於透過我的 safeinclude 函式包含的檔案,我必須將以下程式碼新增到我的 safeinclude 函式中(在變數被使用或包含檔案之前)

<?php
foreach ($GLOBALS as $key => $val) { global $$key; }
?>

因此,完整的程式碼如下所示(非常基本的模型)

<?php
function safeinclude($filename)
{
// 這行程式碼會取得所有全域變數,並在函式中設定它們的作用域:
foreach ($GLOBALS as $key => $val) { global $$key; }
/* 在此處進行預先處理:驗證檔案名稱輸入,確定檔案的完整路徑,
檢查檔案是否存在等等。這顯然不是必需的,但這是我發現有用的步驟。 */
if ($exists==true) { include("$file"); }
return
$exists;
}
?>

在上述程式碼中,'exists' 和 'file' 在預先處理中確定。File 是檔案的完整伺服器路徑,如果檔案存在,則 exists 設定為 true。當然,可以擴展這個基本模型。在我自己的模型中,我新增了其他可選參數,以便我可以呼叫 safeinclude 來查看檔案是否存在,而無需實際包含它(以利用我的路徑/等預處理,而不是僅呼叫檔案存在函式)。

這是一種非常簡單的方法,我在網路上找不到任何地方;我能找到的唯一另一種方法是使用 PHP 的 eval()。
10
gried at NOSPAM dot nsys dot by
8 年前
事實上,所有變數都代表指標,這些指標保存著已指派給此變數的資料的記憶體區域位址。當你透過引用指派某些變數值時,實際上是將來源變數的位址寫入接收變數。當你在函式中宣告某些變數為全域變數時,也會發生相同的情況,它會收到與函式外部全域變數相同的位址。如果你考慮上述解釋,很明顯,混合使用以關鍵字 global 宣告的相同變數和同時透過超全域陣列宣告的相同變數是一個非常糟糕的想法。在某些情況下,它們可能指向不同的記憶體區域,讓你感到頭痛。請考慮以下程式碼

<?php

error_reporting
(E_ALL);

$GLOB = 0;

function
test_references() {
global
$GLOB; // 使用關鍵字 global 取得全域變數的參考,此時區域變數 $GLOB 指向與全域變數 $GLOB 相同的位址
$test = 1; // 宣告一些區域變數
$GLOBALS['GLOB'] = &$test; // 使用超全域陣列使全域變數參考此區域變數,此時全域變數 $GLOB 指向新的記憶體位址,與區域變數 $test 相同

$GLOB = 2; // 透過先前設定的區域表示法將新值設定為全域變數,寫入舊位址

echo "全域變數的值(透過關鍵字 global 設定的區域表示法):$GLOB <hr>";
// 透過區域表示法檢查全域變數 => 2(OK,取得剛剛寫入的值,因為使用舊位址取得值)

echo "全域變數的值(透過超全域陣列 GLOBALS):$GLOBALS[GLOB] <hr>";
// 使用超全域陣列檢查全域變數 => 1(取得區域變數 $test 的值,使用了新的位址)

echo "區域變數 \$test 的值:$test <hr>";
// 檢查透過超全域陣列與全域變數連結的區域變數 => 1(其值未受影響)

global $GLOB; // 使用關鍵字 global 更新全域變數的參考,此時我們更新區域變數 $GLOB 中保存的位址,它會取得與區域變數 $test 相同的位址
echo "全域變數的值(透過關鍵字 global 設定的更新區域表示法):$GLOB <hr>";
// 透過區域表示法檢查全域變數 => 1(也是區域變數 $test 的值,使用了新的位址)
}

test_references();
echo
"函式外部的全域變數值:$GLOB <hr>";
// 檢查函式外部的全域變數 => 1(等於函式中區域變數 $test 的值,全域變數也指向新的位址)
?>
20
larax at o2 dot pl
18 年前
關於使用全域變數的更複雜情況..

假設我們有兩個檔案
a.php
<?php
function a() {
include(
"b.php");
}
a();
?>

b.php
<?php
$b
= "something";
function
b() {
global
$b;
$b = "something new";
}
b();
echo
$b;
?>

你可能會預期這段腳本會回傳 "something new",但實際上它會回傳 "something"。要讓它正確運作,你必須在 $b 的定義中加入 global 關鍵字,以上面的範例來說,應該要改成:

global $b;
$b = "something";
To Top