関数スコープとブロックスコープ (PHPはなぜ var, let, const がない?アロー関数はブロックがない?) #変数

文法上の系譜で分類すると、C系統の言語があります。C、C++、C#、Java、JavaScript、PHPは、Cの文法的スタイルを受け継いだ言語であり、C系統の言語と呼ばれます。

多くの方が初めて、PHPに学ぶ際に、C系統の言語であるにもかかわらず、他のC系統の言語とは異なる文法的特徴が多いため、PHPに対して抵抗感を覚える場合があります。この抵抗感を引き起こす要因の一つとして、PHPには変数を宣言するためのキーワードが存在しないこと、およびブロックスコープがないことが挙げられます。

ブロックスコープがある言語とブロックスコープがない言語の違いをよく理解し、ブロックスコープがない言語に適したコーディングスタイルを理解することで、PHPに対する抵抗感を減らすことができます。

スコープとは?

スコープとは、変数の有効範囲を意味します。通常、ブロックまたは関数ブロックが開かれた際に、そのブロック内で宣言された変数は、ブロックが閉じられるとブロックの外でアクセスできなくなり、コードの実行が終了になってからガベージコレクターによって消滅されます。ブロック内で宣言された変数は、ブロックの外部では使用できません。

JavaScriptで理解する

JavaScriptでは、varで宣言された変数は関数スコープを持ち、constletで宣言された変数はブロックスコープを持ちます。

(本記事はPHPを対象とした説明であるため、以下のJavaScriptの例では、より分かりやすくするために変数名に$を付けております。)

ブロックスコープ

if (true) {
  let $a = 10;
}

console.log($a); // Uncaught ReferenceError: $a is not defined

ブロック内で宣言された$aは、ブロックの外部からアクセスできません。

(function () {
  let $a = 10;
})();

console.log($a); // Uncaught ReferenceError: $a is not defined

関数内で宣言された$aは、関数の外部からアクセスできません。

関数スコープ

if (true) {
  var $a = 10;
} else {
  var $b = 20;
}

console.log($a); // 10
console.log($b); // undefined

JavaScriptのグローバル変数は、グローバルスコープを関数スコープとして扱います。関数の内部にない場合でも、関数スコープの変数はグローバルスコープを関数スコープとして使用すると考えることができます。

JavaScriptには、PHPのissetのように、変数が定義されているかどうかを確認する機能は存在いたしません。そのため、変数を宣言するコードが実行されていなくても、変数の使用時にエラーが発生しないようにする必要があります。JavaScriptでは、エラーを発生させるのではなく、代わりにundefinedという値にするホイスティングという方法を使用します。

関数スコープを使う変数$bは、コードが実行されなかった場合でもブロックスコープのようにエラーではなく、undefined の値となります。

(function () {
  var $a = 10;
})();

console.log($a); // Uncaught ReferenceError: $a is not defined

関数スコープ内で宣言された変数は、関数の外部からアクセスすることはできません。

関数スコープを利用する変数がブロック内で宣言された場合は、同じ関数スコープ内で、undefinedの値としてアクセスできますが、関数スコープの外部ではundefinedの値としてアクセスできなく、エラーが発生します。

PHPコード

ブロックスコープなし

if (true) {
    $a = 10;
}

var_dump($a); // 10

変数$aはブロック内で宣言されましたが、ブロック外でもブロック内で宣言された変数を使用できます。

if (false) {
    $a = 10;
}

var_dump($a); // Warning: Undefined variable $a

JavaScriptでは、関数スコープを使用するvar宣言キーワードは変数をホイスティングし、ランタイムで実行されていない変数であってもエラーではなくundefinedを割り当てるようにします。関数スコープを使用するPHPには、JavaScriptのホイスティングのような機能を使わないため、エラーが発生します。

if (false) {
    $a = 10;
}

var_dump($a ?? null); // null

PHPにはisset, ??, ??=などの文法を使用して、定義されていない変数を確認するキーワードがあるため、定義されていない変数の使用がエラーを引き起こす場合でも、実行中にエラーを発生させることなくコードを展開することができます。

関数スコープあり

(function () {
    $a = 10;
})();

var_dump($a); // Warning: Undefined variable $a

$a変数の宣言は関数内で行われるため、関数外でこの変数を使用すると「定義されていない」というエラーが発生いたします。

ブロックスコープの利点

ブロックを使用すれば、変数はブロック内で宣言され、宣言されたブロック外ではアクセスできません。このような特徴は、ブロック内で宣言し、ブロックの閉じとともにその役割を終えるブロック内でのみ使用される一時的な変数を気軽に作成できるという利点があります。多くの変数を作成しても、ロジックの流れを記述する部分での変数の数を抑え、流れを構成する部分で一時変数は気軽に生成できます。

ブロックスコープがない場合の問題

ブロックスコープがなく、関数スコープのみが使用できる言語では、ブロックは変数の有効範囲を設定する目的ではなく、制御の流れを示すifforswitchなどの文法とともに、これらの文法の作用範囲を決めるために使用されます。ブロックスコープがないため、関数内に宣言されるすべての変数は、ブロック内にあってもそれらのブロックを囲む関数スコープ内に変数が追加されます。

ブロックスコープがあれば、ブロック内で一時変数を使うことができますが、ブロックスコープがない場合、一時的に使用する変数を宣言しても、ブロックの終了とともに消えるのではなく、ブロックを囲む関数スコープにそのまま残ってしまうという問題があります。

特定の文脈で一時的に使用する変数が関数スコープ全体に残るため、関数内部のロジックが長くなればなるほど、一時的に使用される変数が関数スコープ領域に増える問題が発生します。このため、何が使用される変数で何が使用されない変数なのかを区別することが難しくなります。

ブロックスコープは本当に必要か?

ブロックスコープがない場合、ロジックが長くなるほど一時的に使われる変数が増えてしまいます。そのため、ブロックスコープを導入しようという意見に対して、そもそも一つの関数スコープに長いコードを書くことは適切なコーディングプラクティスではないという反論があります。

しかし、時には一つのスコープ内で冗長に書かざるを得ないロジックがあるし、ロジックを分けることが不自然に感じられる場合もあるため、ブロックスコープを導入することが望ましいという意見もあります。

必要であれば関数スコープを使用しよう

基本的に、一つの関数スコープ内のロジックを冗長に書かないように努力すべきです。ロジックを関数で分割することが難しい場合は、関数スコープを使用してロジックを分けて書きます。

関数を使用することは、ロジックに名前を付けるので、そのスコープが持つ役割が何かわかりやすくして、概念的に分けることで、ブロック内のコードを読むことなくコードの流れや文脈を関数のシグネチャを通じて理解できるという利点があります。PHPの関数は外部変数をキャプチャするためにuseキーワードを使用するので、指定した関数の動作のためにどの外部変数を使用するかが簡単に分かります。

ブロックは関数のシグネチャを使用しなくて済むので便利ですが、特別な名前がないため、ブロックの役割を使用された文法や文脈に基づいて考えなければなりません。ブロックスコープがなく関数スコープのみがある言語では、関数を使うことで名前が付けられるため、一時変数を使う場合には適切な関数の抽象化を行うことが推奨される圧力を受けます。

関数への抽象化は、関数のシグネチャを通じて関数の役割をおおまかに理解できるようにし、一時変数を使うべきブロックに名前を付けることで、単位ロジックの使用意図を明確にできるという利点があります。

即時実行関数を使用する

時には、関数で分割することがコードを増やして冗長な印象を与えるため、関数の使用を避けたい場合があります。また、再利用する必要がないのに関数なのに、まるで再利用すべきもののような印象を与えてしまうため、関数の使用を避けたいこともあります。さらに、流れとして変数だけを隔離するパターンのコードを作成したい場合もあります。このような時には、即時実行関数を使用してコードを作成します。

即時実行関数の例

$outerVariable = 10;

$result = (function () use ($outerVariable) {
    $innerVariable = 20;
    return $innerVariable + $outerVariable;
})();

var_dump($result); // 30

無名関数を定義すると同時に(function () {})()のように関数を変数に格納せずに実行する方法です。PHPではブロックスコープがないため、ブロックスコープの代わりに関数スコープを使用して、スコープ内でのみ一時変数で使用する方法が可能です。

スコープの入れ子を推奨しない文法

即時実行関数を使用する問題点は、関数スコープに外部の変数を渡すためにuseキーワードを使って、関数外から関数内に変数を渡さなければならないため、ボイラープレートが増える点です。

しかし、スコープを作るたびに変数をキャプチャするために多くのボイラープレートを使用しなければならないため、多くの無名関数でスコープを作る場合、深い入れ子はその分だけ多くのボイラープレートを生み出すため、スコープの入れ子を抑制します。

一般的に、スコープの入れ子が多いロジックは読みづらく、メンテナンスが難しい場合が多いため、深い入れ子を推奨しない圧力があることは良い点です。

しかし、一部のケースでは、ロジックを分割するよりも深い入れ子の方が適している場合もあります。ただし、極少数のケースのために深い入れ子というアンチパターンを推奨する必要はないです。useキーワードで外部スコープの変数をキャプチャすることはボイラープレートを増やしますが、ボイラープレートが原因で入れ子を推奨しないため、良い文法だと言えます。

即時実行関数の乱用に関するコードスメルの議論

ブロックスコープがないため、一時的に使用する変数の領域を作るために即時実行関数を乱用するコードが作成される可能性がありますが、これが他の言語のコードベースでは見かけにくいコードスメルが発生するコードになることがあります。これがブロックスコープがなく関数スコープのみがある言語に適したコーディングスタイルなのか、それともコードスメルなのかについては議論の余地があります。

オブジェクト指向でスコープの入れ子を抑制する

最近のJavaScriptは、オブジェクト指向よりも手続き型プログラミングや関数型プログラミングを活用してコードを書くことが多いです。そのため、ロジックの流れを表現する部分のコードをブロックでスコープを区別し、一時変数を生成するコーディングが多いです。

イベントやAPI通信中心の多くの非同期ロジックを扱うJavaScriptは、関数中心のコーディングスタイルが適している一方で、PHPはほとんどが同期コードで、前後関係が明確なコードを書くことが推奨されているため、オブジェクト中心のコードスタイルを採用します。

PHPはオブジェクト指向の技法を推奨する言語であり、ロジックの大きな流れの中で一部分を担当するコードはメソッドに分けてコードを書くスタイルが推奨されているため、オブジェクト指向をうまく活用すれば関数スコープの乱用を抑制でき、無名関数の乱用を減らすことができます。

関数のネストで外部スコープの変数をキャプチャできますが、メソッドに分割すると、変数を共有するためにメンバ変数を使用してメソッド間で変数を共有することになります。

しかし、時にはメンバ変数が増えすぎてメンテナンスが難しいクラスになってしまうことがあります。そのような場合は、1つのクラスを冗長に記述するのではなく、複数のクラスに分割し、組み合わせるというオブジェクト指向の考え方で解決できます。

過度なオブジェクト指向は設計に時間がかかり、コードの量も増えてしまうため、適切にメソッドや関数、即時実行関数を活用し、状況に応じたコスパの良いコードを書くようにしましょう。

知っておくと良いこと

変数キャプチャ

useキーワードを使用して関数が外部の変数をキャプチャする際、変数に格納されている値の種類によって挙動が異なります。オブジェクトのような参照型の値は参照として渡され、参照でない値はコピーされます。

 $outerVariable = 10;
 
 (function () use ($outerVariable) {
     $innerVariable = 20;
     $outerVariable = $innerVariable + $outerVariable;
     var_dump($outerVariable); // 30
 })();
 
 var_dump($outerVariable); // 10

$outerVariableが宣言された際の値は、参照ではない値です。参照ではない値をuseでキャプチャすると、基本的にコピーされて関数内に渡されます。そのため、関数内の$outerVariableはコピーされた値であり、値が変更されたとしても関数の外側にある変数とは異なる値となるため、関数内での変更は関数外の変数には反映されません。

$outerVariable = 10;

(function () use (&$outerVariable) {
    $innerVariable = 20;
    $outerVariable = $innerVariable + $outerVariable;
    var_dump($outerVariable); // 30
})();

var_dump($outerVariable); // 30

$outerVariableが宣言される際の値は参照ではない値です。参照ではない値をuseでキャプチャする際に&を付けて参照として指定すると、関数内の$outerVariableは宣言された値と同じ対象を指すことになります。そのため、関数内で値が変更されると、関数外の値も変更されます。

unsetを使った一時変数の管理

時には、コードの展開過程で短い単位のコード内でのみ使用され、もう使われない一時変数を作成することがあります。この場合、unsetを活用して関数スコープの変数が汚染されないようにコードを作成することが推奨されることがあります。

関数スコープのコードが短い場合は、unsetを使用しなくても問題ありませんが、1つのスコープでコードが長くなる場合は、unsetで使用が終わった変数を削除し、変数空間が汚染されないようにする戦略を取ることができます。

ただし、ブロックがないため、宣言された変数がいつunsetされるのかを確認する必要があります。そのため、一目でわかる程度の数行の短いコードにおいてのみ、宣言されたコードと一緒に値の割り当てを解除することを分かる方式で使うのが良いです。「非常に長いロジック + 数行程度の短い一時変数を使用したコード」の場合にのみ、unsetによる変数の割り当て解除が適切なコーディングスタイルです。このケース以外の場合、一時変数の宣言後、一時変数を使用するコードが長くなる場合は、unsetより、関数スコープを使用しましょう。

unsetを乱用することは、CPUのリソース消費を増加させるため、良いコーディングスタイルとは言えません。しかし、unsetで変数がもう定義されなくなる場合、コードを追加する立場から、その変数を使用しようとしても、もう定義されていないため、使用できない一時変数であることが確実にわかります。

スコープと変数シャドウイング(Variable Shadowing)

変数シャドウイングとは、スコープ外の変数をスコープ内で特別なキャプチャ構文なしで使用できるように、スコープ外の変数と同じ名前の変数をスコープ内で宣言し、その名前の変数にアクセスする際に下位スコープで宣言された変数のみにアクセスでき、上位スコープの変数にはアクセスできないようにすることです。

スコープの入れ子が深くなると、特定の変数が上位スコープのどこからきたのか、下位スコープのどこでシャドウイングされたのかを把握しにくい問題があります。変数名が同じであるため、どこで宣言された変数なのかを知っていないと、変数を使用する際に誤った使用を防ぐことができません。JavaScriptにおいて変数宣言キーワードconstが存在するのは、変数シャドウイングを防ぐためです。

PHPでは、関数スコープはuseキーワードを使って関数スコープ外の変数を関数内部に持ち込む必要があり、use文でどの名前の変数をキャプチャするのかが分かるため、関数内部で宣言した変数をuseキーワードで持ってきたものと同じように宣言する可能性は低いです。JavaScriptが意図しない変数シャドウイングを防ぐためにconstを導入したのに対して、PHPのスコープは意図しないシャドウイングが発生しないため、constのような宣言キーワードは必須ではありません。

PHPでは、アロー関数fn($param) => $param + $captureduseキーワードなしで外部の変数をキャプチャできますが、アロー関数は=>の後にすぐに返り値が位置する必要があるため、アロー内で新たに宣言される変数はアロー関数のパラメータとなります。変数の宣言と同時に使用されることがすぐに分かるため、上位スコープの変数を上書きしてもすぐに使用される一時変数であるため、変数シャドーイングが起こってもですロジックに大きな影響を与えることはありません。

さいごに

PHPバックエンド開発をしている場合、JavaScriptを一緒に使うことが多いです。しかし、JavaScriptではうまくいくことが、PHPではうまくいかない、またはその逆のような、PHPとJavaScriptの間で異なる特徴に混乱することがあるかもしれません。

特に、JavaScriptには変数の宣言キーワードvar, const, letがありますが、PHPにはなぜこれらがないのか疑問に思う人もいるでしょう。ブロックスコープや関数スコープの概念と、それに関連した良いコーディングスタイルの理解を深めることで、(ローカル変数に対して)変数宣言キーワードがないPHPコードを特別な不便を感じることなくうまく書ける理由が理解できるでしょう。

PHPに変数宣言キーワードがない理由

  • var: PHPで変数に値を代入することは、varキーワードを使用するのと同じです。
  • let: PHPにはブロックスコープがないため、letの代わりにvarを使用すれば問題ありません。
  • const: 意図しない変数のシャドーイングが発生することはなく、関数のコードを短く書けば自分でも気づかないうちに変数を再定義してしまうこともないため、無理にconstキーワードを導入する必要はありません。

PHPのアロー関数にブロックがない理由

また、PHPのアロー関数がJavaScriptのアロー関数と異なり、() => {}のようなブロックを使用できない理由も、この内容を理解すれば納得できます。アロー関数にブロックを追加する場合、単なるfunction関数の省略形ではなく、少し異なる機能にするためには、従来のアロー関数と同様にuseキーワードなしでスコープ外の変数を自動的にキャプチャする仕様にするのが良いです。しかし、自動キャプチャを導入すると、変数のシャドウイングを防ぐためにJavaScriptのconstの役割のキーワードをPHPに導入する必要があり、言語仕様が複雑になってしまいます。



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link