10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

記事投稿キャンペーン 「PHP強化月間」

【第1回 PHPキモい挙動選手権】

Last updated at Posted at 2023-10-17

キモい挙動選手権

PHPのことは好きです。

explode(文字列分割)

公式ドキュメント(explode)

使い方

explode('-','a-b-c,d')
 => ["a", "b", "c,d"]

explode('-','a')
 => ["a"]

キモい挙動

// 空文字を分割してみる
explode('-','')
 => [""]

// 同じ値を入れてみる
explode('a','a')
 => ["",""]

返り値が空配列じゃない、、、、!
個人的には分割後に文字列が残らない場合は、空文字とか入れないで空配列にしてほしい所存です。

in_array(配列内の値チェック)

公式ドキュメント(in_array)

使い方

in_array('apple',['apple','banana','cookie'])
 => true

in_array('dog',['apple','banana','cookie'])
 => false

in_array('apple',['apple','banana','cookie'],true)
 => true

キモい挙動

// nullを検索
in_array(null,[0,1,2])
 => true

// falseを検索
in_array(false,[0,1,2])
 => true

// ''(空文字)を検索
in_array('',[0,1,2])
 => false

空文字だけ判定結果が違う!!!
nullとfalseがtrue判定になるのだけでも十分キモいのに、''(空文字)になると急に正気に戻ってfalse判定になるのが更にキモいです。

参考までに、PHPで3つをキャストしたりnull判定した際の結果です。

(int)''
 => 0
(int)null
 => 0
(int)false
 => 0

(bool)''
 => false
(bool)null
 => false
(bool)false
 => false

''??'a'
 => ""
null??'a'
 => "a"
false??'a'
 => false

空文字だけ判定結果が違う理由が全然わからなかったので公式ドキュメントを読んだところ、以下のようにありました。

needle が文字列の場合、 比較の際に大文字小文字は区別されます。

つまり第一引数が文字列の場合は評価方法が他と異なるようです。
なので

  • 第一引数が''(空文字)の場合は文字列そのままでの評価がなされてfalse
  • 第一引数がnullやboolの場合は(おそらく)intへキャストされた後に評価がなされてtrue

という挙動になるようです。

// ''同士の比較ならtrue
>>> in_array('',['',1,2])
=> true

// nullと0と0.0はすべてtrue
>>> in_array(null,[0,1,2])
=> true

>>> in_array(0,[0,1,2])
=> true

>>> in_array(0.0,[0,1,2])
=> true

(有名な話なのでご存知の方が多いかと思いますが、第三引数にtrueを渡すと厳密比較が行なえるので、キモ挙動の3つはすべてfalseとなります。
基本的には第三引数にtrueを渡すようにしましょう。)

ちなみに

in_array(null,['',1,2])
=> true

in_array(false,['',1,2])
=> true

in_array(0,['',1,2])
=> false

というキモい挙動もあります。

array(配列)

公式ドキュメント(array)

使い方

$array = ['aa', 'bb', 'c'=>'cc'];
$array[0]
 =>'aa'
$array['c']
 =>'cc'

キモい挙動

$array = ['a', 'a'=>'b', 1=>'c', '1'=>'d', 1.5=>'e', true=>'f', false=>'g'];
var_dump($array)
 => array(3) {
  [0]=>
  string(1) "g"
  ["a"]=>
  string(1) "b"
  [1]=>
  string(1) "f"
}

これもまた有名な、配列のキーが勝手にキャストされるという挙動です。

  • 1と'1'は10進数の数値として扱われる
  • floatの1.5はintにキャストされ、小数点以下が切り捨てになる(PHP8.1からは警告が出るように)
  • true,falseもintにキャストされる

また、配列の中ではキーが重複している場合、後ろの値で上書きされますので、

  • 1と'1'と1.5とtrueはすべて重複扱いになり、true=>'f'(1=>'f')が残る
  • 0とfalseも重複扱いになり、false=>'g'(0=>'g')が残る

という結果になります。

余談ですが、

$array = ['a'=>'b', 1=>'c', 'a', '1'=>'d', 1.5=>'e', true=>'f', false=>'g'];
var_dump($array)
 => array(4) {
  ["a"]=>
  string(1) "b"
  [1]=>
  string(1) "f"
  [2]=>
  string(1) "a"
  [0]=>
  string(1) "g"
}

数値のキーが振られている場合、次のキーのない値に割り当てられるのは振られている数字+1の値になります。
なので'a'のキーは自動的に2となります。

また配列の生成時にキーのソートなどはされないので、キーの並びはそのまま1→2→0になります。
キーを順番に並べたいときは、生成後に別途ソートを行う必要があります。

// 配列をキーで昇順にソートする
ksort($array)

var_dump($array)
 => array(4) {
  ["a"]=>
  string(1) "b"
  [0]=>
  string(1) "g"
  [1]=>
  string(1) "f"
  [2]=>
  string(1) "a"
}

これで順番通りになりました。

本題

この記事をご覧になったことはありますか?
PHP「1...10 === '10.1' は true」僕「えっ──!?」

1...10が10.1に???なんで????????

なぜこんな結果になるのか気になりますよね。
コメント欄を読んでも、いったい何が「割と不思議でも無い」のか私にはさっぱりでした。
「小数点の前または後ろのゼロは省略することができます」という仕様を踏まえても不思議しかないが・・・????

ということで、色々な値で検証してみましょう。

1...10 "1"..."10" 1.0.0.10 1.0 . 0.10 1.0 + 0.10
"10.1" エラー エラー "10.1" 1.1
"1.0" ."0.10" 1. . .10 1. + .10 1...1
1.00.10 "10.1" 1.1 "10.1"

うーーーん???わからない、、、、

そこで以下の式を用意しました。

1 . 0 . 0.10

③の式のピリオド前後にスペースを入れたものです。
結果は、、

1 . 0 . 0.10
"100.1"

③とは結果が変わりました。
ピリオド前後のスペースの有無で処理が変わるようです。

⑩の結果から1 . 0 . 0100になってるであろうことはわかります。
また、①と⑧の結果の違いからも、文字列結合が最後に行われているんだろうな、と推測できます。

ほんとか?

次に以下の式を試しました。

1...11
"10.11"

①の式の右端の1011に変えてみました。
やはり文字列結合が最後に行われている、という推測で間違いないようです。

⑪の結果を踏まえると、

  • 「1...11」→「"1". "0". ".11"」→「"10.11"」
  • 「1...10」→「"1". "0". ".10"」→「"10.10"」→末尾の0が省略→「"10.1"」

という処理が行われているように思えます。
ただ①の式の結果で末尾の0が省略される理由が分からず・・・
処理の順番は本当に上記であっているのでしょうか?

どんどん検証を進めましょう。

1.0..10 1..0.10
"10.1" エラー

⑫と⑬の違いは真ん中の0の位置です。
⑫では1.0.10で文字列結合できそうな位置に、⑬では1.0.10と文字列結合できそうな位置におきました。

⑫と⑬で処理結果が変わるということは、文字列結合より先に別の処理が行われているようです。
小数点前後の0の考慮が関係していそうな気がしてきました。

ここで今まで試した式と結果をまとめました。

1...10 "1"..."10" 1.0.0.10 1.0 . 0.10 1.0 + 0.10 "1.0" ."0.10"
"10.1" エラー エラー "10.1" 1.1 1.00.10
1. . .10 1. + .10 1...1 1 . 0 . 0.10 1...11 1.0..10 1..0.10
"10.1" 1.1 "10.1" "100.1" "10.11" "10.1" エラー

仮説が仮説の域を出ませんので、"10.1"になった式を分解して、1つづつvar_dumpしてみましょう。

1...10 1.0 . 0.10 1. . .10
var_dump(1.)
=> float(1)
var_dump(1.0)
=> float(1)
var_dump(1.)
=> float(1)
var_dump(.10)
=> float(0.1)
var_dump(0.10)
=> float(0.1)
var_dump(.10)
=> float(0.1)
1...1 1.0..10
var_dump(1.)
=> float(1)
var_dump(1.0)
=> float(1)
var_dump(.1)
=> float(0.1)
var_dump(.10)
=> float(0.1)

上記の結果から、"10.1"になる式の条件は

  1. 式の左端の値がfloat型にキャストしたとき1になる値であること
  2. 式の右端の数字がfloat型にキャストしたとき0.1になる値であること
  3. 式から上記の2つの数値を除いたとき、ピリオドが1つだけになる(文字列結合ができる式である)こと

の3つのようです。

つまり処理の順番としては、

  1. 式の数字をそれぞれfloatとして評価
  2. それぞれの値を文字列にキャスト
  3. 文字列結合

の順で実行されていることがわかります。
③がエラーで④が成功したのも納得です。

元記事のコメントに記載されている通り、PHPでは小数点の前または後の0を省略して書くことが可能です。

var_dump(0.)
 => float(0)
var_dump(.0)
 => float(0)

// 「0を省略している」と解釈されている
(string).1
 => "0.1"

// なので0.1をintにキャストした結果と.1をintにキャストした結果が同じになる
(int)0.1
 => 0
(int).1
 => 0

// ちなみに前後どちらも省略するとエラーになる
var_dump(.)
PHP Parse error: Syntax error, unexpected '.' on line 1

その仕様と、文字列結合を.(ピリオド)で行う仕様が重なってこのようなキモい挙動になるようです。

ここまで理解した上で元記事のコメントを読むと

小数点の前または後ろのゼロは省略することができます。
また、解釈が曖昧にならない限り、トークンの間のスペースは省略することができます。
これらのルールを使って可能な限り省略するとああなるというわけです。

たしかに割と不思議でもn・・・いやじゅうぶん不思議じゃないですかね??
レビューでこの挙動を前提としたコードがあったら指摘してしまうかもしれません。
初見殺しなキモい挙動のご紹介でした。

あとがき

業務中の業務外活動ってなんでこんなに捗るんだろう。

軽い気持ちで深淵を覗いて衝撃を受け、そのままの勢いで記事を書いてしまいました。
言葉足らずだったり解釈が間違ってる場所もあると思うので、次回からはもう少し練って記事を書きます。
最後まで読んでいただきありがとうございました。

10
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?