はじめに
phpの配列の特徴
phpでは、配列とは一つのタイプで、[1,2,3,4,5]
と['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5]
の二つの方式を使用できます。最初のはインデックス付き配列で、2番目は連想配列と呼ばれます。
静的型付け言語では、インデックス付き配列はキーが存在しません。メモリ上の特定の位置にデータを一定のバイト間隔で順次に読み込みます。あるインデックスの値にアクセスする際には、1つの要素が持つバイト数分だけ「インデックス番号 x 1つの要素が占めるバイト数」を飛び越える方式で処理するため、非常に高速です。
phpのインデクシング配列は連想配列と内部処理は同じように処理されます。というのは、[1,2,3,4,5]
が[0 => 1, 1 => 2, 2 => 3, 3 => 4, 4 => 5]
と同じであるためです。インデックス付き配列として作成しても、結局phpではすべて連想配列として処理されます。
phpの配列はインデックス付き配列でも連想配列でも、内部的にはハッシュテーブルを使用して処理方式が同じであるため、区別なく同じ「array」タイプとして扱われます。
機能のスペック問題
関数やクラスを定義するのは、仕様を作成する作業です。関数の名前、パラメーター、タイプをすべて含めて、機能のインターフェースを示す用語を関数の「シグネチャ(signature)」と呼びます。関数のシグネチャでパラメータとして配列を使用すると配列内のどの値を利用すべきかシグネチャだけでは分かりません。内部のコードを確認する必要があるという欠点があります。関数やクラスはカプセル化されたシグネチャだけを通じて、そのシグネチャがどのような機能を示しているのか分かるように作る必要があります。配列を渡すならどのような構造の配列を受け取るべきか知っておく必要があります。しかし、配列のタイプヒントのパラメータで引数を受け取る場合、配列で受け取ることができる形の多様性により、どの値を受け取るべきか曖昧なことが多いです。
複雑なソフトウェアを作るほど、各単位機能のテスト可能性は重要です。一般的にテストが可能であるということは、純粋関数のようにインプットに対してアウトプットを持つことができ、独立して取り出したときにインプットを入れるとアウトプットの結果を確認できることを意味します。テスト可能な機能はどのような役割を果たすか予測しやすく、予測どおりに動作するかどうかを確認するためのテストコードを作成しやすいという利点があります。しかし、関数が配列を入力として受け取るとき、どのような形の配列を入力として受け取るのかがわからないため、関数の動作を確認する際に曖昧な場合があります。
テストしやすいコードにするためには、関数やクラスのシグネチャだけで、どんな入力を与えたときにどんな結果が得られるかを確認できる必要があります。配列の場合、その構造の形がいろいろあるため、どのような値を入れるべきか考えるのが難しいケースが多いです。結局、関数が配列を引数として受け取る場合、どの入力に対してどのような結果が得られるか予測しにくい場合が多く、内部のコードを確認しなければならないコードを作る可能性があります。
インプットに対するアウトプットが明確な仕様なら、テストコードを作成しやすく、機能がどのように動作するかを確認するのがしやすいです。しかし、配列をパラメータとして使用する場合、どのような構造の配列を渡すべきか機能の仕様だけではわからないため、動作確認及びテストしにくいコードになります。
関数に渡された配列を内部でどのように処理するか、どのような値を渡せば機能の仕様に合う動作を作り出すのかを確認するためにシグネチャだけでは十分でなく、内部のコードの処理過程を確認する必要があるならば、値の変化の様相を確認するためのデバッグがより多く必要になり、管理が難しいコードになります。プログラミングにおいて抽象化は内部のロジックを確認しなくてもどのような機能かある程度推論できるようにすることですが、phpの配列を関数のパラメーターとして受け取ると、関数やメソッドのシグネチャだけではどのような形の配列を値で渡すべきかがわからなくなるという問題が生じます。
記事作成の目的
単純なプログラムを書く場合はすべてのコードを読んでも問題ありませんが、コードの長さが増え、プロジェクトに参加する人数が増え、複雑なロジックを処理する必要がある場合、抽象化を導入する場合、すべてのコードを読んで解釈することは非効率です。プロジェクトを管理し、成長させるためには、コードが明確で理解しやすく、テストしやすく、再利用しやすい必要があります。多くのプロジェクトではこれらを達成するためのさらに多くのルールを導入しています。
これを達成する方法の一つは、関数のシグネチャだけで関数の機能を推測できるようにし、関数の内部コードを知らなくても使用できるように抽象化することです。関数を抽象化する際に配列を渡す必要がある場合、関数が要求する配列の構造を明確に表現する方法についていくつか紹介します。それぞれに長所と短所があるため、状況に応じて適切なソリューションを採用してください。
本論
文字列パラメータの問題
文字列パラメーターの例を考えてみましょう。一般的なコーディングスタイルでは、文字列に対して何らかの一般的な処理をするのではなく、何らかの識別子として文字列を使用するコーディングスタイルは好まない場合が多いです。型によって制限される方式とは異なり、文字列パラメーターは文字列に誤りが発生した場合、ランタイムでエラーを検出できないことがあり、IDEがコーディングミスを通知しないこともあります。コンパイル言語は、コンパイル時のタイプチェックまたはIDEで使用するコードを定義されたコードに移動する機能などを使用して、コーディングミスを減らすために、文字列パラメーターの使用を推奨しないコーディングプラクティスが存在します。
無条件に文字列パラメーターを使用しないというわけではありません。一般的な文字列に対する処理を行う場合には、文字列をパラメーターとして渡すのは問題ありません。しかし、関数内部で特定の文字列に対してのみ別途の処理をする際は、関数は特定の文字列に依存する処理を持ちます。このような場合、文字列は何らかの状態を区別する識別子の役割をしますが、それを文字列として処理するとミスを誘発する可能性が高まります。
識別子の用途に文字列を使用するのではなく、enumを使用したりカスタムクラスを作成してnew CustomType(文字列)
のコンストラクターに文字列を渡し、__invoke
マジックメソッドを通じてバリデーションされた文字列を使用する方法をお勧めします。文字列は非常に多様な値になるため、識別子として文字列を使用する場合、ランタイムやIDEなどのツールを使用したコーディングミス防止機能が動作しない可能性が高く、バグを発生させる可能性が高まります。このようなスタイルを制約するために、enumやカスタムクラスを作成し、タイプヒントとしてこれらのタイプを使用することをお勧めします。
enumの例として、enum Gender: string { case Man = 'man'; case Woman = 'woman'; }
$canPregnant = function(Gender $gender): bool { ... }
を考えてみましょう。文字列パラメーターで'man'
や'woman'
を渡す場合、$canPregnant = function(string $gender): bool { ... }
のstring $gender
パラメーターに'man'
や'woman'
を渡すべきか、'm'
や'w'
を渡すべきか、'male'
や'female'
を渡すべきか、'm'
や'f'
を渡すべきか、関数の内部実装を確認しなければわかりません。それに対して、enumを使用すれば関数がどのパラメーターを使用するのか明確な仕様を知ることができます。
文字列が非常に多様な値になれるので、機能の仕様を定義するためにenumやカスタムクラスを作成してタイプヒントとして使用するのと同様に、配列も機能の仕様上の制約を設けるためにタイプヒントを使用する方法を考えるのが良いでしょう。
カスタムタイプ例
class StrYmType
{
private string $yyyymm;
public function __construct(string $yyyymm) {
$year = intval(substr($yyyymm, 0, 4));
$month = intval(substr($yyyymm, 4, 2));
if (checkdate($month, 1, $year)) {
$this->yyyymm = $yyyymm;
} else {
throw new Error('invalid Ym');
}
}
public function __invoke(): string
{
return $this->yyyymm;
}
}
$fn = function (StrYmType $yearMonth) {
echo 'result: '.$yearMonth().PHP_EOL;
};
$fn(new StrYmType('202406')); // result: 202406
$fn(new StrYmType('2024-06')); // Uncaught Error: invalid Ym
上記のコードは、YYYYMM形式の文字列かどうかを確認するクラスを作成し、関数$fn
のパラメータのタイプヒントとして使用しています。
タイプヒントを通じて、特定の文字列のみを受け取ることができるカスタムタイプを作成し、渡される値の種類を制限することができます。
入力値の型と戻り値の型が異なるため、厳密には型と呼べなく、型アサーションのための型と呼べると思います。
一般的な配列処理
phpの配列も文字列と同じような問題を抱えています。配列の形と構造が多様なので、どの構造の配列を引数として渡せばよいかがわからないという問題があります。文字列が識別子として使用される場合、どの文字列が機能内部の特別な処理ケースを持つか確認する必要があるように、配列もどの構造の配列を渡せば使用しようとしている機能で望む結果を得られるかは、内部コードを確認しなければわからない問題があります。
配列をパラメーターとして使用する機能がある場合、配列の構造に関係なく、すべての配列の構成に対して動作する仕様を作るのが良いです。特定の配列の構造に依存するのではなく、配列のすべてのデータを巡回する方式の処理を行う機能で作ります。また、特定のキーに依存する機能や特定のキーの値に対する特別な処理を行う場合、どのキーにどの処理を行うかがわからないため、内部のコード動作の特別な処理を確認しない限り、機能の動作結果を予測するのが難しくなります。
もし配列の特定のキーや特定の値に対する処理を行うことなら、特定のキーを指定して処理できるように$key
や$needle
のようなパラメーターの関数を作成し、機能の仕様(関数の名前、パラメーター名・タイプ、戻り値など)で内部の特別な処理を明確に示すように定義する必要があります。
$add10 = function (array $arr): array {
array_walk($arr, function (int $value, string $key) use (&$arr) {
$arr[$key] = $arr[$key] + 10;
});
return $arr;
};
var_dump($add10(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]));
配列のすべての要素に10を加える上記のような関数があるとしましょう。しかし、ある要件が追加され、bキーには20を加えなければならない状況が生じました。
$add10 = function (array $arr): array {
array_walk($arr, function (int $value, string $key) use (&$arr) {
if($key === 'b') $arr[$key] = $arr[$key] + 20;
else $arr[$key] = $arr[$key] + 10;
});
return $arr;
};
var_dump($add10(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]));
上記のように既存の関数に特別な追加処理を行うと、$add10
という関数名は誤ったものになります。このような場合、関数の既存の意味をできるだけ維持する方法でコーディングするか、それとも関数名を変更する必要があります。
関数名を変更すると「add10But20WhenB」という名前の関数になります。関数名が変わることは機能が変わることです。一つの関数が複数のコードで使用されている場合、関数の機能が変わると依存している他のコードの動作に影響を与える可能性があります。したがって、関数名を変更する方法もありますが、新しい関数を作成する方法で解決できます。
$add10 = function (array $arr): array {
array_walk($arr, function (int $value, string $key) use (&$arr) {
$arr[$key] = $arr[$key] + 10;
});
return $arr;
};
$add10WhenKeyExisted = function (array $arr, string $key): array {
if (array_key_exists($key, $arr)) $arr[$key] += 20;
return $arr;
};
var_dump($add10WhenKeyExisted($add10(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]), 'b'));
add10WhenKeyExisted
という関数を作成し、特定のキーに対する変更を行えるようにしました。このように配列を扱う際には、特別な処理を行う関数よりも、一般的に動作する関数を作成して使用すると、複数のコードの文脈で関数の動作を直接変更するのではなく、複数の関数を組み合わせてコードを作成することが可能になります。
強い型の言語では、タイプが合わないすべてのコードをコンパイラまたは型チェッカーで確認して、変更されたコードの影響を簡単に確認して修正できますが、型処理が弱い言語では、共通のコードの動作を変更すると、それを使用しているすべてのコードを実行して動作を確認する必要があります。共通で使用されるコードを変更するよりも、一般的に動作する関数を作成し、上記のように組み合わせる方法で使用すれば、共通機能をそのままにして、必要な部分にのみ関数を追加して動作を変更できるため、メンテナンスの安定性が向上するという利点があります。
上記の例の関数は、関数名add10WhenKeyExisted
とパラメーターarray $arr
、string $key
及び戻り値:array
だけを見ても、配列の指定したキーの値が存在する場合、指定したキーの値に10を加えた結果を出す、入力として与えた配列と同じ構造の配列を返すことが予測できます。このように配列を扱う際には、配列の形態に依存することより、配列に対する一般的な処理を行う機能を作成することで、動作を予測できるコードを作成することができます。
特殊な配列の形式に依存する場合
JavaScriptで特殊な配列形式を処理する方法
JavaScriptの配列はphpのようにキーと値の形式ではなく、インデックスキーのみあり、文字列キーはありません。文字列キーがある概念はオブジェクトです。phpの連想配列は、JavaScriptの配列とオブジェクトの両方の性質があります。
JavaScriptには分割代入パターン(デストラクチャリング代入パターン・Destructuring Assignment Pattern)という概念があります。以下は分割代入パターンの例です。
const fn = ({a, b, c, d}) => console.log(a, b, c, d);
fn({a: 1, b: 2, c: 3, d: 4}); // 1 2 3 4
JavaScriptの関数パラメータとしてオブジェクト形式で値のみ抜き出された{a, b, c, d}
のようなパラメータを使うと、fn({a: 1, b: 2, c: 3, d: 4})
では「a「が「1」、「b」が「2」、「c」が「3」、「d」が「4」の値を持ちます。また、fn({a: 1, b: 2, c: 3})
では「a」が「1」、「b」が「2」、「c」が「3」、「d」が「undefined」の値になります。fn({a: 1, b: 2, e: 3})
の場合は「a」が「1」、「b」が「2」、「c」が「undefined」、「d」が「undefined」の値になります。
const fn = ({a, b}, c, d) => console.log(a, b, c, d);
fn({a: 1, b: 2}, c: 3, d: 4); // 1 2 3 4
上記のように、一部のパラメータは分割代入パターンを利用して引数として形式が決まるオブジェクトで受け取り、一部のパラメータは通常の値として受け取ることができます。JavaScriptでは分割代入パターンを通じて、特殊な形式のオブジェクト構造に依存する仕様の機能を作ることができます。
特定のキーを持つ配列を処理する方法
$fn = function (array $arr) {
foreach($arr as $e) {
var_dump($e);
}
};
$fn(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]);
phpでは、javaScriptの分割代入のように特定の形状の配列を受け取ることはできません。したがって、特定の構造の配列を受け取りたい場合は、配列を使わず、受け取りたい要素を全てパラメータとして指定する方法を使用します。
配列内部の特定のキーの値が重要である場合、パラメータとして特定のキーを含む配列を受け取るのではなく、配列のキーの値をパラメータに引数に渡すことも、処理対象を明確にすることができるので良い方法です。また、配列のキーをパラメータ名として定義しても、phpでは引数として配列を渡して、配列のキーに対応するパラメータ名で配列のキーに応じる値を引数で引き取られます。
$fn = function (int $a, int $b, int $c, int $d) {
var_dump($a);
var_dump($b);
var_dump($c);
var_dump($d);
};
$fn(...['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]);
echo "=========================================".PHP_EOL;
$fn(...['a' => 1, 'b' => 2], ...['c' => 3], ...['d' => 4]);
echo "=========================================".PHP_EOL;
$fn(...['a' => 1, 'b' => 2], ...['c' => 3], d: 4);
echo "=========================================".PHP_EOL;
$fn(...['a' => 1, 'b' => 2], c: 3, d: 4);
echo "=========================================".PHP_EOL;
$fn(1, 2, ...['c' => 3, 'd' => 4]);
echo "=========================================".PHP_EOL;
$fn(...['a' => 1, 'b' => 2], ...['c' => 3], 4); // Fatal error: Cannot use positional argument after argument unpacking
配列 ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]
をスプレッド構文...
を使って関数の引数として渡すと、関数のパラメータにパラメータと同じ名前のキーに応じる配列の要素がマッピングされることがわかります。上記の例では、...['a' => 1, 'b' => 2]
として引数を渡すと、配列のキーがパラメータの名前と一致するように渡されるため、 'a' =>
は$a
に、 'b' =>
は $b
に値が渡され、$a
は1、$b
は2の値が入ります。
また、 ...['a' => 1, 'b' => 2]
で一部の引数をパラメータ名を指定して渡す場合は、 ...['c' => 3], ...['d' => 4]
や ...['c' => 3], d: 4
のように、続く引数もパラメータ名を指定して渡す必要があります。引数を渡すときにパラメータ名を指定する方法には、連想配列のキーをパラメータ名として指定した配列を渡す方法と、 c: 3, d: 4
のように「パラメータ名: 渡す引数」 の形式で特定のパラメータを指定して渡す方法があります。どちらも名前を指定して渡す方法であり、引数を列挙するときのカンマ(,)を使って混用することができます。パラメータ名を指定して渡す方法を公式な用語で「名付けられた引数」(named arguments)と呼びます。
関数の引数を指定する際にパラメータ名を指定する方法を使用することで、特定の構造の配列を渡すことができます。しかし、一次元配列の構造化しかできないという欠点があり、パラメータ名が指定された引数と指定されていない引数を混用するためには、 $fn(1, 2, ...['c' => 3, 'd' => 4])
のように 1, 2
というパラメータ名が指定されていない引数を先に列挙し、 ...['c' => 3, 'd' => 4]
というパラメータ名が指定された引数を後に配置する必要があります。名前が指定された引数を先に配置し、名前が指定されていない引数を後に配置することはできないため、引数の順序に制限があるという欠点があります。また、どのパラメータからどのパラメータまでがスプレッド構文 ...
で渡す配列なのかを判別することができないという問題もあります。(次の内容で記述)
スプレッド連想配列パラメーターを区別する方法
クロージャを使用
$fn = function (int $a, int $b): Closure {
return function (int $c , int $d) use ($a, $b) {
var_dump($a);
var_dump($b);
var_dump($c);
var_dump($d);
};
};
$fn(1, 2)(...['c' => 3, 'd' => 4]);
上記のように、配列を受け取る対象とそうでない対象を区別するためにクロージャを使用して、一般的な値を受け取る対象と配列の値を受け取る対象を明確に分離する方法もありますが、返されるクロージャ関数の仕様も把握する必要があるなど、複雑になるというデメリットがあります。
コメントを使用する
$fn = function (int $a, int $b, /*[*/ int $c, int $d /*]*/) {
var_dump($a);
var_dump($b);
var_dump($c);
var_dump($d);
};
$fn(1, 2, ...['c' => 3, 'd' => 4]);
上記のように、コメントを使用して配列が渡される部分を表記する方法があります。次のように改行を入れて区別感を出す方法もあります。
コメントと改行使用する
$fn = function (
int $a, int $b,
int $c, int $d, // ...[]
) {
var_dump($a);
var_dump($b);
var_dump($c);
var_dump($d);
};
$fn(1, 2, ...['c' => 3, 'd' => 4]);
しかし、このようなコメント処理方法は一般的ではないため、プロジェクトや部署ごとにコメント処理規則を作って使用する必要があります。
パラメーター名に特別な命名法使用する
$fn = function (
int $a, int $b, int $_c, int $_d,
) {
var_dump($a);
var_dump($b);
var_dump($_c);
var_dump($_d);
};
$fn(1, 2, ...['_c' => 3, '_d' => 4]);
このように、配列のキーをパラメータの値として割り当てる場合、配列のキー名に特別なルールを設けて区別する方法もあります。
連想配列の特定のキーに依存する機能であれば、機能の動作を推論するのが難しいため、配列型のパラメーターを使用する方法の代わりに、渡される配列を構成するキーをパラメーターとして列挙する方法を検討することができます。
可変パラメータで配列を受け取る方法
phpでは、関数は可変関数の構文(RFC: Syntax for variadic functions)の可変パラメータ(variadic parameter)を使用して配列を受け取ることができます。可変引数(variadic arguments)を受け取るとは、一つのパラメータが受け取ることができる引数の数に制限なく続けて受け取ることができる構文です。パラメータに「...配列」の構文を使用して、可変引数を受け取るパラメータを作成することができます。
$fn = function (int $a, int $b, int ...$c) {
var_dump($a);
var_dump($b);
var_dump($c);
};
$fn(1, 2, 3);
// int(1) int(2) array(1) { [0]=> int(3) }
echo "=========================================".PHP_EOL;
$fn(1, 2, 3, 4, 5);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
echo "=========================================".PHP_EOL;
$fn(1, 2, 3, 4, 5, 6, 7);
// int(1) int(2) array(5) { [0]=> int(3) [1]=> int(4) [2]=> int(5) [3]=> int(6) [4]=> int(7) }
echo "=========================================".PHP_EOL;
$fn(1, 2, ...[3,4,5]);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
パラメータを...$c
として設定することで、$fn(1, 2, 3)
, $fn(1, 2, 3, 4, 5)
, $fn(1, 2, 3, 4, 5, 6, 7)
と引数の数を増やしても、引数を受け取ることができます。
...$c
に型を付けることができますが、そうすると可変引数で受け取るすべての引数はタイプが指定さます。パラメータがint ...$c
として定義されている場合、...$c
に該当するすべての引数は整数型でなければなりません。
(phpにはありませんが)一部の言語ではジェネリックという文法を提供しています。ジェネリックを使用すると、配列を使用する際に<T>[]
と型を設定することで、配列のすべての要素の型をT型に強制することができます。
可変パラメータに型を付け、スプレッド構文で配列を引数として受け取ることで、配列にジェネリックを付けて要素の型を制限するような設定ができます。
短所としては、可変パラメータはパラメータを列挙する際に必ず最後に位置し、必ずパラメータの中で一つだけ可変パラメータとして指定できます。
可変パラメータは引数を引き続き受け取ることができるため、可変パラメータの後に定義されたパラメータの引数を受け取ることができません。次の例を見てみましょう。
$fn = function($a, ...$b, $c) {
var_dump($a);
var_dump($b);
var_dump($c);
};
$fn(1, ...[2, 3, 4], 5);
// Fatal error: Only the last parameter can be variadic
スプレッド構文の配列を可変パラメーターに渡せない場合
$fn = function (int $a, int $b, int ...$c) {
var_dump($a);
var_dump($b);
var_dump($c);
};
$fn(1, 2, 3);
// int(1) int(2) array(1) { [0]=> int(3) }
echo "=========================================".PHP_EOL;
$fn(1, 2, 3, 4, 5);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5)
echo "=========================================".PHP_EOL;
$fn(1, 2, 3, 4, 5, 6, 7);
// int(1) int(2) array(5) { [0]=> int(3) [1]=> int(4) [2]=> int(5) [3]=> int(6) [4]=> int(7) }
echo "=========================================".PHP_EOL;
$fn(1, 2, ...[3,4,5]);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
echo "=========================================".PHP_EOL;
$fn(1, 2, ...['c' => 3]);
// int(1 int(2) array(1) { ["c"]=> int(3) }
echo "=========================================".PHP_EOL;
$fn(1, 2, ...['c' => [3, 4, 5]]);
// Argument #3 must be of type int, array given
echo "=========================================".PHP_EOL;
$fn(1, 2, c: ...[3, 4, 5]);
// Parse error: syntax error, unexpected token "..."
echo "=========================================".PHP_EOL;
$fn(a: 1, b: 2, ...[3, 4, 5]);
// Cannot use argument unpacking after named arguments
可変パラメーターは引数の数に制限がないという基本的に意味があります。可変パラメーターに配列をスプレッド構文...
を使用して$fn(1, 2, ...[3,4,5])
のように割り当てる方法を通じて、インデックス付き配列を受け取ることができます。また、 $fn(1,2,3)
、 $fn(1,2,3,4)
、 $fn(1,2,3,4,5)
のように可変パラメーター引数を複数受け取れます。 つまり、関数に可変パラメーターを使用したことは引数を複数受け取ることができるということと、スプレッド構文を使用して配列を受け取ることができるという二つの意味に解釈できます。
$fn(1, 2, ...[3, 'key' => 4, 5]);
のように配列にキーが指定された値を送ると、キーと一致するパラメーター名をマッチング探します。可変パラメーターはパラメーターの最後に位置するため、$fn(1, 2, 可変引数)
のように可変パラメーター以外の引数はあらかじめすべて指定した状態で、その最後に可変引数を割り当てます。この場合、...['c' => '3']
で値を渡すことはできますが、...['c' => '3', 4, 5]
はパラメーター名を指定して渡す方法('c' => '3'
)の後にパラメーターを指定しない方法(4, 5
)で渡すことになるため、文法的に不可能です。パラメーターを指定しない方法である(3, 4
)の次にパラメーター名を指定する方法('c' => 5
)を使うと混在させることができるので、...[3, 4, 'c' => 5]
で使用すると、キーを指定する方式とキーを指定しない方式を混用できます。
$fn(1, 2, ...['c' => [3, 4, 5]])
のようにパラメーター名を指定して可変パラメーターに値を送る場合、可変パラメーター...$c
のタイプをintにすると、[3, 4, 5]
が配列として渡され、タイプエラーが発生します。しかし、$fn(1, 2, ...['c' => ...[3, 4, 5]])
を使用すると、...['c' => ...[3, 4, 5]]
と...[3, 4, 5]
の...
は異なる文法で、前者の...
は配列を展開して引数に割り当てる文法ですが、後者の...
は配列を展開するため、配列のキーと値ペアで値として単一の値が必要ですが、複数の値が割り当てられるため文法エラーが発生します。
このような文法的な制約のため、可変パラメーターとして定義されたパラメーター(int ...$c
)にパラメーター名を使用してスプレッド構文の配列を渡すことはできません。また、$fn(1, 2, c: ...[3, 4, 5])
のようにパラメーターを指定して可変引数に配列をスプレッド演算子で渡すこともサポートされていない文法です。パラメーター名を指定して引数を割り当てる方法では可変パラメーターに引数を割り当てることができないため、可変パラメーターを使用してスプレッド構文で配列を渡したい場合、パラメーター名を使用して引数を渡す方法を使用せず、コンマ(,)で引数を並べる方法で...配列
を渡す必要があります。
可変パラメーターにスプレッド構文の配列を渡す
可変パラメーターに名前を付けてスプレッド構文の配列を渡すことはできないと前述しましたが、方法がないわけではありません。可変パラメーターには名前を指定せずにスプレッド構文の配列を使用し、残りの引数は名前付き引数としてスプレッド構文で渡す方法があります。次の例を見てみましょう。
$fn = function (int $a, int $b, int ...$c) {
var_dump($a);
var_dump($b);
var_dump($c);
};
$fn(1, 2, ...[3, 4, 5]);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
echo "=========================================".PHP_EOL;
$fn(1, ...['b' => 2], ...[3, 4, 5]);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
echo "=========================================".PHP_EOL;
$fn(...['a' => 1, 'b' => 2], ...[3, 4, 5]);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
echo "=========================================".PHP_EOL;
$fn(...['a' => 1], ...['b' => 2], ...[3, 4, 5]);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
echo "=========================================".PHP_EOL;
$fn(...['a' => 1], ...[3, 4, 5], ...['b' => 2]);
// int(1) int(2) array(3) { [0]=> int(3) [1]=> int(4) [2]=> int(5) }
echo "=========================================".PHP_EOL;
$fn(...[3, 4, 5], ...['b' => 2], ...['a' => 1]);
// Uncaught Error: Named parameter $b overwrites previous argument
echo "=========================================".PHP_EOL;
$fn(1, ...[3, 4, 5], ...['b' => 2]);
// Uncaught Error: Named parameter $b overwrites previous argument
まず、スプレッド構文を使った連想配列でパラメーター名を指定して引数を渡し、その後スプレッド構文でパラメーター名を指定しない配列を渡すことで可変パラメーターに引数を渡す方法を使います。
$fn(1, ...['b' => 2], ...[3, 4, 5])
の例では、最初の引数はパラメーター名を指定せず、次の引数からパラメーター名を指定しています。そして、可変パラメーターはパラメーター名を指定していないことがわかります。$fn(...['a' => 1, 'b' => 2], ...[3, 4, 5])
の例でも、可変パラメーター以外のパラメーターはスプレッド構文が付いた連想配列を使用し、可変パラメーターはインデックス付き配列を使用することで、パラメーターの名前を指定する方式でも可変パラメーターのみ名前を指定しない方式でインデックス付き配列をスプレッド構文で可変パラメーターに渡すことができます。
ただし、可変パラメーターを最初の引数として渡してはいけません。例えば、$fn( ...[3, 4, 5], ...['b' => 2], ...['a' => 1])
の例を見てみましょう。可変パラメーターを最初の引数として渡すと、$a
に「3」、$b
に「4」、$c
に「5」が割り当てられて、すでに$b
の値が渡された状態で...['b' => 2]
で再度$b
に値を渡すことになり、「Named parameter $b overwrites previous argument」というエラーが発生します。同様に、$fn(1, ...[3, 4, 5], ...['b' => 2])
でも、$a
に「1」、$b
に「3」、$c
に[4, 5]
を割り当てた後、...['b' => 2]
で再度$b
に値を渡すことになるため、同じエラーが発生します。
可変パラメーターは関数のパラメーターの中で一つだけ指定されて、最後のパラメーターとして定義しなければなりません。名前付き引数を一つでも渡した後は、可変パラメーターを除く残りの引数も名前付き引数として渡す必要があります。結局のところ、残りは名前付き引数として渡されるため、可変パラメーターに渡すスプレッド構文の配列は最大一つだけ存在し、どの順序でも位置することができます。$fn(...['a' => 1], ...[3, 4, 5], ...['b' => 2])
で、...[3, 4, 5]
が最後の位置に渡されなくても動作することが確認できます。
可変パラメーターは一つしか使用できないし、名前付き引数の後にスプレッド構文の配列で可変パラメーターを渡さないといけないという制約がありますが、可変パラメーターを使用することで、配列要素の型が指定された配列を関数の宣言部で定義できます。
可変パラメータを使用する際の注意点 1
スプレッド構文の連想配列を引数として渡すとき、連想配列のキーと一致するパラメータ名がある場合キーの値を一致するパラメーターに渡します。しかし、連想配列のキーにマッピングされるパラメータ名がない場合、すべて可変パラメータに渡されます。このため、可変パラメータにはスプレッド構文で連想配列を関数に渡す時、パラメータ名に存在しない残りの値が渡される可能性があるという欠点があります。
$fn = function ($a, $b, int ...$c) {
var_dump($a);
var_dump($b);
var_dump($c);
};
$fn(1, ...['b' => 2, 'c' => 3], ...['d' => 4, 'e' => 5]);
// int(1) int(2) array(3) { ["c"] => int(3) ["d"] => int(4) ["e"] => int(5) }
パラメータは$a
, $b
, $c
が存在します。連想配列のキーとして「b」, 「c」, 「d」, 「e」が渡されました。最初の引数1は$a
にマッピングされ、スプレッド構文で渡された配列のキー「b」は$b
に渡されます。しかし、連想配列のキー「c」, 「d」, 「e」はすべて$c
に配列のキーとバリューのペアとして渡されることが確認できます。スプレッド構文とともに使用された連想配列のキーにマッチするパラメータ名がない場合、すべて可変パラメータとして渡されました。
すなわち、可変パラメータに渡す意図がない値もスプレッド構文で渡された配列のキーがマッチするパラメータが存在しないミスが生じると、可変パラメータに渡す意図がない値も可変パラメータに入ってしまう問題点があります。
また、名前付き引数と可変パラメータに渡す値を連想配列で区別することが難しいことが$fn(1, ...['b' => 2, 'c' => 3], ...['d' => 4, 'e' => 5])
でわかります。このため、可変パラメータには連想配列よりもインデックス付き配列を渡して区別感をあるように設定などの考慮も必要です。
このような問題点のため、可変パラメータを使用するときは、関数のパラメータ数を少なくしてミスの余地を少なくしたときに使用するのが良いです。
可変パラメータを使用する際の注意点 2
$fn = function ($a, $b, int ...$c) {
var_dump($a);
var_dump($b);
var_dump($c);
};
$fn(1, 2, 3, 4); // int(1) int(2) array(2) { [0]=> int(3) [1]=> int(4) }
$fn('1', '2', '3', '4'); // string(1) "1" string(1) "2" array(2) { [0]=> int(3) [1]=> int(4) }
$fn('1', '2', '3', '4')
の部分を見ると、パラメーター $c
の値として array(2) { [0]=> int(3) [1]=> int(4) }
の値が渡される。引数が渡された際にパラメーターの型と一致しない場合、phpは暗黙の型変換を行います。
暗黙の型変換は意図しない結果を招くことがあります。例えば、'123abc'
が'123'
に変換されることでバグを引き起こす可能性があります。可変パラメーターに型を制限する場合、ストリクトモードを使用して暗黙の型変換を防ぐことをお勧めします。
declare(strict_types=1);
$fn = function ($a, $b, int ...$c) {
var_dump($a);
var_dump($b);
var_dump($c);
};
$fn(1, 2, 3, 4); // int(1) int(2) array(2) { [0]=> int(3) [1]=> int(4) }
$fn('1', '2', '3', '4'); // Fatal error: Uncaught TypeError: {closure}(): Argument #3 must be of type int, string given
declare(strict_types=1)
でストリクトモードで実行すると、渡された引数とパラメーターの型が異なる場合、エラーが発生させます。
phpdocを使用する
phpdocには「array shape」という機能があります。これはコメントを通じて配列の形を示すもので、パラメーターとして配列を使用する際にどのような形の配列を受け取るかを示すことができます。
array shape
array-shapesのドキュメントは、phpstanという静的解析ツールを使用する際に、静的解析による推論で正しい値が入っているかを確認できる機能を提供します。phpstorm IDEのarray-shapeサポートにより、phpdocのarray-shapes構文を使用する際、コードで指定した値の型やアクセス方式などの推論による自動補完が強化されました。
/**
* @param $arr array{c: int, d: int}
*/
$fn = function (int $a, int $b, array $arr) {
var_dump($a);
var_dump($b);
var_dump($arr['c']);
var_dump($arr['d']);
};
$fn(1, 2, ['c' => 3, 'd' => 4]);
上記のようなコードを書くと、IDEによって $arr[|]
(|はカーソルのポインター)を入力するだけで$arr['c']
、$arr['d']
の自動補完が表示されます。このような方式で、配列にどのような値が入っているかを知ることができます。
ただし、このようなサポートはあくまでコメントによるものであり、コードの変更に伴ってコメントが更新されない場合があります。そのため、個人ではphp文法による制約が実現されることを望んでいます。単に配列の構造を記述できないのが不満なので仕方なく使う一つの手段に過ぎません。
docblock generic
/**
* @param $arr array<int, string>
*/
$fn = function (int $a, int $b, array $arr) {
var_dump($a);
var_dump($b);
var_dump($arr[0]);
var_dump($arr[1]);
};
$fn(1, 2, ['c', 'd']);
ジェネリックを使って上記のように記述することもできます。phpではインデックス付き配列のキーは整数なので、ジェネリック表示 <int, string>
の最初の型はキーの型を意味し、インデックスを示すためintを使用し、2番目の型にはキーに対応する値の型として<int, string>
でコメントを付けました。
array shape, docblock genericの制約事項を設定する
単にコメントだけで表記するため、文法的な制約がないという問題点に対する代替として、関数の内部コードの最上行に前提条件を付ける方法を使用することができます。
静的解析による検査は時間がかかるし、プロジェクトに静的解析が導入されてない場合、簡単にコードを動作させることで間違った形式か確認するための方法を提供します。
/**
* @param $arr array{c: int, d: int}
*/
$fn = function (int $a, int $b, array $arr) {
assert(array_key_exists('c', $arr), "Key 'c' does not exist in the array.");
assert(is_int($arr['c']), "Value of 'c' key must be an integer.");
assert(array_key_exists('d', $arr), "Key 'c' does not exist in the array.");
assert(is_int($arr['d']), "Value of 'd' key must be an integer.");
var_dump($a);
var_dump($b);
var_dump($arr['c']);
var_dump($arr['d']);
};
/**
* @param $arr array<int, string>
*/
$fn = function (int $a, int $b, array $arr) {
assert(array_reduce(array_keys($arr), fn($acc, $v) => $acc && is_int($v), true), 'All array keys must be integers.');
assert(array_reduce($arr, fn($acc, $v) => $acc && is_string($v), true), 'All array values must be strings.');
var_dump($a);
var_dump($b);
var_dump($arr[0]);
var_dump($arr[1]);
};
$fn(1, 2, ['c', 'd']);
assert文法はローカルやテスト環境で動作し、実際のサーバーでは動作しないように設定した後使用する文法です。配列に要素が多い場合、すべての要素を巡回してキーや値を確認しなければならないため、リソースが多くかかります。したがって、assertで配列の制約事項を確認できるコードを書くのが適切です。(phpでassertが実際のサーバーで動作しないようにするには、php.iniの設定が必要です。)
assert文法はコードの始点に書いて関数の宣言部と一緒に読むことができるようにするのが良いです。
DTOを使用する
DTOとはData Transfer Objectの略称です。データ転送オブジェクトは、データを伝達する際の規格を定めます。DTOはデータを伝達するキャリアの役割なので、ビジネスロジックやバリデーション以外のアルゴリズムが含まれない値のみを持つようにします。
phpではクラスを使用して型を作成することができます。クラスを作成し、パラメータにそのクラスをタイプヒントとして使用し、作成したオブジェクトを引数として渡すことで、受け取る引数の仕様を明確にすることができます。
$fn = function($a, $b, Dto $cdObj) {
var_dump($a);
var_dump($b);
var_dump($cdObj->c);
var_dump($cdObj->d);
};
$fn(1, 2, new Dto(3, 4));
echo "=========================================".PHP_EOL;
$fn(1, 2, new Dto(...[3, 4]));
echo "=========================================".PHP_EOL;
$fn(1, 2, new Dto(...['c' => 3, 'd' => 4]));
class Dto
{
public function __construct(
public int $c,
public int $d,
) {
}
}
上記の例を見ると、Dtoというクラスを関数$fn
のパラメータのタイプヒントとして指定しています。$fn
関数は、値を受け取る際にnew Dto(3, 4)
でオブジェクト化されたパラメータを受け取ります。配列であれば['c' => 3, 'd' => 4]
の値が渡されることになります。DTOはパブリックメンバーで定義したデータのみを受け取り、取り出すことができます。上記の例ではDtoクラスはcとdという値を受け取り、cとdの値を取り出せるように制限されています。配列をタイプヒントにした場合、関数$fn
はどのような形式の配列を受け取るべきか明確ではないため、内部の実装を確認する必要がありますが、DTOはデータの構造が決まっているため、どの値を使用するのかが分かります。
問題は、タイプヒントを使用するためにクラスを作成しなければならないことです。関数に引数を渡すために、引数を生成する際DTOオブジェクトを生成するためには、クラスコードが存在する必要があります。また、関数のパラメータをタイプヒントとして使用するためには、クラスコードが存在する必要があります。phpでは通常、一つのphpファイルには一つのクラスを定義するのが一般的です。一つのファイルに定義されたクラスを他のファイルで使用するためには、名前空間を定義する必要があり、名前空間を通じてクラスをインポートしなければ、DTOクラスをタイプヒントとして使用することができません。単に関数のパラメータに配列を使用するのを避けるためにDTOクラスを別ファイルに分けなければならないため、ファイルの配置、フォルダ構造、名前空間の定義などボイラープレートが増えるという欠点があります。一般的にDTOクラスは別のDTOを集めたフォルダに定義します。これにより、DTOを分類するためのフォルダを定義し、DTOクラスのphpファイルを生成する手間がかかります。
DTOは通常、getterとsetterが定義されています。しかし、上記の例ではgetterとsetterを使用しないことでボイラープレートを大幅に削減させました。getterとsetterを使用すると、intやstringなどの一般的な型よりも、例えば自然数の範囲や日付形式の文字列など、より具体的な値を受け取ることができます。前述のカスタムタイプについて説明しました。getterやsetterのないDTOの代わりに、カスタムタイプクラスをメンバ変数のタイプヒントとして付けることで、getterやsetterなしでも同様の役割を果たす簡略化されたDTOクラスを作成することができます。
$fn = function($a, $b, Dto $cdObj) {
var_dump($a);
var_dump($b);
var_dump($cdObj->c);
var_dump(($cdObj->yyyymm)());
var_dump($cdObj->yyyymm->__invoke());
var_dump(call_user_func($cdObj->yyyymm));
};
$fn(1, 2, new Dto(3, new StrYmType('202407')));
class Dto
{
public function __construct(
public int $c,
public StrYmType $yyyymm,
) {
}
}
class StrYmType
{
public function __construct(private string $yyyymm) {
$year = intval(substr($yyyymm, 0, 4));
$month = intval(substr($yyyymm, 4, 2));
if (checkdate($month, 1, $year)) {
$this->yyyymm = $yyyymm;
} else {
throw new Error('invalid Ym');
}
}
public function __invoke(): string
{
return $this->yyyymm;
}
}
StrYmType
クラスが__invoke
関数を使用したため、Dto
クラスを型とするオブジェクトのメンバー$cdObj->yyyymm
にアクセスする際に$cdObj->yyyymm()
とアクセスするとStrYmType
インスタンスのメソッドを呼び出す文法となるのでメンバーがないというエラーが発生します。そのため、メンバー変数にアクセスした後にその値を関数として実行する方式のコード($cdObj->yyyymm)()
, $cdObj->yyyymm->__invoke()
, call_user_func($cdObj->yyyymm)
を記述しました。
DTOクラスのボイラープレート減らす
PSR(PHP Standards Recommendations)によれば、「Namespaces and classes MUST follow an "autoloading" PSR: [PSR-0, PSR-4]」とされており、クラスと名前空間はPSR-0およびPSR-4に従わなければならないとされています。PSR-0およびPSR-4は、phpにおける自動ローディング(importおよびexportの実装)に関する規約です。必ずimportおよびexportが行われるように作られなければならず、そのためには1つのファイルに1つのクラス(+インターフェース、抽象クラス、enum)を定義し、名前空間を付け加えなければなりません。
しかし、phpの文法では一つのファイルに複数のクラスを定義することが可能で、文法的に制約されていません。オートローディングは、phpでimportおよびexportを実装するために作られたもので、外部にexportされるクラスをメインクラスとし、名前空間を付けてexportする必要のないクラスは同じファイルで名前空間を定義しない方法で使用することができます。このようにすると、オートローディングの動作に影響を与えずに一つのファイルで複数のクラスを定義することができ、メインクラスで必要なDTOを一つのファイルにまとめてボイラープレートを減らすことができます。しかし、「classes MUST follow an "autoloading" PSR」という記述があるため、importしないサブクラスであってもクラスであるため、オートローディングの規則に従わなければなりません。PSRを遵守するためには、同じフォルダにサブクラスを定義する方法を使用することができます。
PSRを遵守しながらも、少しでもボイラープレートを減らすためには、同じ名前空間であるメインクラスと同じフォルダにクラスファイルを一つ追加すると、importする際に必要な名前空間を省略することができ、DTOを分類するためのフォルダを作成する必要がないため、少し簡単にDTOクラスを作成することができます。ただし、DTOクラスをexportするためには、名前空間を定義する必要があるため、少しのボイラープレートが必要です。
同じネームスペースは、そのネームスペースの範囲内にある名前空間のクラスに使用するのに適していますが、複数のクラスで使用されるDTOの場合は、別のDTOファイルをまとめたフォルダを使用することをお勧めします。
カプセル化の活用
DTOを使用するには、別のファイルにクラスを定義する必要があるという不便さや、一つのファイルに複数のクラスを定義する方法などが気に入らない場合、配列のデータを特定の方法でのみ操作できるように制限する匿名クラス(Anonymous class)を作成する方法があります。
配列に関するすべての操作を匿名クラスに委任して、データストアのコントロールに関してはカプセル化されたオブジェクトに任せて、オブジェクトが提供するメソッドを使用して値の生成、変更、取得を行います。この方法は、配列に関する具体的な操作を匿名クラスの外で行わず、配列に関する関心事を匿名クラス以外に集中させる方法として使用されます。
$cdStoreObj = new class {
private array $arr;
public function setC(int $c): self
{
$this->arr['c'] = $c;
return $this;
}
public function setD(int $d): self
{
$this->arr['d'] = $d;
return $this;
}
public function getArr(): array
{
return $this->arr;
}
};
$cdStoreObj->setC(3)->setD(4);
$fn = function($a, $b, object $cdObj) {
$cdArr = $cdObj->getArr();
var_dump($a);
var_dump($b);
var_dump($cdArr['c'] ?? 'Key c is not exists');
var_dump($cdArr['d'] ?? 'Key d is not exists');
};
$fn(1, 2, $cdStoreObj);
匿名クラスを使用してオブジェクトを作成する方法は、匿名クラスにをタイプヒントとして使用できないため、適切な型としてobjectを指定しました。タイプヒントを指定できないため、IDEの推論も制限されます。しかし、同じフォルダに定義されている場合、IDEが推論を行うこともあります。
配列のメソッドを介して値を定義するため、上記の $cdStoreObj
は「c」値と「d」値の設定および変更しかできず、「c」値と「d」値しか取得できないことがわかります。しかし、配列を使用する場合と比較しても良い点が見当たりません。なぜならobject $cdObj
の代わりにarray $cdArr
を作成すれば良いため、匿名クラスを書く必要がないからです。
オブジェクトタイプの値はさまざまな実装が存在し、一般的な実装というものは存在しません。配列とは異なり、オブジェクトを受け取ったことは、オブジェクト内部のプロパティやメソッドにアクセスせずに使用するか、特殊な形式のオブジェクトを使用してそのオブジェクトに定義されたメソッドを使用する目的で使用されます。オブジェクト型のタイプヒントを使用していた場合、開発者は渡されたオブジェクトの仕様を確認するでしょう。匿名クラスを配列の代わりに渡す方法は、渡される配列のデータが多く複雑な場合、カプセル化されたオブジェクトを介して配列を扱う方法を示し、どのようにしてオブジェクトで配列を扱って渡すべきかを示しています。したがって、単純な配列の場合は単に「array」型で受け取る方が良いですが、複雑な配列の場合はこのような方法も使用できます。また、匿名クラスを使用するのは、単一のファイル内で使用するための手段であり、タイプヒントやリスコフの置換などについて意識する必要はありません。
関数$fn
において、$cdArr['c'] ?? 'Key c is not exists'
および$cdArr['d'] ?? 'Key d is not exists'
というコードを作成しました。これは、$fn
が匿名クラスcdStoreObj
のsetterメソッドが使用されるか否かに関わらず動作するようにするためです。$cdStoreObj->setC(3)->setD(4)
や $cdStoreObj->setC(3)
、 または $cdStoreObj->setD(4)
のいずれの場合でも成立できる汎用的な実装にする必要があります。
単一のファイルではなく、複数のファイルで使用される関数やメソッドの場合、クラスとして実装してタイプヒントを使用してオブジェクトを受け取るのが良いです。
さいごに
phpの配列型を使用する際、配列がインデックス付き配列を受け取るか、連想配列を受け取るか、特定のインデックスやキーに依存するロジックが含まれているかを予測することは困難な場合があります。抽象化の理由の一つは、動作するすべてのコードを確認しなくても理解して使用する目的があります。抽象化されたコードの定義部分だけを見ておおよその動作を推測できる必要があり、推測した動作以外の例外的な動作の可能性をできるだけ低くして、予測可能なコードを作成することで、すべてのコードを一つづつ確認せずに複雑なロジックを処理できるようになります。
phpは柔軟にも厳密にも作ることができる自由度の高い言語です。生産性を高めるために状況に応じてより柔軟にすることもありますが、プロジェクト管理のためにより具体的で厳密にすることもあります。プロジェクトが成長するにつれて、柔軟なロジックから厳密なロジックへの切り替えが容易にコーディングできる能力が求められます。phpは他の厳密な言語と比べて比較的制約が少ない動的言語という偏見がありますが、状況に応じて柔軟にも厳密にもコーディングできる特性を持つphpは意外にも難しい言語です。プロジェクトに参加するメンバーがどのように厳密にするかについての知識がない場合、厳密でないコードが持つロジックを正しく解釈できない情報不足により、コードを意図した通りに理解する可能性が高まり、誤った理解と従来の厳密でないコーディングスタイルを参考にしてコードを作成し、より混乱したコードを作成してしまう問題が生じます。このため、プロジェクトが成長するにつれて社内スタディの導入、厳密なコード管理、強い型言語への転換などを模索します。
これらの問題を引き起こす代表的なケースとして、phpの配列を挙げることができます。phpで配列を扱うさまざまな方法や、配列を代替する方法を紹介することで、状況に応じた適切なソリューションを見つけるための記事を書きました。カスタムタイプやカスタムタイプを利用したDTO、スプレッド構文を使用した引数割り当て方法、可変パラメーターを利用する擬似ジェネリック文法、カプセル化を使用した方法など、ニッチなスキルも紹介しました。役立つことを願っています。