Q1
商品の在庫ごとに割り振られる連番の商品 ID を, PHP 上で自動採番するクラスを考えます。下記で使われている static
に関して
- 🔰 どういう役目を持っているでしょうか?
- 🔰 どの PHP バージョン以降で有効でしょうか?
(但し,PHP 5.0 より前には遡りません) - 🔰 出力される商品 ID はどうなるでしょうか?
<?php
class Product
{ // ↓ ①
protected static int $counter = 0;
public readonly int $id;
public function __construct(
public readonly string $name,
public readonly int $price,
) { // ↓ ②
$this->id = ++static::$counter;
}
}
class SpecialProduct extends Product
{ // ↓ ③
protected static int $counter = 100000;
}
// 出力はどうなる?
var_dump(
(new Product(name: 'レンズ', price: 300000))->id,
(new SpecialProduct(name: '高級レンズ', price: 1000000))->id,
(new Product(name: 'カメラ', price: 250000))->id,
);
回答
- ①③ 静的プロパティ です。全インスタンスで共有されます。PHP 5.0 から利用できます。
- ① も ② も
private
ではないため,継承クラスでは ③ が ① を上書きしています。
- ① も ② も
- ② 遅延静的束縛される静的プロパティ のインクリメントです。 PHP 5.3 から利用できます。
-
++static::$counter
が書かれた場所を見ただけでは参照するプロパティが確定しません。実際にnew
演算子によるインスタンス化を記述した場所で, 「どのクラスのコンストラクタが呼ばれるか」 を考慮して参照するプロパティが決定されます。
-
出力
int(1)
int(100001)
int(2)
もし ++self::$counter
であれば,以下のようになってしまいます。
int(1)
int(2)
int(3)
Q2
Q1 の商品に対して,下記のように使える,イミュータブルとミュータブルの特性を持った Product
専用のコレクションクラスをそれぞれ作成するとします。
$products = new ProductCollection(
new Product(name: '洗濯機', price: 250000),
new Product(name: 'パソコン', price: 160000),
);
// 元の値は書き換えない
$updated = $products
->push(
new Product(name: 'レンズ', price: 300000),
new SpecialProduct(name: '高級レンズ', price: 1000000),
)
->push(
new Product(name: 'カメラ', price: 250000),
)
->sort();
$products = new MutableProductCollection(
new Product(name: '洗濯機', price: 250000),
new Product(name: 'パソコン', price: 160000),
);
// 最初にコピーした場合は書き換えずに済む
$updated = $products
->copy()
->push(
new Product(name: 'レンズ', price: 300000),
new SpecialProduct(name: '高級レンズ', price: 1000000),
)
->push(
new Product(name: 'カメラ', price: 250000),
)
->sort();
// コピーしなければ元の値を書き換える
$products
->push(
new Product(name: 'レンズ', price: 300000),
new SpecialProduct(name: '高級レンズ', price: 1000000),
)
->push(
new Product(name: 'カメラ', price: 250000),
)
->sort();
下記で使われている static
に関して
- 🔰 どういう役目を持っているでしょうか?
- 🎓 どの PHP バージョン以降で有効でしょうか?
(但し,PHP 5.0 より前には遡りません)
<?php
class ProductCollection
{
protected array $products;
public function __construct(Product ...$products)
{
$this->products = $products;
}
public function copy(): static
{ // ↓ ①
return new static(...$this->products);
}
// ↓ ③
public function push(Product ...$products): static
{ // ↓ ①
return new static(...$this->products, ...$products);
}
public function all(): array
{
return $this->products;
}
// ↓ ③
public function sort(): static
{ // ↓ ②
$copy = static::copy();
usort(
$copy->products,
// ↓ ④
static fn (Product $x, Product $y): int => $x->id <=> $y->id,
);
return $copy;
}
}
class MutableProductCollection extends ProductCollection
{ // ↓ ③
public function push(Product ...$products): static
{
array_push($this->products, ...$products);
return $this;
}
// ↓ ③
public function sort(): static
{
usort(
$this->products,
// ↓ ④
static fn (Product $x, Product $y): int => $x->id <=> $y->id,
);
return $this;
}
}
回答
- ① 遅延静的束縛されるコンストラクタコール です。PHP 5.3 から利用できます。
-
new static()
が書かれた場所を見ただけではインスタンス化するクラスが確定しておらず,new
演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
-
- ② 遅延静的束縛されるメソッドコールです。 PHP 5.3 から利用できます。
-
static::copy()
が書かれた場所を見ただけではどのクラスのメソッドを呼び出すか確定しておらず,new
演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
-
- ③ 遅延静的束縛される返り値 です。PHP 8.0 から利用できます。
-
: static
が書かれた場所を見ただけでは返り値として拘束する型が確定しておらず,new
演算子で作られたクラスから実際にメソッドを呼ぶ記述をした場所で確定します。
-
- ④ クロージャへの
$this
バインドを行わない宣言 です。 (アロー関数を使わなければ) PHP 5.3 から利用できます。- エッジケースではありますが,明示的に宣言することによって,メモリ使用量の削減に繋がる場合があります。
- ① もし
new self()
であれば,MutableProductCollection
から->copy()
を呼んでもProductCollection
が作られてしまいます。 - ③ もし
: self
であれば,静的解析ツールではMutableProductCollection
の返り値を正しく認識することができません。MutableProductCollection
から->copy()
を呼んでも (実際はMutableProductCollection
になるものの)ProductCollection
であると解析されてしまいます。- 但し,
static
はほぼ静的解析ツールのためだけのものです。 継承先のstatic
型として見なせるものは全て継承元self
型でもある ため,動作上はこの 2 つの間に差異はありません。
- 但し,
- ④ ここではクロージャ内部に
$this
は登場していませんが, 参照していなくても暗黙的に$this
がバインドされています。以下のようなコードを書いた場合はstatic
宣言がないとメモリを大量消費してしまいます。class Example { private readonly array $veryLargeProperty; public function __construct() { $this->veryLargeProperty = array_fill(0, 10000, true); } public function createSizeCalculator(): Closure { $count = count($this->veryLargeProperty); return static fn () => $count; // $this のバインドを回避 } } $callbacks = []; for ($i = 0; $i < 10000; ++$i) { // Example インスタンス自体は破棄できているので, // $count の値を持つクロージャの分しかメモリは消費しない $callbacks[] = (new Example())->createSizeCalculator(); }
Q3
下記で使われている static
に関して
- 🔰 どういう役目を持っているでしょうか?
- 🎓 どの PHP バージョン以降で有効でしょうか?
(但し,PHP 5.0 より前には遡りません) - 🎓 出力はどうなるでしょうか?
<?php
function counter(): int
{// ↓ ①
static $counter = 0;
return ++$counter;
}
// 出力はどうなる?
var_dump(counter());
var_dump(counter());
<?php
class ParentClass
{
public function __construct()
{
// ↓ ②
static $i = 0;
++$i;
// ↓ ③
echo static::class . "::__construct() has been called {$i} time(s)\n";
}
}
class ChildClass extends ParentClass
{
}
// 出力はどうなる?
new ParentClass();
new ChildClass();
new ParentClass();
new ChildClass();
回答
- ①② 静的変数 です。 PHP 5.0 の時点で既に利用できます。
- ①普通の関数の内部でも状態保持のために利用できますし,②プロパティの機能を持っているクラス・オブジェクトもメソッド単位で利用できます。
- ③ 遅延静的束縛されるクラス名文字列 です。PHP 5.5 から利用できます。
-
static::class
が書かれた場所を見ただけでは文字列の内容が確定していません。実際にnew
演算子によるインスタンス化を記述した場所で, 「どのクラスのコンストラクタが呼ばれるか」 を考慮して文字列が決定されます。
-
出力
int(1)
int(2)
ParentClass::__construct() has been called 1 time(s)
ChildClass::__construct() has been called 2 time(s)
ParentClass::__construct() has been called 3 time(s)
ChildClass::__construct() has been called 4 time(s)
引っ掛け問題ですが… static
変数はクラスの継承関係なくメソッドの存在ごとに独立しているので,カウンターは親子で共有されています。
Q4
- 👹 出力はどうなるでしょうか?
<?php
class ParentClass
{
public function __construct()
{
echo static::class . '::__construct() has been called '
. static::increment() . " time(s)\n";
}
public static function increment(): int
{
static $i = 0;
return ++$i;
}
}
class ChildClass extends ParentClass
{
}
// 出力はどうなる?
new ParentClass();
new ChildClass();
new ParentClass();
new ChildClass();
回答
PHP バージョンによって出力が変わります。
PHP 8.1 以降
ParentClass::__construct() has been called 1 time(s)
ChildClass::__construct() has been called 2 time(s)
ParentClass::__construct() has been called 3 time(s)
ChildClass::__construct() has been called 4 time(s)
PHP 8.0 以前
ParentClass::__construct() has been called 1 time(s)
ChildClass::__construct() has been called 1 time(s)
ParentClass::__construct() has been called 2 time(s)
ChildClass::__construct() has been called 2 time(s)
先ほど
static
変数はクラスの継承関係なくメソッドの存在ごとに独立している
と書きましたが,これは PHP 8.1 以降で初めて担保された話です。 PHP 8.0 以前は,コンストラクタとそれ以外のメソッドで一貫性のない動きをしていました。
PHP バージョン | コンストラクタ内での状態保持スコープ | コンストラクタ以外での状態保持スコープ |
---|---|---|
8.1 以降 | メソッド単位で独立 | メソッド単位で独立 |
~8.0 以前 | メソッド単位で独立 | クラス・メソッド単位で独立 |
PHP 8.0 以前の動きを活用したことがあったので,個人的にはこちらの動作が正でコンストラクタの動作がバグであってほしかったのですが,逆になってしまいましたね…
関連記事