文字列オフセットは、string型の変数を配列([])と同じ記述で文字列内の一文字をあつかうものです。
PHPの文字列オフセットの使い方を、原始的なC(C++)、変数の型に厳しいJava、最近メジャー昇格しているPythonのサンプルコードを使って比較していきます。
文字列オフセットとは何か?
オフセット(offset)は『位置』です。文字列オフセットは、ある一文字の位置のこと。
string型変数で配列と同じ書き方($variable[])で参照・変更ができます。
PHPでは文字列オフセットが使えますが、プログラム言語によってはエラーになります。
そのような言語では、PHPのように文字列オフセットと配列が同じ書き方をするような曖昧なことは許しません。Javaが代表的です。
また、文字列オフセットを配列と同じ書き方で使うことはあまりありません。文字列のオフセットは1byteずつの位置なので、日本語のように1byteで表現できないマルチバイトの文字では使えないから。
このあたりもサンプルコードを交えながら見ていきましょう。
PHPの文字列オフセット
まずはPHPの文字列オフセットを見ていきます。[]を使っていますが、この書き方は使わないことに注意してください。
なぜ使わないのかも読んでいけば分かります。
<?php
$tmp = "sample";
var_dump("strlen:" . strlen($tmp));
for( $idx = 0; $idx < strlen($tmp); $idx++) {
var_dump($tmp[$idx]);
}
$tmp = "サンプル";
var_dump("encode:" . mb_detect_encoding($tmp));
var_dump("strlen:" . strlen($tmp));
for( $idx = 0; $idx < strlen($tmp); $idx++) {
var_dump($tmp[$idx]);
}
string型の変数で、半角英数の "sample" 、マルチバイトの変数 "サンプル" を用意し、文字列オフセットを1ずつ動かしてそのまま出力します。
結果はこうなります。
string(8) "strlen:6"
string(1) "s"
string(1) "a"
string(1) "m"
string(1) "p"
string(1) "l"
string(1) "e"
string(12) "encode:UTF-8"
string(9) "strlen:12"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
string(1) "�"
日本語は文字化けしました。あたりまえです。初歩のまちがいをしているから。
日本語などのマルチバイト言語を扱う時、strlen() ではなく mb_strlen() を使わないと意図した文字列長は取れません。
ただ間違ったことで発見が。UTF-8では1文字あたり3byte使ってるのが分かります。
文字化けは無くせるけどやっては行けない方法
マルチバイトは3byteずつ連結すれば文字化けは起きません。サンプルコードを少し修正してみましょう。
<?php
$tmp = "sample";
var_dump("strlen:" . mb_strlen($tmp));
for( $idx = 0; $idx < mb_strlen($tmp); $idx++) {
var_dump($tmp[$idx]);
}
$tmp = "サンプル";
var_dump("encode:" . mb_detect_encoding($tmp));
var_dump("strlen:" . mb_strlen($tmp));
for( $idx = 0; $idx < mb_strlen($tmp); $idx++) {
$idx2 = $idx * 3;
var_dump($tmp[$idx2] . $tmp[$idx2 + 1] . $tmp[$idx2 + 2]);
}
string(8) "strlen:6"
string(1) "s"
string(1) "a"
string(1) "m"
string(1) "p"
string(1) "l"
string(1) "e"
string(12) "encode:UTF-8"
string(8) "strlen:4"
string(3) "サ"
string(3) "ン"
string(3) "プ"
string(3) "ル"
このコード、やってはいけないことがあるのに気づいたでしょうか?
strlen() を mb_strlen() に変えたのは正しい修正です。おイタなのは文字列オフセットを連結して3byte = 1文字にしているところ。
たしかにUTF-8ではほとんどの日本語は3byteですが、マニアックな漢字などは4byteです。ほかの文字コードでは2byteだったりします。
また、文字列にシングルバイト(半角英数)とマルチバイト(全角)が混ざると対応できません。
ということで最初に言ったように、文字列オフセットの配列と同じ書き方はよほど意図しないかぎり使いません。
PHPでは、mb_***() の関数が用意されている、mbstringの拡張パッケージを使います。
UTF-8は、1~6byteで表現する可変サイズの文字コードで、通常使われる日本語は3byte表現に割り当てられています。
難しくて読めない漢字やあまり使われない全角文字は4byte表現に割り当てられます。
(5byte, 6byte領域はこれから増えるときのためのストック。)
配列のようで配列でない文字列オフセット
もうひとつ、文字列オフセットのループ処理をします。
<?php
$tmp = "sample";
foreach( $tmp as $char ) {
var_dump($char);
}
PHP Warning: Invalid argument supplied for foreach() in /home/lLcWjR/prog.php on line 5
string型の変数はforeachでは処理ができません。PHP8の警告のほうがもっと分かりやすく出ています。
PHP Warning: foreach() argument must be of type array|object, string given in /home/vagrant/string_offset.php on line 5
はっきりと、foreachでは配列かクラスオブジェクトを渡せと言ってます。
PHPでの文字列オフセットという言葉は、警告や通知、エラーで出ることが多いです。
エラーは修正するのが当たり前ですが、通知や警告であっても修正しましょう。
マルチバイトを扱っていると、動いても中身のデータがおかしくなってることがあります。
PHPの文字列オフセット
あたかも1文字の配列かのように使える。
マルチバイトの文字は1byteに分割されるので文字化けする。
[]は通常使ってはいけない。mbstringの関数を使う。
foreachでは使えない。(配列とはちがうと言いたいらしい。)
String型変数と文字列変数はちがう
String型変数と文字列変数はちがう
『何言ってんだ?』と思うでしょう。
String型は、2000年代の初頭にJavaが急激に人気が出はじめたことで知られた変数の型で、それ以前の文字列は文字の配列で表現していました。
代表的なのはC言語。C言語の文字列変数を見てみます。
#include <stdio.h>
#include <string.h>
int main(void) {
char tmp[12] = "sample";
printf("%s\n", tmp);
printf("length:%d\n", strlen(tmp));
for(int idx = 0; idx < strlen(tmp); idx++) {
printf("%c\n", tmp[idx]);
}
char tmp2[12] = "サンプル";
printf("%s\n", tmp2);
printf("length:%d\n", strlen(tmp2));
for(int idx = 0; idx < strlen(tmp2); idx++) {
printf("%c\n", tmp2[idx]);
}
return 0;
}
sample
length:6
s
a
m
p
l
e
サンプル
length:12
�
�
�
�
�
�
�
�
�
�
�
�
これ、見覚えありませんか?
はい、PHPの文字列オフセットと同じです。
C言語では文字列変数用の型はありません。1文字を入れるchar型の配列(char[])で表現します。
C言語のchar型のサイズは1byte。文字型ですが符号なし(unsigned char)を使ってbool値として使うなど、文字にしばられず1byteの値を入れるオールラウンダーの型。
ただ、マルチバイトの1文字分のデータは1byteで分割される。当然、マルチバイト用の関数が必要になる。
文字列変数ではもうひとつchar型のポインタ(char*)を使う方法もありますが、メモリに格納されるデータは同じ。
ポインタでは、あらかじめ変数の格納サイズを作っておく必要があります。
また、サンプルでは書いてませんが、char[], char*は、データを入れる前に変数領域をクリアする処理を入れます。
これで最初のセリフの意味がわかるでしょう。
String型変数と文字列変数(char[], char*)はちがう
String型 = 文字列型が一般化して15年以上は経っているので、String登場以前を知らないとイメージが湧かない文字列オフセットの盲点です。
つまり、PHPのstring型は過去の文字列型の性格が残ったちょっとややこしい奴。
(に見える。Javaをやってる人からすると。)
C言語の文字列オフセット
C言語にはString型がない。
文字列はchar型配列として扱うので文字列オフセットがイメージしやすい。
マルチバイトに対応していないのが弱点。
PHPのstring型は『もどき』
JavaのString型はクラスです。String型が登場したのはオブジェクト指向プログラム言語が最初。
(メジャーなプログラム言語では。)
PHPは今でこそオブジェクト指向でクラスを使いますが、かつては多くの関数が用意されたシェルスクリプトみたいなシンプルなものでした。
PHP8になってやっと変数の型を意識するようになりましたが、いまでも型宣言なしがメインの言語です。
(PHP7.4以降、クラスのプロパティで型宣言ができる。)
そんなPHPのstring型はオブジェクト指向のString型と同じではありません。
もともと型がない言語なので、『文字列の入った変数』という意味で使っています。
型宣言を一部するようになって『string型』と呼ぶようになりました。
PHPと似ているがシンプルで高機能なPython
ここからは、他の言語でも見てみましょう。まずは、PHPと同じ型宣言のないPythonから。
tmp = "sample"
for idx in range(len(tmp)):
print("c" + str(idx+1) + ": " + tmp[idx])
tmp = "サンプル"
for idx in range(len(tmp)):
print("c" + str(idx+1) + ": " + tmp[idx])
tmp = "sample"
for c in tmp:
print(c)
tmp = "サンプル"
for c in tmp:
print(c)
c1: s
c2: a
c3: m
c4: p
c5: l
c6: e
c1: サ
c2: ン
c3: プ
c4: ル
s
a
m
p
l
e
サ
ン
プ
ル
Pythonのスゴいところは、PHPのforeachに該当する、ループ処理で[]を使わない方法でもできるし、マルチバイトにも対応しているところ。
(python3のみ。python2は未対応。)
PHPと同じ、原始的な文字列の配列の性格をもっていて、ループ処理がオールOKの分かりやすい仕様。
Pythonの文字列オフセット
PHPと同じ文字の配列の性格をもつ。
ループ処理ができる。
型宣言がないのでstring型という概念はない。
JavaのString型。拒否ってる点で潔い
次は、String型を有名にしたJavaです。
charの配列ではマルチバイトに対応できない、文字コードの変換が難しいなど、文字列のいろいろな加工で不都合なところを補うためにString型は作られました。
Stringはクラスです。クラス内部にいろんなメソッドが用意されています。
(Stringクラスは文字列操作のスペシャリスト。)
そしてここがJavaの特長で、配列ではなくクラスオブジェクトなので[]やループ処理では使えません。エラーになります。
/* package whatever; // don't place package name! */
import java.util.*;
import java.lang.*;
import java.io.*;
/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
public static void main (String[] args) throws java.lang.Exception
{
String tmp = "sample";
System.printf(tmp[2]);
tmp = "サンプル";
System.printf(tmp[2]);
}
}
Main.java:13: error: array required, but String found
System.printf(tmp[2]);
^
Main.java:16: error: array required, but String found
System.printf(tmp[2]);
^
2 errors
文字列内の任意の文字などをいろいろしたいときは、Stringクラスに用意されているメソッドやその他の文字列を加工するクラスを使います。
(String型を文字の配列に変換可。)
Javaにはchar型もあります。ただサイズがC言語とはちがって2byte。
Javaの文字列オフセット
String型では文字列オフセットは使えない。(エラー)
char型配列を使うと文字列オフセットが使えるが、String変数を使いクラスのメソッドを使ったほうが便利。
C++のString型。やっぱりC言語の子孫。
もうひとつのオブジェクト指向プログラム言語のC++のstring型も見てみましょう。
#include <string>
using namespace std;
int main() {
string tmp("sample");
cout << "length:" << tmp.length() << endl;
for(int idx = 0; idx < tmp.length(); idx++) {
cout << "c" << idx+1 << ": " << tmp[idx] << endl;
}
string tmp2("サンプル");
cout << "length:" << tmp2.length() << endl;
for(int idx = 0; idx < tmp2.length(); idx++) {
cout << "c" << idx+1 << ": " << tmp2[idx] << endl;
}
return 0;
}
length:6
c1: s
c2: a
c3: m
c4: p
c5: l
c6: e
length:12
c1: �
c2: �
c3: �
c4: �
c5: �
c6: �
c7: �
c8: �
c9: �
c10: �
c11: �
c12: �
C++のstring型もクラスでstd::stringを使いますが、内部でchar[]の性格をもっているらしく、ループ処理ができます。が、マルチバイトには対応していない。
C++はC言語を拡張してオブジェクト指向に発展させた言語なので、C言語の文字列の性格を残しています。
結果も全く同じ。マルチバイトに対応していないところも。
マルチバイトに対応するにはライブラリを別途入れるか自作する必要があります。ここではC++の勉強ではないのでこのへんで。
C++の文字列オフセット
stringクラスにはchar型配列の性格があり文字列オフセットが使える。
まとめ
文字列オフセットは、原始的な文字列変数の性格を表しています。String型の挙動もプログラム言語によってさまざま。
PHPでややこしいのは、Pytonのように潔くループで回せるわけではなく、中途半端にできたりできなかったりするところ。
(forはできるがforeachはできない。)
またJavaのように、string型から文字列オフセットの性格を完全に消す潔さもない。
このへんどうにかして欲しいところだったんですが、PHP8では警告が分かりやすくなってるだけです。
string型の成り立ちであったり、それぞれの性格が出るのでしょうがないですが。
型宣言がない (型がゆるい) | string型で文字列オフセットの直接アクセス可。 |
型宣言あり (型が厳しい) | <オブジェクト指向> String型で文字列オフセットの直接アクセス不可。 別途char[]が使える。 <非オブジェクト指向> String型なし。 char[]で文字列オフセットが使える。 |
ボクの感覚ではこのように大まかに見ています。そのあとに細かいところを個々に見たほうが分かりやすいので。