ツイート
シェア
LINEで送る
B! はてぶでブックマーク
Pocketでブックマーク
RSSフィード

PHP8, 数値と文字列の比較結果が変わる。データの厳密化を意識しよう!

php
イラストダウンロードサイト【イラストAC】
の画像をもとに加工しています。

PHP8では、数値と文字列の非厳密な比較の結果が変わります。

非厳密とは、データの型を合わせる必要がないゆるい比較で、内部で型を強制変換して比較してくれる機能のこと。

PHP8はデータの厳密化がポイントなので比較にも影響します。個人的には型ちがいの比較はおススメしません。

2020年11月26日、5年ぶりにPHPのメジャーバージョンが上がりました。PHP8です。

メジャーアップデートだけに変更点も大きいです。数値と文字列の比較も一部、結果が変わります。

一部変わるというところがめんどいですが。

数値と非数値文字列の比較に注意

数値と数値文字列の比較に変更はありません。PHP8では数値文字列の判定が変わりましたがその影響はなし。

もともと、数値文字列をキャストして比較してたんじゃないかな?

(個人の見解。)

PHP8でも、数値と数値文字列の比較は数値として比較すると言ってるし。

気をつけないといけないのは、数値と数値じゃない文字列の比較。

今回、それを見ていきます。

結果が変わる比較

PHP7とPHP8で、数値と非数値文字列の比較の結果が変わるパターンの表です。

PHP7PHP8
0 == "not-a-number "truefalse
'not-a-number' > 0falsetrue
0 == ""truefalse
'' < 0falsetrue
42 == "42foo"truefalse

PHP8では数値の方を文字列に変換して比較します。ひとつずつ見ていきます。

ひとつ注意点が。表にある大小比較をしてしまうプログラミングは絶対に避けてください。

このような比較をすること自体、どんな結果になったとしてもバグです。数値と文字列の大小なんて意味不明でしょう?

個人的にはイコールも避けるべきだと思いますが、意味は分かるのでモヤッとするけどギリOKなのかな? 一般的には。

0と非数値文字列の比較

サンプルコード
<?php

var_dump( "0 == 'foobar' :");
var_dump( 0 == 'foobar' );

var_dump( "'foobar' > 0 :");
var_dump( 'foobar' > 0 );

var_dump( "'foobar' < 0 :");
var_dump( 'foobar' < 0 );

var_dump( "'foobar' cast :");
var_dump( (int)'foobar' );

var_dump( "0 cast to string :");
var_dump( (string)0 );

var_dump( "strcmp( 'foobar', (string)0 ) :" );
var_dump( strcmp( 'foobar', (string)0 ) );

strcmp()はPHP8の実行結果に影響するので入れました。

PHP7の実行結果
string(15) "0 == 'foobar' :"
bool(true)
string(14) "'foobar' > 0 :"
bool(false)
string(14) "'foobar' < 0 :"
bool(false)
string(15) "'foobar' cast :"
int(0)
string(18) "0 cast to string :"
string(1) "0"
string(31) "strcmp( 'foobar', (string)0 ) :"
int(54)

PHP7の結果を見ると、非数値文字列をキャストして比較してるんじゃないかと疑います。

大小比較すること自体おかしいのでfalseで正解に見えますが、イコールはどう見てもおかしい。

こういう比較をすること自体やってはいけない証拠です。

PHP8の実行結果
string(15) "0 == 'foobar' :"
bool(false)
string(14) "'foobar' > 0 :"
bool(true)
string(14) "'foobar' < 0 :"
bool(false)
string(15) "'foobar' cast :"
int(0)
string(18) "0 cast to string :"
string(1) "0"
string(31) "strcmp( 'foobar', (string)0 ) :"
int(54)

PHP8ではイコールの結果が直ってますが、大小比較の結果にtrueがあります。

これ、おかしいですよね?

『何を比較してOKってしてるんだ?』

と思ったので確認したところ、strcmp()を使ってバイナリセーフで比較するらしい。って、コード値の大小ですか?

何の意味が?

PHP8でもこういう比較をする場面を作るのはやめましょう。バグになるのが目に見えてる。

数値と空文字の比較

サンプルコード
<?php

var_dump( "0 == '' :");
var_dump( 0 == '' );

var_dump( "'' > 0 :");
var_dump( '' > 0 );

var_dump( "'' < 0 :");
var_dump( '' < 0 );

var_dump( "'' cast :");
var_dump( (int)'' );
PHP7の実行結果
string(9) "0 == '' :"
bool(true)
string(8) "'' > 0 :"
bool(false)
string(8) "'' < 0 :"
bool(false)
string(9) "'' cast :"
int(0)

PHP7では空文字と0はイコールとして扱いますが、そういう仕様と言われると納得はしないけど分からんでもない。

PHP8の実行結果
string(9) "0 == '' :"
bool(false)
string(8) "'' > 0 :"
bool(false)
string(8) "'' < 0 :"
bool(true)
string(9) "'' cast :"
int(0)

PHP8では、イコールについては、空でない非数値文字列と数値の比較と同じ結果になります。

大小の結果は、空でない非数値文字列と数値の比較と逆の結果になります。strcmp()を使ってるんでそうなるでしょうね?

意味不明ですが。

数値と数値始まりの文字列の比較

今度は、数値と数値始まりの文字列の比較です。

サンプルコード
<?php

var_dump( "42 == '42foo' :");
var_dump( 42 == "42foo" );

var_dump( "42 > '42foo' :");
var_dump( 42 > "42foo" );

var_dump( "42 < '42foo' :");
var_dump( 42 < "42foo" );

var_dump( "is_numeric('42foo')' :");
var_dump( is_numeric('42foo') );

var_dump( "'42foo' cast :");
var_dump( (int)'42foo' );
PHP7の実行結果
string(15) "42 == '42foo' :"
bool(true)
string(14) "42 > '42foo' :"
bool(false)
string(14) "42 < '42foo' :"
bool(false)
string(22) "is_numeric('42foo')' :"
bool(false)
string(14) "'42foo' cast :"
int(42)

PHP7では、'42foo' をis_numeric()で見るのではなく、キャストで型変換して判定しているので、変な結果になっています。

PHP8の実行結果
string(15) "42 == '42foo' :"
bool(false)
string(14) "42 > '42foo' :"
bool(false)
string(14) "42 < '42foo' :"
bool(true)
string(22) "is_numeric('42foo')' :"
bool(false)
string(14) "'42foo' cast :"
int(42)

イコールの結果が修正されたのはいいですが、大小比較でtrueを返すものが。これもstrcmp()を使ってるから。

『いや、そこもちゃんとやってよ!』と思うのはボクだけでしょうか?

『数値と文字列の比較なんだから、イコール、Not イコールしかしないっしょ?』 ということなのか?

にしてもモヤモヤしてしょうがない。

PHPはゆるい比較を問題視している

PHPの世界では、データ(変数)の型がゆるいこと、そのゆるい変数で比較するのは避けるべきだという流れになっています。

たとえば、イコール "==" は "==="、Not イコール "!=" は "!===" を使うことは今や常識ですね?

(比較の厳密化。型も合わないとtrueにならない。)

理由はバグになりやすいしセキュリティが甘くなるから。

バグの潜在数に厳しい基幹システムなどは厳密な型を採用しているC(C++)やJavaで書くことが多いのも意識しているのでしょう。

PHP8では、比較以外にも変数の型の厳密化への修正が行われています。

ただ、in_array()やswitch構文など、ゆるい比較が内部で行われているので、なかなかやめるとは言えない事情があるらしい。

PHP8でも数値と文字列の比較はやめよう!

何度も意味不明、分からんを連発しましたが、C, C++, Javaを使ってきた人からするとPHPの比較はそう見えます。

CやJavaなどでは変数の比較には変数の型を意識します。意識しないと怒られるレベル。というかエラーになるのでやりようがない。

(intとStringの比較なんてありえない。)

PHPにもキャストや型変換関数が多く提供されているので、それを使って比較すればいいじゃんという感想。

PHP8では変数の型の厳密化が進みましたが、どうせやるならもう一歩進んでほしかった。

まだ、ん?が出てしまうのが残念。

少数(浮動小数点)について

忘れてました。少数について。

PHP8では、少数と少数文字列の比較は基本的に整数と同じ。少数と整数文字列のときは、整数文字列をfloatでキャストして比較します。

サンプルコード
<?php

var_dump( "1 == '1.0' :");
var_dump( 1 == '1.0' );

var_dump( "1 > '1.0' :");
var_dump( 1 > '1.0' );

var_dump( "1 < '1.0' :");
var_dump( 1 < '1.0' );

var_dump( "1 == '1.0e1' :");
var_dump( 1 == '1.0e1' );

var_dump( "1 > '1.0e1' :");
var_dump( 1 > '1.0e1' );

var_dump( "1 < '1.0e1' :");
var_dump( 1 < '1.0e1' );

var_dump( "'1.0e1' :");
var_dump( (float)'1.0e1' );
実行結果
string(12) "1 == '1.0' :"
bool(true)
string(11) "1 > '1.0' :"
bool(false)
string(11) "1 < '1.0' :"
bool(false)
string(14) "1 == '1.0e1' :"
bool(false)
string(13) "1 > '1.0e1' :"
bool(false)
string(13) "1 < '1.0e1' :"
bool(true)
string(9) "'1.0e1' :"
float(10)

指数表記(eを使う)でもOK。PHP8での変更点はありません。

特殊な浮動小数のINFとNAN

NANとINFは浮動小数点の特殊な定数です。

NANNot a Number
非数。
無効な浮動小数点の値。
INFinfinity
正(+)の無限大。
-INF負(-)の無限大。

比較にNANを使うと、相手方の値がどんなものであれfalseを返します。

(PHP7とPHP8で同じ。)

サンプルコード
<?php

var_dump( "NAN == '1.0' :" );
var_dump( NAN == '1.0' );

var_dump( "NAN > '1.0' :" );
var_dump( NAN > '1.0' );

var_dump( "NAN < '1.0' :" );
var_dump( NAN < '1.0' );

var_dump( "INF == 'INF'" );
var_dump( INF == 'INF' );

var_dump( "-INF == '-INF'" );
var_dump( -INF == '-INF' );

var_dump( "NAN == 'NAN'" );
var_dump( NAN == 'NAN' );

var_dump( "INF == '1e1000'" );
var_dump( INF == '1e1000' );

var_dump( "-INF == '-1e1000'" );
var_dump( -INF == '-1e1000' );
PHP7の実行結果
string(14) "NAN == '1.0' :"
bool(false)
string(13) "NAN > '1.0' :"
bool(false)
string(13) "NAN < '1.0' :"
bool(false)
string(12) "INF == 'INF'"
bool(false)
string(14) "-INF == '-INF'"
bool(false)
string(12) "NAN == 'NAN'"
bool(false)
string(15) "INF == '1e1000'"
bool(true)
string(17) "-INF == '-1e1000'"
bool(true)
PHP8の実行結果
string(14) "NAN == '1.0' :"
bool(false)
string(13) "NAN > '1.0' :"
bool(false)
string(13) "NAN < '1.0' :"
bool(false)
string(12) "INF == 'INF'"
bool(true)
string(14) "-INF == '-INF'"
bool(true)
string(12) "NAN == 'NAN'"
bool(false)
string(15) "INF == '1e1000'"
bool(true)
string(17) "-INF == '-1e1000'"
bool(true)

PHP8では、INFは文字列 "INF" とイコールになります。

前の投稿
PHP8, is_numeric, 判定が変わり後ろ空白がtrueに
PHP8, エラー、警告、通知の変更のまとめ。レベル格上げが主な変更点。
次の投稿
コメントを残す

*