静的コンストラクタとは、C#では静的コンストラクタ、Javaでは静的イニシャライザ、KotlinではCompanion objectsと何故か言語ごとにバラバラの名前で呼ばれている機能です。
実は細かい違いがあるのかもしれませんがよくわかりません。
public class Hoge{
static {
System.out.println("hoge");
}
}
このメソッドは、このクラスを初めて使おうとしたときに最大一回だけ実行されます。
使おうとしたときというのはnew()
しようとしたとかstaticメンバーを呼び出そうとしたとかその他諸々です。
複数回呼ばれることはないので、一度だけ実行したいクラス変数の初期化などに使えます。
たとえばC#のドキュメントではログファイルへの接続がサンプルとしてあげられていました。
というわけでPHPにも静的コンストラクタがあると便利なんじゃないかなというRFCが出ていたので見てみることにします。
以下は該当のRFC、Static Constructorsの紹介です。
PHP RFC: Static Constructors
Introduction
PHPでは、静的プロパティを定数でない値で初期化するためには面倒な回避策が必要であり、コードが読みにくくなります。
以下は、いくつかのコードベースで遭遇したコードの例です。
このコードでは、静的プロパティを使用する前に必ず初期化メソッドを呼び出して、適切に初期化されているかどうかを確認する必要があります。
class MyClass
{
private static \DatetimeImmutable $minDate;
public function __construct(
private \DateTimeInterface $scheduledDate
) {
self::initializeMinDate();
if ($scheduledDate < self::$minDate) {
$errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
throw new \DomainException($errMsg);
}
}
private static function initializeMinDate()
{
if(isset(self::$minDate)) {
return;
}
// コンフィグとかから取得
self::$minDate = new \DatetimeImmutable('2024-01-01 00:00:00');
}
// ...
}
このような定型文は可読性が低下し、メソッドを追加したときに初期化メソッドを呼び忘れてバグを誘発する可能性があります。
class MyClass
{
private static \DatetimeImmutable $minDate;
public function __construct(
private \DateTimeInterface $scheduledDate
) {
self::initializeMinDate();
if ($scheduledDate < self::$minDate) {
$errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
throw new \DomainException($errMsg);
}
}
public static function getMinDate()
{
return self::$minDate;
}
private static function initializeMinDate()
{
if(isset(self::$minDate)) {
return;
}
self::$minDate = new \DatetimeImmutable('2024-01-01 00:00:00');
}
// ...
}
// メソッドを増やしたけど初期化し忘れたのでエラー
echo MyClass::getMinDate()->format('d/m/Y H:i:s') . PHP_EOL;
Proposal
静的プロパティの初期化を効率化し、コードの明確化を向上させるため、新たなマジックメソッド__staticConstruct
の導入を提案します。
このメソッドは、クラスの静的プロパティ初期化処理の直後に、PHPエンジンから呼び出されます。
このRFCを提案する前に、ユーザランドオートローダーを利用して同機能を実装できないか検討しましたが、この機能はオートローダーの範囲外であることは明らかでした。
Addressing Potential Criticism
あるデザインパターンで名指しされているように、またテスト可用性の視点から静的プロパティの使用は避けるべきであると言う人もいるかもしれません。
しかし、静的プロパティはPHPに既に存在しており、また一部のシナリオでは役立つことはあきらかです。
このRFCを否定しても、静的プロパティが有益である実際の使用例や自然なアプローチを否定することはできません。
Comparison with Other Object-Oriented Languages
Javaでは、この目的のためにメソッド呼び出しもしくは静的ブロックを使用します。
オブジェクト指向原則に厳格な言語でも、静的プロパティが役立つ場合があり、適切な初期化メソッドが必要であるという事実を示しています。
A real-life example
既に存在する例のひとつとして、ComposerのClassLoaderクラスがあります。
このクラスは、クロージャを使用して静的プロパティ$includeFile
を初期化しています。
ただしコンストラクタ内で行われているため、コードが読みにくくなっています。
// ※実際は長文コメントがあるがコピペにあたり削除している
class ClassLoader
{
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
__staticConstruct
が実装されたら、以下のように書けるようになります。
class ClassLoader
{
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
private static function __staticConstruct()
{
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
静的コンストラクタが導入されると、静的プロパティの初期化が簡素化されるだけでなく、コードの可読性と保守性も向上することを示しています。
このRFCは、静的コンストラクタの導入によってPHPのオブジェクト指向機能を強化することを目的としています。
What this RFC is not
このRFCは、静的プロパティの無差別な使用を勧めるものではなく、静的プロパティが有用な場合に適切な初期化方法を提供することを目的としています。
Implementation on other programming languages
Java
Javaには静的コンストラクタそのものはありませんが、静的プロパティを初期化するための他の方法が用意されています。
1.staticイニシャライザ
import java.util.*;
import java.lang.*;
import java.io.*;
class Whatever {
public static int myVar;
static {
myVar = 10;
}
}
class Main {
public static void main(String[] args) {
System.out.println("Hello world (" + Whatever.myVar + ")!");
}
}
2.変数に直接メソッド書く
import java.util.*;
import java.lang.*;
import java.io.*;
class Whatever {
public static int myVar = initializeInstanceVariable();
protected static int initializeInstanceVariable() {
return 20;
}
}
class Main {
public static void main(String[] args) {
System.out.println("Hello world (" + Whatever.myVar + ")!");
}
}
Kotlin
コンパニオンオブジェクトと@JvmStatic
アノテーションで実装できます。
class Whatever {
companion object {
@JvmStatic
var myVar: Int
init {
myVar = 10
}
}
}
fun main() {
println("Hello, world(" + Whatever.myVar + ")!")
}
C#
静的コンストラクタは修飾子のないstaticメソッドであり、ランタイムからのみ呼び出されます。
using System;
using System.Reflection;
public class C1
{
public static int myVar;
static C1()
{
C1.initStaticProps();
Console.WriteLine("C1::__staticConstructor");
}
public static void initStaticProps()
{
C1.myVar = 10;
Console.WriteLine("Initializing C1::test");
}
public static void test()
{
Console.WriteLine("C1::test");
}
}
public class Program
{
public static void Main()
{
/// どのように呼び出そうとしても静的コンストラクタがトリガーされる
// Reflection
Type c1Type = typeof(C1);
// Instance constructor
var c = new C1();
// Static method call
C1.test();
// Property access
Console.WriteLine("C1::myVar:" + C1.myVar);
}
}
C++
C++17
において、定数式ラムダを使用する方法が提供されました。
#include <iostream>
#include <vector>
class Example {
public:
inline static const std::vector<char> letters = [] {
std::vector<char> letters;
for (char c = 'a'; c <= 'z'; c++) {
letters.push_back(c);
}
return letters;
}();
};
int main() {
for (auto i = 0; i < Example::letters.size(); i++) {
std::cout << "Example::letters[" << i << "]: " << MyClass::letters[i] << std::endl;
}
return 0;
}
Swift
C++同様、クロージャでプロパティの初期化が可能です。
class Example {
static var myVar: Int = {
print("Static initializer called")
return 42
}()
static func display() {
print("Example::myVar: \(myVar)")
}
}
Example.display()
Design decisions
この実装はC#からヒントを得ていますが、C#ほど厳しくはありません。
ユーザは必要に応じて__staticConstruct
メソッドを任意に呼び出すことが可能です。
__staticConstruct
メソッドの可視性はprivate
です。
これにより、意図しない親クラスの静的プロパティのリセットを防ぎます。
このRFCは、以下の2シグネチャをサポートしています。
private static __staticConstruct();
private static __staticConstruct(): void;
__staticConstruct
メソッドに引数はありません。
Exception handling
静的コンストラクタから例外がスローされる可能性があり、ユーザはそれを回復することができます。
どのような対応を行うかはユーザに一任されます。
静的コンストラクタは、PHPエンジンからは2度呼び出されることはありません。
つまり、静的コンストラクタが例外を出した場合、そのクラスは初期化されません。
Example
子クラスのない例。
class MyClass
{
protected static \DatetimeImmutable $minDate;
public function __construct(
private \DateTimeInterface $scheduledDate
) {
if ($scheduledDate < self::$minDate) {
$errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
throw new \DomainException($errMsg);
}
}
public static function getMinDate()
{
return self::$minDate;
}
private static function __staticConstruct(): void
{
self::$minDate = new \DatetimeImmutable('2024-01-01 00:00:00');
}
}
// エラーは出ない
echo MyClass::getMinDate()->format('d/m/Y H:i:s') . PHP_EOL;
子クラスのある例。
class MyClass1
{
protected static \DateTimeImmutable $minDate;
public function __construct(
private \DateTimeImmutable $scheduledDate
) {
if ($scheduledDate < self::$minDate) {
$errMsg = 'Cannot set a scheduledDate before [' . self::$minDate->format('d/m/Y H:i:s') . ']';
throw new \DomainException($errMsg);
}
}
public static function getMinDate()
{
return static::$minDate;
}
public function getScheduledDate(): \DateTimeImmutable
{
return $this->scheduledDate;
}
protected static function initMinDate(): \DateTimeImmutable
{
return new \DatetimeImmutable('2024-01-01 00:00:00');
}
private static function __staticConstruct(): void
{
self::$minDate = self::initMinDate();
}
}
class MyClass2 extends MyClass1
{
protected static \DateTimeImmutable $minDate;
private static function __staticConstruct(): void
{
self::$minDate = parent::initMinDate()->modify('+1 day');
}
}
echo MyClass1::getMinDate()->format('d/m/Y H:i:s'); // 01/01/2024 00:00:00
echo MyClass2::getMinDate()->format('d/m/Y H:i:s'); // 02/01/2024 00:00:00
Backward Incompatible Changes
ユーザランドで__staticConstruct
メソッドを実装していた場合は動かなくなります。
ただし、__
で始まるメソッドはユーザランド実装は推奨されていません。
GitHub内には__staticConstruct
メソッドを実装しているコードは存在しませんでした。
Version
次のマイナーバージョン。
Implementation
https://github.com/php/php-src/compare/master...erickcomp:php-src_static-constructor:master
感想
メーリングリストは意外なことにわりと否定的意見が多めです。
あれば便利だとは思うんですけどどうしてなんでしょうかね。
個人的には、既存実装を参考にするのであれば仕様も既存実装にしたがってほしかったですね。
要するに自力で__staticConstruct
呼べちゃうのはやめようってことですが。
まあPHPにはprivateより狭い可視性ってないからしょうがないのかな。わからんけど。
そんなわけでもしかしたら今後PHP8.5とかで入るかもしれない、あるいはそのまま立ち消えになるかもしれない静的コンストラクタの紹介でした。