奇特なブログ

「殊勝に値する行いや心掛け」を意味する、奇特な人になる為のブログです

PHPのコーディングにおけるベストプラクティスを考える:empty関数

PHPのコーディングにおけるベストプラクティスを考える準備記事 - 奇特なブログ


さて、今回は上記の記事の「第一弾」です。
しばらくは、「微妙な関数シリーズ」になると思います。
で、やっぱり最初は、この企画を考えるきっかけになった「empty関数(厳密には関数ではないとの事ですが)」かなと。
これまでも、「散々苦しめられました」しね。


まずは、関数のマニュアルを見てみましょう。

PHP: empty - Manual


つまり、empty() は本質的に !isset($var) || $var == false と同じことを簡潔に記述しているだけです。

この辺が引っかかるんですよね、「 == 」と2個になっている所が。
だから、動きにも納得はいくんですけど、そこがどうなんだ?と。


例2 文字列のオフセットに対する empty()
PHP 5.4 以降では、文字列内でのオフセット指定を渡したときの empty() の挙動が変わりました。

長い(原文は短いですが)ので引用はしませんが、
上記の例2のソースと、5.4からの実行結果を見てみて下さいよ。
分からんでもないですが、この法則覚えていられますか?っていう(苦笑)

基本的に、「trueになる値が多過ぎて、覚えていられない」というのが、
問題としてあると思っているんですよね。
だから、issetなんかは、まあアレも例外的な値としてnull値がありますけど、
それだけではあるので、まだマシかと。


あと、業務であるあると思われるのが、
ソースを「書いている時」と「読んでいる時(特に、他人の)」に、
それぞれあるかなと思いまして。

書いている時なら、
上記の「trueになる値」が多く、
ケアレスミスをちょくちょくしてしまったり、
あるいは、テストミスで「想定していない値が、分岐を通ってしまった」とか。
後者の方がヤバいでしょうけど。
「 ==(イコール2個) 」でも、似た様な話はちょいちょいありますが、その辺も似てますね。
「なんとなく、値が入ってなくて空なら処理してほしい」って時に、empty()を使う気もしますが、
その「なんとなく」っていうのが、危ないんじゃないですかねっていう。

逆に、読んでいる時なら、
その変数が型指定されていれば、若干マシですが、
「どんな型の、どんな値が入っているか」が分かりにくいと思いますね。
「empty($value)」って見た時に、その辺が分かんないじゃないですか。
だから、部分抜粋ですけど、以下の様なコードなら、
「あ、int型だな」って分かるから、少し読みやすくなるんじゃないかと。
でも、こういうコード見たことないですし、
あと、それでもわざわざemptyを使う必然性は、感じない様にも思いますし。
大体、関数定義から近いから分かるだけって気もしますし。

function test(int $value)
{
  if (empty($value))
  {
  }
}


この関数に限らず基本的な話として思うのは、
「出来るだけ、読みやすく(リーダブルに)、簡単にシンプルに済ませた方が良い」と。
でも、emptyは、「書くのが楽」っていうのが使われる原因じゃないかと思っていますが、
その場は楽でも、その後の動作確認やら、他人がそのソースを読んだ時の読みやすさやら考えるとっていう。
後者は、「コメントを書く」なんかにも、言えると思うんですけどね。


じゃあ、どうすれば良いのかということで、
以下に改善案(検証用も含んでいます)のソースを書いてみました。
一部(文字列だけは、少々面倒か?)除くと、別に面倒でもないと思うんですけどね。


あと、GitHub
https://github.com/kitoku-magic/other/blob/master/php_coding_best_practice/empty.php
にも上がってますが、同じソースです。

<?php

// PHP7.4.15にて検証

class foo
{
}

try
{
  // empty()に渡した結果、結果がtrueになるのは以下の値
  // false
  // 0
  // +0
  // 00
  // 0.0
  // +0.0
  // 00.0
  // array()
  // null
  // '0'
  // ''
  // 未定義の変数

  // 検証用の値を全て含んだ配列
  $empty_values = [
    // bool
    true,
    false,

    // int
    PHP_INT_MIN - 1,
    PHP_INT_MIN,
    -1,
    0,
    1,
    +0,
    +1,
    PHP_INT_MAX,
    PHP_INT_MAX + 1,
    00,
    01,

    // float
    // PHP_FLOAT_MINは、正の値の中での最小値なので0になる
    PHP_FLOAT_MIN - 0.1,
    PHP_FLOAT_MIN,
    (-1 * PHP_FLOAT_MAX) - 0.1,
    (-1 * PHP_FLOAT_MAX),
    -1.1,
    -1.0,
    -0.1,
    0.0,
    0.1,
    1.0,
    1.1,
    +0.0,
    +0.1,
    +1.0,
    +1.1,
    PHP_FLOAT_MAX,
    PHP_FLOAT_MAX + 0.1,
    00.0,
    01.0,
    INF,
    NAN,

    // array
    [],
    array(),
    array(null),

    // object
    new foo(),

    // null
    null,
    NULL,

    // string
    'true',
    'false',
    '-1',
    '0',
    '1',
    '0',
    '1',
    '+0',
    '+1',
    '-1.1',
    '-1.0',
    '-0.1',
    '0.0',
    '0.1',
    '1.0',
    '1.1',
    '+0.0',
    '+0.1',
    '+1.0',
    '+1.1',
    '[]',
    'array()',
    'array(null)',
    'new foo()',
    "fopen('/tmp/test', 'x')",
    'null',
    'NULL',
    '',
    '00',
    '01',
    '.0',
    '.1',
    '0.',
    '1.',
    // PHP_INT_MIN - 1
    '-9223372036854775809',
    // PHP_INT_MIN
    '-9223372036854775808',
    // PHP_INT_MAX
    '9223372036854775807',
    // PHP_INT_MAX + 1
    '9223372036854775808',
    // (-1 * PHP_FLOAT_MAX) - 1
    '-179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858369',
    // (-1 * PHP_FLOAT_MAX)
    '-179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368',
    // PHP_FLOAT_MAX
    '179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368',
    // PHP_FLOAT_MAX + 1
    '179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858369',
    '1 ',
    ' 1',
    ' 1 ',
    '1 ',
    "1\t",
    '0 ',
    ' 0',
    ' 0 ',
    '0 ',
    "0\t",
  ];

  echo "--------------------改善前--------------------\n";

  // empty関数での判定結果を出力
  foreach ($empty_values as $empty_value)
  {
    echo var_export($empty_value, true) . ' is ' . var_export(empty($empty_value), true) . "\n";
  }

  // 以下2つは、var_exportで出力出来ないので別枠
  echo 'リソース型 is ' . var_export(empty(fopen('/tmp/test', 'x')), true) . "\n";
  unlink('/tmp/test');

  echo '存在しない変数 is ' . var_export(empty($not_exist), true) . "\n";

  echo "--------------------改善後--------------------\n";

  // 以下からが改善案(結果は、上記と全く同じ)
  foreach ($empty_values as $empty_value)
  {
    // bool
    if (is_bool($empty_value) === true)
    {
      echo var_export($empty_value, true) . ' is ' . var_export($empty_value === false, true) . "\n";
    }
    // int
    else if (is_int($empty_value) === true)
    {
      echo var_export($empty_value, true) . ' is ' . var_export($empty_value === 0, true) . "\n";
    }
    // float
    else if (is_float($empty_value) === true)
    {
      echo var_export($empty_value, true) . ' is ' . var_export($empty_value === 0.0, true) . "\n";
    }
    // array
    else if (is_array($empty_value) === true)
    {
      echo var_export($empty_value, true) . ' is ' . var_export(count($empty_value) === 0, true) . "\n";
    }
    // object
    else if (is_object($empty_value) === true)
    {
      echo var_export($empty_value, true) . ' is ' . var_export($empty_value === null, true) . "\n";
    }
    // null
    else if (is_null($empty_value) === true)
    {
      echo var_export($empty_value, true) . ' is ' . var_export($empty_value === null, true) . "\n";
    }
    // string
    else if (is_string($empty_value) === true)
    {
      echo var_export($empty_value, true) . ' is ' . var_export($empty_value === '' || $empty_value === '0', true) . "\n";
    }
  }

  // fopenの失敗時の戻り値がfalseの為(なので、リソース型を扱う関数によって違う)
  echo 'リソース型 is ' . var_export(fopen('/tmp/test', 'x') === false, true) . "\n";

  echo '存在しない変数 is ' . var_export(isset($not_exist) === false, true) . "\n";

  echo "--------------------「is_null」の速度調査--------------------\n";

  // おまけ:「is_null」と「=== null」での比較調査(一千万件で以下なので、正直誤差レベルかと)
  ini_set('memory_limit', -1);
  $null_values = array_fill(0, 10000000, null);
  $start = microtime(true);
  foreach ($null_values as $null_value)
  {
    if (is_null($null_value) === true)
    {
    }
  }
  // time: 0.24362707138062(上記の、「 === true」を省略すると、time: 0.17196798324585)
  echo 'time: ' . (microtime(true) - $start) . "\n";

  echo "--------------------「 === null」の速度調査--------------------\n";

  $start = microtime(true);
  foreach ($null_values as $null_value)
  {
    if ($null_value === null)
    {
    }
  }
  // time: 0.19953608512878
  echo 'time: ' . (microtime(true) - $start) . "\n";

  while (count($empty_values) <= 10000000)
  {
    $empty_values = array_merge($empty_values, $empty_values);
  }

  echo "--------------------「empty」の速度調査--------------------\n";

  // empty関数での判定時間
  $start = microtime(true);
  foreach ($empty_values as $empty_value)
  {
    if (empty($empty_value) === true)
    {
    }
  }
  // time: 0.382817029953(速度は優位の模様)
  // 百万件だと、0.048418045043945
  echo 'time: ' . (microtime(true) - $start) . "\n";

  echo "--------------------「改善案」の速度調査--------------------\n";

  $start = microtime(true);
  foreach ($empty_values as $empty_value)
  {
    // bool
    if (is_bool($empty_value) === true)
    {
      if ($empty_value === false)
      {
      }
    }
    // int
    else if (is_int($empty_value) === true)
    {
      if ($empty_value === 0)
      {
      }
    }
    // float
    else if (is_float($empty_value) === true)
    {
      if ($empty_value === 0.0)
      {
      }
    }
    // array
    else if (is_array($empty_value) === true)
    {
      if (count($empty_value) === 0)
      {
      }
    }
    // object
    else if (is_object($empty_value) === true)
    {
      if ($empty_value === null)
      {
      }
    }
    // null
    else if (is_null($empty_value) === true)
    {
      if ($empty_value === null)
      {
      }
    }
    // string
    else if (is_string($empty_value) === true)
    {
      if ($empty_value === '' || $empty_value === '0')
      {
      }
    }
  }
  // time: 1.4359290599823(型チェックもしているから当然か)
  // 百万件だと、0.18049216270447
  echo 'time: ' . (microtime(true) - $start) . "\n";

  echo "--------------------「改善案(1回のif文)」の速度調査--------------------\n";

  $start = microtime(true);
  foreach ($empty_values as $empty_value)
  {
    if (
      ($empty_value === false) ||
      ($empty_value === 0) ||
      ($empty_value === 0.0) ||
      (is_array($empty_value) === true && count($empty_value) === 0) ||
      ($empty_value === null) ||
      ($empty_value === '') ||
      ($empty_value === '0')
    )
    {
    }
  }
  // time: 2.0437140464783(判定回数が多くなるので当然か)
  // 百万件だと、0.24031591415405
  echo 'time: ' . (microtime(true) - $start) . "\n";
}
finally
{
  unlink('/tmp/test');
}

// 参考:上記の全ての結果
/*
true is false
false is true
-9.223372036854776E+18 is false
-9223372036854775807-1 is false
-1 is false
0 is true
1 is false
0 is true
1 is false
9223372036854775807 is false
9.223372036854776E+18 is false
0 is true
1 is false
-0.1 is false
2.2250738585072014E-308 is false
-1.7976931348623157E+308 is false
-1.7976931348623157E+308 is false
-1.1 is false
-1.0 is false
-0.1 is false
0.0 is true
0.1 is false
1.0 is false
1.1 is false
0.0 is true
0.1 is false
1.0 is false
1.1 is false
1.7976931348623157E+308 is false
1.7976931348623157E+308 is false
0.0 is true
1.0 is false
INF is false
NAN is false
array (
) is true
array (
) is true
array (
  0 => NULL,
) is false
foo::__set_state(array(
)) is false
NULL is true
NULL is true
'true' is false
'false' is false
'-1' is false
'0' is true
'1' is false
'0' is false
'1' is false
'+0' is false
'+1' is false
'-1.1' is false
'-1.0' is false
'-0.1' is false
'0.0' is false
'0.1' is false
'1.0' is false
'1.1' is false
'+0.0' is false
'+0.1' is false
'+1.0' is false
'+1.1' is false
'[]' is false
'array()' is false
'array(null)' is false
'new foo()' is false
'fopen(\'/tmp/test\', \'x\')' is false
'null' is false
'NULL' is false
'' is true
'00' is false
'01' is false
'.0' is false
'.1' is false
'0.' is false
'1.' is false
'-9223372036854775809' is false
'-9223372036854775808' is false
'9223372036854775807' is false
'9223372036854775808' is false
'-179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858369' is false
'-179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368' is false
'179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368' is false
'179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858369' is false
'1 ' is false
' 1' is false
' 1 ' is false
'1 ' is false
'1	' is false
'0 ' is false
' 0' is false
' 0 ' is false
'0 ' is false
'0	' is false
リソース型 is false
存在しない変数 is true
*/


ちょっとビックリしたのが、
「empty(0.0)」はtrueで、
「empty('0.0')」はfalseなんですね(苦笑)
上記もマニュアルの方の内容も含めて、覚えていられます?(苦笑)
こういうのは、ガ〜っとコードを書いている時に、
混乱の元にしかならないと思うんですけどね。


で、改善案について、書くのって別に大変じゃないと思うんですけど、どうなんですかね?
さっきも書いたように、文字列だけは若干面倒と思いますけど、
ユーティリティ的に、別に関数を作って(str_empty()とか?)、それを呼べば良いんじゃないかとも思いますし。


「サッと書けて、楽だから(推測ですが)」って理由で、使われているのかもしれないですけど、
上記に書いた様な問題があると思いますので、今回取り上げました。


あと、もっと良い書き方がある等ありましたら、教えて頂けるとありがたいですね。