array_filter(), array_values()を使って、配列から空要素を削除する方法があります。
よく使う方法ですが、絶対にやってはいけないことがあります。
『コールバックのstrlen()』です。
どうして?の人からそもそも知らない人まで、分かりやすいようにサンプルコードで説明します。
array_filter(), array_values()を使った空要素削除の方法
まずはじめに、array_filter(), array_values()ではどのようにして空要素を削除するのか見てみましょう。
<?php
$array = [
'data1',
'',
'data2',
'',
'data3',
'',
'data4',
'',
'data5',
];
$tmp = array_filter($array, 'strlen');
$array = array_values($tmp);
echo "Intermediate results:\n";
echo var_dump($tmp) . "\n\n";
echo "result:\n";
echo var_dump($array) . "\n\n";
実行結果はこうなります。
Intermediate results:
array(5) {
[0]=>
string(5) "data1"
[2]=>
string(5) "data2"
[4]=>
string(5) "data3"
[6]=>
string(5) "data4"
[8]=>
string(5) "data5"
}
すばらしい!意図した結果が出ましたね?
- array_filter()で各配列のデータに対してstrlen()を実行
- 0が返るものは配列から削除。実際は0はfalseと判定される
- このままでは、削除しただけで配列のインデックスは変わっていない。インデックスを振りなおすためにarray_values()を実行。
このように、array_filter(), array_values()で空要素を削除します。
しかしアプリケーション開発では、データの型が変わることはよく起きます。もし、配列のデータの型がクラスのインスタンスや配列になったらどうなるでしょう?
実はここに問題があります。
さきに答えを言っときます。
問題の犯人は
コールバックのstrlen()の実行
配列やインスタンスは空でなくても消えてしまう
ここからstrlen()がどのようにして問題になっていくのかプログラムを動かしていきます。
配列のデータに、配列やクラスのインスタンスが含まれているサンプルプログラムです。
空要素の削除処理を実行してみましょう。
<?php
class TEST {
private $property = 'property';
}
$array = [
new TEST(),
'',
['array1-1', 'array1-2', 'array1-3'],
'',
];
$array = array_filter($array, 'strlen');
$array = array_values($array);
echo "result:\n";
echo var_dump($array) . "\n\n";
実行結果はこうなります。
result: array(0) {
}
配列も、クラスのインスタンスも消えてしまいました。これはかなりマズい。
array_filter($array, 'strlen')では、配列やクラスのインスタンスが空でなくても消えてしまう。
array_filter()のコールバックstrlen()で空要素を削除してはいけない理由
array_filter($array, 'strlen')を多く使うとどうなるでしょうか?
データが消える原因を調べたり、データがつくられているか確認する人もいるでしょう。膨大な時間がかかるかもしれません。
ちょっとデータ型を変えるとデータが消えるプログラムをどう思いますか?
誰にとってもいいことはないです。しかも厄介なのは、PHP5.3より前のバージョンは、配列データは消えませんでした。
strlen()で配列を指定すると"Array"の文字列長(5)を返すからです。PHP5.3以降は0を返します。
この仕様変更は、PHPのドキュメントにも書いています。
array_filter($array, 'strlen'), array_values()を多く使うとバグの温床になります。
array_filter()は、配列データのひとつひとつに対して、コールバック(関数)を実行するものです。
コールバックはboolean(true, false)を返します。
falseを返したときそのデータは配列から削除されます。
配列データ(array)のひとつひとつを見て、フィルタリングする(filter)
のでarray_filter()です。
フィルタリングの意味は『ろ過』です。データの取捨のことを言います。
PHPバージョン5.3を境に結果が変わるのは、値を16進コードで見ると分かります。
値 | コード | 結果 |
false | 0x00 | 削除。 |
null | 0x00 | 削除。 |
''(空文字) | 0x00 (たぶん。まちがってるかも) | 削除。 |
5 | 0x05(0x00じゃない) | PHP5.3より前。配列残す。 |
0 | 0x00 | PHP5.3以降。配列削除。 |
nullや空文字、数値の0が消える意味が分かりますね?コードにするとfalseと同じです。
ドキュメントにある空要素削除
PHP公式ドキュメントに書いてあるものです。
array_filter()のコールバックを未指定にする
<?php
$array = array_filter($array);
$array = array_values($array);
これダメなんです。
プログラムを動かしてみましょう。strlen()をはずす前と後、一気に実行します。
<?php
class TEST {
private $property = 'property';
}
$array = [
new TEST(),
'',
['array1-1', 'array1-2', 'array1-3'],
'',
'0',
'1',
'2',
0,
1,
2,
];
$array1 = array_filter($array, 'strlen');
$array1 = array_values($array1);
echo "before result:\n";
echo var_dump($array1) . "\n\n";
$array2 = array_filter($array);
$array2 = array_values($array2);
echo "after result:\n";
echo var_dump($array2) . "\n\n";
結果はこうなります。
before result:
array(6) {
[0]=>
string(1) "0"
[1]=>
string(1) "1"
[2]=>
string(1) "2"
[3]=>
int(0)
[4]=>
int(1)
[5]=>
int(2)
}
after result:
array(6) {
[0]=>
object(TEST)#1 (1) {
["property":"TEST":private]=>
string(8) "property"
}
[1]=>
array(3) {
[0]=>
string(8) "array1-1"
[1]=>
string(8) "array1-2"
[2]=>
string(8) "array1-3"
}
[2]=>
string(1) "1"
[3]=>
string(1) "2"
[4]=>
int(1)
[5]=>
int(2)
}
strlen()を使うもの、コールバックなしはお互いに消えてしまうものがあります。
コールバックstrlen() | '0', 0は残る。配列、インスタンスは消える。 |
コールバックなし | '0', 0が消える。配列、インスタンスは残る。 |
そして、共通しているのが、
配列の中の配列(多次元配列)に対応していない。
コールバックなしは、内部でempty()を使っているかも。
empty()も'0', 0は『空』判定します。
一気に解決します
この問題点をまとめて解決します。ちょっと上のレベルの『再帰処理』をします。
一番かんたんなのは『処理をシンプルに素直に書く』です。
array_filter(), array_values()は使いません。
<?php
class TEST {
private $property = 'property';
}
$array = [
new TEST(),
'',
['array1-1', 'array1-2', 'array1-3'],
'',
'0',
'1',
'2',
0,
1,
2,
null,
];
function org_array_filter($array) {
$tmp = [];
foreach($array as $item) {
if( is_array($item) ) {
$tmp[] = org_array_filter($item);
} else if( ! empty($item) || $item === '0' || $item === 0 ) {
$tmp[] = $item;
}
}
return $tmp;
}
$array = org_array_filter($array);
echo "result:\n";
echo var_dump($array);
結果はこうなります。
result:
array(8) {
[0]=>
object(TEST)#1 (1) {
["property":"TEST":private]=>
string(8) "property"
}
[1]=>
array(3) {
[0]=>
string(8) "array1-1"
[1]=>
string(8) "array1-2"
[2]=>
string(8) "array1-3"
}
[2]=>
string(1) "0"
[3]=>
string(1) "1"
[4]=>
string(1) "2"
[5]=>
int(0)
[6]=>
int(1)
[7]=>
int(2)
}
org_array_filter()を用意して、配列の各データを走査する。
走査するデータが配列のとき、再帰的にorg_array_filter()を呼び出す。
これで、多次元配列(2次元以上の配列)のすべての階層の空要素を削除します。
配列データ以外で空要素でないときそのままデータをコピーする。
空要素判定にはempty()をつかい、不足している『'0', 0』の判定を追加する。
たった10数行なのでむずかしくないでしょう。
まとめ
なんでstrlen()のコールバックを使ってるんでしょうね?
'0'と0を消さないようにだと思うんですが、配列やインスタンスは消えるので意味がありません。
歴史の追跡はおいといて、このstrlen()をコールバックを使う方法はけっこう広まっています。
某プログラミングスクールやテック系Q&Aのサイトでも『配列の空要素の削除方法』として書いてあるし。
array_filter()のstrlen()は危ないよ!
コールバックなしもデータが消えるよ!
があたりまえになってほしいです。
問題点をまとめて解決、再帰まで考えるなら、
array_filter(), array_values()を使わない
のが一番かんたん。再帰までいらないなら、
$array = array_filter($array, function($value){
if( empty($value) && $value !== '0' && $value !== 0 ) {
return false;
} else {
return true;
}
});
$array = array_values($array);
で十分。これは、array_filter(), array_values()を使います。
PHPの公式ドキュメントにも記載されていますが、配列の空要素を除外したいだけなら
array_filter($targetArray)のようにcallback無しで配列を渡す事で実現出来ます。
https://php.net/manual/ja/function.array-filter.php
何故かcallbackにstrlen()を渡す実装パターンが巷にあふれていますが、
これは記事に記載の通りクラスインスタンスを消してしまう上にエラーを吐きます。
この記事を煽り文句を読むとarray_filter()そのものに問題があるかのように誤認識してしまう人が
出る可能性がありますので、上記の内容を踏まえて修正して頂ければと思います。
問題があるのは無駄にstrlenを引数に渡す行為であってarray_filter()ではありません。
ご指摘の通りarray_filter()のコールバック未使用で同じことができますね?
追記しました。
煽りについてはわざとです。
もともとこの記事を書くきっかけが、有名なプログラミングスクールのサイトで当然のようにstrlen()を使っていたからです。
いまでも、テック系Q&AのサイトがGoogle検索で上位表示されます。
個人サイトの声なんて、企業のマンモスサイトには負けますから。
ということで気に入らないでしょうが、煽りは残します。
修正ありがとうございます。
SEO的にも過激な煽り文句を使うこと自体はいいと思いますが、
array_filterではなくstrlenが悪い点を結論で強調して頂けると嬉しいです
検索上位に出てくる貴サイトで正しい記載をして頂ければ世の中の糞コードも少しは減ると思いますので…
電車の中づり広告みたいで好みじゃないんですが...安っぽくなるし。
読んでもらわないと始まらないので。
近いうち、全体の構成を見直す予定です。
strlenを強調する案がひとつ浮かんだので。
ご指摘のおかげで記事がよくなりそうです。
ありがとうございます。
【訂正】
array_filter($array)と自作の結果はちがいます。
自作は再帰処理も入ってます。
array_filter($array)は、再帰処理はできません。
ちがいは記事本文にまとめました。
自分で書いといて忘れてました。
ぼくはこういうヤツです。
自分で忘れてる時点で内容が甘かったですね。
リライトしてよかったです。
全体の構成見直しまでして頂き感激しています。
それぞれのメリット、デメリットが初心者にも分かりやすく解説されており、
とても素晴らしい記事だと思います。
ですね
コールバック無しのarray_filter()は「PHPが空だと判定するもの」を無くす為、
普段empty関数等を多用しているPHPerからすると自然な動きにはなりますが、
それ故に予期せぬ挙動をする場合があったり、多言語畑から来た人からすると
意味不明な挙動であったりする場合がありますよね。
ここまでめんどくさい指摘をネチネチしておいてアレですが、僕も自分で実装する際には
arrayFilter($array, $strict = false, $recursive = false)みたいな静的関数を作ってたりしますw
この度は要望に対応して追記して頂いただけでなく、記事全体のリライトまでして頂きありがとうございました。
この記事は間違いなく初心者PHPerの助けになると思います。
すみません、引用の後に空行打ってなかったせいで余計な部分まで引用文扱いされてしまいました…
引用部直しました。
リライトしてて、すべては『empty()はなんぞや?』にいきつくことに気づきました。
コールバックになんでstrlen()を使うのか分かったし。
(たぶんemptyの不足分を補おうとした)
検索順位上位のものは生粋のPHPerが書いたものかもしれないですね?
ぼくは、C,Javaから入った人間なんで、いまだに『isset』『empty』とかいちいち気になります。
『ちがいがややこしい。ひとつにまとめろ!』って。
正直、正確にちがいは覚えてないです。
まぁ『なんかあったな』は覚えているので、そのつど確認してますが。
修正ありがとうございます。
コールバックのstrlen()ですが、僕個人の意見としては多言語畑から来たPHP経験の浅い人が
とりあえず今までの経験に基づいて書いたコードという認識です。
PHP経験がある程度ある人間って0とかが消えるのは当たり前っていう認識があるので、
わざわざstrlen()使ってまでそれを残す必要があるコードをそもそも作らないですし、
縦しんばその必要が出てきたとしても空の判定にstrlen()使うとか考えにくいです。
僕自身も似たような状況に陥ったことがありますが、元々多言語を触っていてPHP畑に来ると
(PHPにおいては)気にする必要のない型や空判定の挙動に無駄に振り回されてしまって、
その結果意味のない判定をしてる上にバグが含まれたコードを書いてしまう事があるんですよね。
なので今回のstrlen()もそういう状況の人が書いたコードなんじゃないかなと思っています。
PHP書くときに型を意識するのって悪いことじゃないんですが、PHP7以降でガッツリ型宣言して
書くとかじゃない限りは却ってバグの原因になる事多くて何とも言えないところです…。
言われてみればそうですね。
生粋のPHPerがこんなミスするとは考えにくいし。
strlenじゃなくて、こう書けばいいですからね。
このコード記事本文にも追記しました。