概要
再帰的に配列を作成し返す関数とGeneratorを返す関数を書き、その違いを見比べてみます。
経緯
任意の配列をコンソールに出す処理を書いていました。
始めは直接ループ内で出力していましたが、非常にテストしづらいので、先に出力する文字列の配列を全部作成してから一気に出力しようと考えました。
そこで、大きい配列を出力するためだけに同じような(あるいは大きい)配列を作成し、メモリを使うのは勿体無いので、Generatorを使って書くに至りました。
恥ずかしながら、Generatorは今まで使った事が無かったのですが、ちゃんと書いてみると配列バージョンとGeneratorバージョンの書き方の対応がよく分かったので、メモがてら残そうと思った次第です。
コード
配列を作成し返す場合のコード
/**
* 配列を表示するための文字列を取得する
*
* @param array $data
* @return array
*/
function getOutputStr_Array(array $data, int $depth = 0) : array
{
$result = [];
$indent = str_repeat(' ', $depth);
if (empty($data)) {
$result[] = $indent.'[]';
return $result;
}
$result[] = $indent.'[';
foreach($data as $key => $val) {
$val = normalize($val);
if (is_array($val)) {
$result[] = $indent.' '.$key.' =>';
$result = array_merge($result, getOutputStr_Array($val, $depth+1));
} else if (is_object($val)) {
if (method_exists($val, 'toArray')) {
$result[] = $indent.' '.$key.' =>';
$result = array_merge($result, getOutputStr_Array($val->toArray(), $depth+1));
} else if (method_exists($val, 'toString')) {
$result[] = $indent.' '.$key.' => '.$val->toString();
} else if (method_exists($val, '__toString')) {
$result[] = $indent.' '.$key.' => '.$val;
} else {
$result[] = $indent.' '.$key.' => '.get_class($val);
}
} else {
$result[] = $indent.' '.$key.' => '.$val;
}
}
$result[] = $indent.']';
return $result;
}
Generatorを返す場合のコード
/**
* 配列を表示するための文字列を取得する
*
* @param array $data
* @return \Generator
*/
function getOutputStr_Generator(array $data, int $depth = 0) : \Generator
{
$indent = str_repeat(' ', $depth);
if (empty($data)) {
yield $indent.'[]';
return;
}
yield $indent.'[';
foreach($data as $key => $val) {
$val = normalize($val);
if (is_array($val)) {
yield $indent.' '.$key.' =>';
foreach (getOutputStr_Generator($val, $depth+1) as $line) {
yield $line;
}
} else if (is_object($val)) {
if (method_exists($val, 'toArray')) {
yield $indent.' '.$key.' =>';
foreach (getOutputStr_Generator($val->toArray(), $depth+1) as $line) {
yield $line;
}
} else if (method_exists($val, 'toString')) {
yield $indent.' '.$key.' => '.$val->toString();
} else if (method_exists($val, '__toString')) {
yield $indent.' '.$key.' => '.$val;
} else {
yield $indent.' '.$key.' => '.get_class($val);
}
} else {
yield $indent.' '.$key.' => '.$val;
}
}
yield $indent.']';
}
その他コード
normalize
という関数は以下の内容です。
function normalize(mixed $val) : mixed
{
return match ($val) {
'' => '\'\'',
[] => '[]',
null => 'null',
false => 'false',
true => 'true',
default => $val,
};
}
テストコードは以下の内容です。
※ケースは最低限のみ記述しています。
/**
* @dataProvider case_getOutputStr
*
* @param array $data
* @param array $expected
* @return void
*/
public function test_getOutputStr(array $data, array $expected)
{
$resultGen = getOutputStr_Generator($data);
$idx = 0;
foreach ($resultGen as $str) {
$this->assertEquals($expected[$idx], $str);
//echo PHP_EOL.$str;
$idx++;
}
$resultArr = getOutputStr_Array($data);
$idx = 0;
foreach ($resultArr as $str) {
$this->assertEquals($expected[$idx], $str);
//echo PHP_EOL.$str;
$idx++;
}
}
public function case_getOutputStr()
{
return [
// 空配列
[
[],
[
'[]',
]
],
// キー省略
[
[
'test'
],
[
'[',
' 0 => test',
']',
]
],
// キー省略無し
[
[
'key' => 'val'
],
[
'[',
' key => val',
']',
]
],
// 入れ子
[
[
[
'key' => 'val'
],
],
[
'[',
' 0 =>',
' [',
' key => val',
' ]',
']',
]
],
[
[
[
[
'key' => 'val'
],
],
],
[
'[',
' 0 =>',
' [',
' 0 =>',
' [',
' key => val',
' ]',
' ]',
']',
]
],
// 入れ子が混在
[
[
[
'key1' => 'val1'
],
[
[
'key2' => 'val2'
],
],
],
[
'[',
' 0 =>',
' [',
' key1 => val1',
' ]',
' 1 =>',
' [',
' 0 =>',
' [',
' key2 => val2',
' ]',
' ]',
']',
]
],
// etc..
];
}
上記のコードを見比べる
配列に1つ追加する場合
1行分の文字列を作成する度に、配列を返す関数では$result
という配列に追加($result[] = 'hoge'
)していきますが、
Generatorを返す関数では都度yield 'hoge'
を行っています。
途中でreturnする場合
配列を返す関数で、途中でreturn
する箇所
if (empty($data)) {
$result[] = $indent.'[]';
return $result;
}
は、Generatorを返す関数では
if (empty($data)) {
yield $indent.'[]';
return;
}
のようにyield
直後に空のreturn
を書くことで同じ動きを表現出来ています。
※Generatorを返す関数では、return
で返した値はGenerator::getReturn
で取得します。参考
再帰的に処理を呼ぶ場合
配列を返す関数では
$result = array_merge($result, getOutputStr_Array($val, $depth+1));
と書き、$result
に纏めて追加しています。
Generatorを返す関数では「纏めてyield
する」といったことはしませんので、
foreach (getOutputStr_Generator($val, $depth+1) as $line) {
yield $line;
}
のように再帰的に処理を呼んでいる箇所でループさせ、yield
をします。
※ただし、配列を返す関数で以下のように書けば配列に1つ追加する場合と同じだと思えます。
foreach (getOutputStr_Array($val, $depth+1) as $line) {
$result[] = $line;
}
終わり
初めてGeneratorを使ってコードを書いてみましたが、書いた後となっては普通に配列操作をするのと大差無いと感じました。
ただし、返り値として受け取った、単純な配列をそのままループさせるなら使えますが、受け取った後に配列関数を噛ませる等は不可能です。
その(可能性のある)ような場合の使用を避けるか、そもそもそうならないように設計することを意識する必要がありますね。
追記
配列を出力するなら
json_encode($array, JSON_PRETTY_PRINT);
を使いましょう。