str_replaceとstrtrの違いって何なのさ

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
シュトゥットガルト空港のIATAコードはSTRだそうです

この記事を三行にまとめると

両者はちょっとだけ違う動きをしています
パターンによっては意図しない結果が得られてしまうことがある
シュトゥットガルト空港の3レターコードは「STR」
PHPで文字列の一部を別の文字に置換したい時、よく使うのはstr_replace()やstrtr()だと思います。どちらを使っても同じ結果を得ることができます。

じゃあ両者は何が違うのかって話なのですが、置換したい文字を配列で指定した場合、strtr()は指定した文字をそれぞれ変換するだけなのに対し、str_replace()は配列の先頭から順番に置換を行います。

どういうことかっていうと、例えば「abcde」という文字のうち、aとeだけ大文字に置換して「AbcdE」という文字列を得たい場合は、それぞれこんな風に書きます。

//strtr
strtr('abcde', array('a' => 'A', 'e' => 'E'));

//str_replace
str_replace(array('a', 'e'), array('A', 'E'), 'abcde');

この時、得られる結果は同じなのですが、実は両者はちょっとだけ違う動きをしています。

strtr()の方は「aをAに、eをEにそれぞれ置換する」という動きをしているのに対し、str_replace()の方は「まずaをAに置換し、次にその文字列のeをEに置換する」という動きをしています。「abcde → Abcde → AbcdE」みたいな流れで二回置換を行ってるってイメージですかね。

だから上記のような場合は特に問題はないのですが、パターンによっては意図しない結果が得られてしまうことがある。



意図せぬパターン

例えば、ある二進数の数字の0と1を反転させたい、みたいな場合があったとします。「110100」みたいな数字があったら「001011」にしたいってことですね。

これを先ほどと同じようにstrtr()とstr_replace()でそれぞれ置換した場合。

//strtr
strtr('110100', array('1' => '0', '0' => '1'));

//str_replace
str_replace(array('1', '0'), array('0', '1'), '110100');

この書き方だと、strtr()の方は「001011」が得られるのに対し、str_replace()は「111111」になってしまいます。strtr()は1を0に、0を1に置換するだけなのに対し、str_replace()は、まず1を0に置換して「000000」とした後に0を1に置換するので、「110100 → 000000 → 111111」という流れで全部1になってしまうわけです。

これは配列の順番によって変わるので、書き方次第では全部0になっちゃう場合もある。

//111111
str_replace(array('1', '0'), array('0', '1'), '110100');

//000000
str_replace(array('0', '1'), array('1', '0'), '110100');

こんなパターンの置換を行う時は注意が必要ってことですね。おとなしくstrtr()を使っとく方が安心かもしれん。

ちなみにstrtr()の場合はこんな書き方でもオッケーです。

strtr('110100', '10', '01');

これも「1を0に、0を1に置換」の動きをします。



速度やメモリ

予期せぬ結果になるんだったらstr_replace()を使わない方が良いじゃんって思うかもしれませんが、速度とかメモリ使用量とか気にする場合は、必ずしもstrtr()の方が良いとも言いきれないかもしれません。

速度に関してはstrtr()の方が速いって言う人とstr_replace()の方が速いって言う人の両方がいるのでどっちが良いのかはよく分からないんですが、僕が試してみた限り、少なくともメモリの使用量はstr_replace()の方が少なかったです。

$str = 'Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch';
$start = microtime(true);
for($i = 0; $i < 1000000; $i++) {
  $a[] = str_replace(array('a', 'e'), array('A', 'E'), $str);
}
$end = microtime(true);
echo $end - $start;//処理時間
echo memory_get_usage();//メモリ

適当にこんな感じのコードを書いて検証してみました。PHPのバージョンは7.1です。

str_replace()とstrtr()のそれぞれの計測結果はこうでした。

+-------------+-------------+-------------+
|             | str_replace |    strtr    | 
+-------------+---------------------------+
|  time(s)    |  0.3 ~ 0.5  |  0.3 ~ 0.5  | 
|  memory(MB) |      130    |      290    | 
+-------------+-------------+-------------+

時間はどちらもだいたい0.3秒から0.5秒くらいでほぼ変わらずですが、メモリはstrtr()の方が倍くらい消費してますね。

ちなみにこれは配列で置換のパターンを指定した場合の話です。以下の書き方をした場合はstrtr()の方も130MBくらいでした。時間はやはり0.3秒から0.5秒くらい。

for($i = 0; $i < 1000000; $i++) {
  $a[] = strtr($str, 'ae', 'AE');
}

じゃあこれがベストなんじゃねーの?って気もするのですが、この書き方は置換するパターンの文字列の長さを一緒にしなきゃいけないという決まりがあります。違う長さにしてもエラーにはなりませんが。

例えば「abcde」の文字列を「ABC」に変換したいような場合。

echo strtr('abcde', 'abcde', 'ABC');

こんな風に書いても「ABC」にはなりません。「ABCde」となってしまう。

配列じゃない書き方の場合は文字の短い方に合わせる仕様になってるので、この場合は「aとA、bとB、cとC」だけが対応し、残りの「dとe」は無視されます。「strtr('abcde', 'abc', 'ABCDE')」と書いた場合も一緒。「DE」は無視される。

配列バージョンでいうと、以下の場合と同じ動きになるって感じですね。

strtr('abcde', array('a' => 'A', 'b' => 'B', 'c' => 'C'));






ってなわけでstr_replace()にしろstrtr()にしろ「絶対こっちを使うべきだ!」と言いきるのは難しいのかもしれません。上記の点に注意して、あとは使いたい方を使うって感じなのかなと。

僕はstr_replace()を使うことの方が多いですね。ってか、実はstrtr()の存在をつい最近まで知らなかった。いやはやお恥ずかしい。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください