暗黙の文字列変換

投稿者:: Yusuke Konno

概要: Delphi 2009における暗黙の文字列変換について解説します。

Delphi 2009ではUnicode化に伴い、UnicodeStringの新設と、AnsiStringの拡張による任意のコードページのサポートなどによって、多くの文字列型が存在しています。ここでは、文字列型の代入時に発生する文字列変換についての解説を行います。

    Delphi 2007以前とDelphi 2009の違い

    Delphi 2007以前:変換しないAnsiString

Delphi 2007までのStringはAnsiStringにマップされています。そして、このStringはコードページがデフォルトで固定されています。(日本語環境であればShift_JISになります)しかし、自動的に変換を行ったりしないので、EUCのデータを扱う時にもStringを用いたりします。

以下はStringの変数SにEUCの"あいう"に相当するコントロール文字列を代入してみました。

Code#001

Code#001
var
  S: String;
begin
  //EUC String 2007
  S := #$A4#$A2#$A4#$A4#$A4#$A6;//あいう
  ShowMessage(S); 
end;

このコードを実行すると以下のようになります。

Hide image
implict_convert_001

ShowMessageの表示は化けましたが、変数S内部のエレメントはコントロール文字列の通りになっています。

インデックス

1

2

3

4

5

6

文字

バイナリ

A4

A2

A4

A4

A4

A6

これをそのままテキストファイルとして書き出せば、適切なEUCのデータとして保存されます。当然、EUC対応のエディタであれば"あいう"という文字列として閲覧が可能です。

    D2009:コードページ付きのAnsiStringへの代入

さて、Delphi 2009ではAnsiStringが拡張され、任意のコードページで固定したAnsiStringを使えるようになりました。ここで、先ほどと同じ例で、EUC_JP(CP20932)のAnsiStringに代入をしてみます。

Code#002

//Code#002
type
  EUC = type AnsiString(20932);
var
  S: EUC;
begin
  //EUC String 2009
  S := #$A4#$A2#$A4#$A4#$A4#$A6;//あいう
  ShowMessage(S); 
end;

これを実行すると以下のような結果を得られます。

Hide image
implict_convert_001

コード的に"あいう"と表示されそうですが、実行結果はDelphi 2007と同じになってしまいます。しかし、両者には決定的な違いがあります。それは変数Sのエレメントがどうなっているかです。

インデックス

1

2

3

4

5

6

7

8

9

10

11

12

文字

バイナリ

8E

A2

8E

A4

8E

A2

8E

A2

8E

A2

8E

A6

Delphi 2007と違って、エレメントの数が12個になりました。また、各エレメントのバイナリも異なるものになっています。これをファイルに書き出した場合、EUC対応のエディタで閲覧しても"あいう"とは表示されません。(EUCのデータとしては適切ですが、文字列は意味のない羅列でしかありません。)おかしな文字のデータがそのまま書き出されることになります。

なぜ代入で予期せぬ事が起こったかというと、それは代入時に暗黙の文字列変換が起こっているからです。この暗黙の文字列変換によって、バイナリが全く違う値に書き換えられてしまっているのです。

    暗黙の文字列変換の仕組み

ここではまず、簡単な例を使って暗黙の文字列変換について説明します。Shift_JISで"あいう"という文字列を生成し、それをEUCの変数に代入します。

Code#003

//Code#003
type
  SJIS = type AnsiString(932);
  EUC = type AnsiString(20932);
var
  S: SJIS;
  E: EUC;
begin
  //SJIS -> EUC
  S := #$82#$A0#$82#$A2#$82#$A4;//あいう
  E := S;
  ShowMessage(E);
end;

上記コードを実行すると、以下のような結果が得られます。

Hide image
implict_convert_002

単に代入しただけですが、ちゃんと"あいう"として表示されています。この時何が起こったかを説明する前に、変数Sと変数Eのエレメントを見比べてみましょう。

変数S(CP932=Shift_JIS)のエレメント

インデックス

1

2

3

4

5

6

文字

バイナリ

82

A0

82

A2

82

A4

変数E(CP20932=EUC_JP)のエレメント

インデックス

1

2

3

4

5

6

文字

バイナリ

A4

A2

A4

A4

A4

A6

変数SもEも当然エンコーディングが違うので、ASCII以外の文字であればエレメントの取る値・数は異なってきますが、どちらも文字が共通しています。

何を当たり前のことを言っているのかと思ったかも知れませんが、ここが重要なポイントです。暗黙の文字列変換とは、「文字を極力維持して文字列を他のコードページに変換すること」だからです。つまり、変数Eに変数Sを代入すると、エレメント値がそのままコピーされるのではなく、できるだけ同じ文字になるように変換されてデータが格納されるのです。今回の例ではShift_JISにもEUC_JPにも"あいう"が存在するので、変数Eには正常に"あいう"として文字列が格納されました。

    コントロール文字列を代入すると何が起こるのか?

Code#002の例では、EUCの変数にダイレクトでコントロール文字列を代入しました。そして、代入したコントロール文字列はEUCの"あいう"に相当する正しい文字列でした。では、なぜ暗黙の文字列変換が起こったのでしょうか?

原因は、コントロール文字列の性質にあります。ANSIのコントロール文字列は、2つ以上が連結されているとAnsiStringと見なされます。ここで、AnsiStringはコードページを指定しなければデフォルトのコードページになるという性質がありました。すなわち、日本語環境ではShift_JISのAnsiString、すなわちtype AnsiString(932)と同じと言うことになります。ということは、先ほどのCode#003の例と同様に、Shift_JISの変数からEUC_JPの変数への代入が起こっているのと同じです。つまり、暗黙の文字列変換が発生してしまっています。

処理の流れは、

・コントロール文字列が2つ以上連結している

・コントロール文字列をAnsiStringと解釈する(コードページはデフォルト=932=Shift_JIS)

・EUCにShift_JISの文字列を代入する

・出来る限り文字列を維持してコードページを変換し、エレメントを格納する
となっています。

では、コントロール文字列と代入後の変数Sのエレメントを比較してみましょう。

コントロール文字列をAnsiStringとみなす

インデックス

1

2

3

4

5

6

文字

バイナリ

A4

A2

A4

A4

A4

A6

代入後の変数S

インデックス

1

2

3

4

5

6

7

8

9

10

11

12

文字

バイナリ

8E

A2

8E

A4

8E

A2

8E

A2

8E

A2

8E

A6

エレメント数、内容はまるで一致していませんが、文字が完璧に一致しています。つまり、暗黙の文字列変換が行われたという証拠です。

Delphi 2009では異なるコードページのAnsiStringに代入すると、勝手に暗黙の文字列変換が働きます。
ということは、従来のような文字列変換関数が基本的に必要ないと言うことです。

    UnicodeとANSI

    代入互換性の話

先ほどまでは馴染みのあるShift_JISとEUCの話でしたが、Delphi 2009ではUnicodeStringがメインです。では、UnicodeStringとAnsiStringやUTF8String(AnsiStringの一種ですが)などでは暗黙の文字列変換は起こるのでしょうか?

まず、暗黙の文字列変換の前に代入互換性の話をします。代入互換性とは、文字通り「ある型に別の型を代入が出来るかどうか」ということです。例えば、Integer型の変数にByte型の変数を代入しても何も問題は起こりません。この場合は代入互換性があると言います。

文字列型にも代入互換性という話は存在します。Delphi 2009で使用できる文字列型は大別すると、

  • UnicodeString
  • AnsiString(UTF8String、RawByteString含む)
  • WideString
  • UCS4String

の4つに分けられますが、このうちUCS4String以外は全てが互いに代入互換性を持ちます。

すなわち、
UnicodeStringにはAnsiString(UTF8String、RawByteString含む)もWideStringも代入できるし、
AnsiString(UTF8String、RawByteString含む)にはUnicodeStringもWideStringも代入できるし、
WideStringにはUnicodeStringもAnsiString(UTF8String、RawByteString含む)も代入できると言うことです。

つまり、これらの代入互換性のある型を代入すると、暗黙の文字列変換が起こると言うことです。

    UnicodeString <-> AnsiString

Code#003の例を今度はUnicodeStringとAnsiStringでやってみましょう。今回はAnsiStringをUnicodeStringに代入してみます。

Code#004

//Code#004
type
  SJIS = type AnsiString(932);
var
  S: SJIS;
  U: UnicodeString;
begin
  //AnsiString to UnicodeString
  S := #$82#$A0#$82#$A2#$82#$A4;//あいう
  U := S;
  ShowMessage(U);
end;

上記コードを実行すると、以下のような結果が得られます。

Hide image
implict_convert_002

これも"あいう"と表示されました。エレメントを比較してみましょう。

変数Sのエレメント(CP932=Shift_JIS)

インデックス

1

2

3

4

5

6

文字

バイナリ

82

A0

82

A2

82

A4

変数Uのエレメント(UnicodeString=UTF-16LE)

インデックス

1

2

3

文字

バイナリ

3042

3044

3046

やはり暗黙の文字列変換が行われています。そもそもAnsiStringとUnicodeStringではエレメントのサイズが異なるわけですから、エレメントをそのまま代入しようとしてもエレメント数が足りなくて代入できませんね。

    文字列リテラル

Delphi 2009では文字列リテラルはUnicodeStringとして解釈されます。つまり、UnicodeStringで扱える文字はすべて使用できると言うことです。(ただし、エディタの性能上結合文字列とかはオススメできませんが・・・)ということは、UnicodeString以外に文字列リテラルで代入すると暗黙の文字列変換が起こります。

    暗黙の文字列変換・代入互換性の落とし穴:文字列の欠損

    暗黙の文字列変換は変換後の文字列を保証しない

暗黙の文字列変換は文字列変換関数を不要にしたりするなど、一見すると便利な機能にも思えますが、大きな落とし穴があります。それは、変換後のコードページに存在しない文字列は問答無用で失われると言うことです。

例えば、UnicodeStringに"surrogatepair野屋"という文字列を代入し、それをAnsiStringに代入してみます。

Code#005

//Code#005
type
  SJIS = type AnsiString(932);
var
  S: SJIS;
  U: UnicodeString;
begin
  //UnicodeString to AnsiString
  U := #$D842#$DFB7+'野屋';//サロゲートペアを含む文字列
  S := U;
  ShowMessage(S);
end;

上記コードの実行結果は以下のようになります。

Hide image
implict_convert_003

実行した結果、サロゲートペアが失われました。至極当然ですね。Shift_JISには存在しない文字ですから。

ちなみに、AnsiStringからUnicodeString変換しても失われる文字もあります。

    DCC警告W1057・W1058の意味

暗黙の文字列変換が行われる場合、デフォルトの設定ではコンパイラは警告を出力してくれます。

  • [DCC 警告] ユニット名.pas(行数): W1057 文字列の暗黙的なキャスト ('型名' から '型名')
  • [DCC 警告] ユニット名.pas(行数): W1058 データ損失の可能性がある文字列の暗黙的なキャスト ('型名' から '型名')

W1057は、暗黙の文字列変換が発生していることを意味します。
W1058は、暗黙の文字列変換が発生し、結果として変換後に文字列が失われる可能性があることを示しています。

これらの警告が意図していないのに出ているのであれば、注意しなければなりません。なお、警告を消すために任意の文字列型でキャストをすることは無意味です。なぜなら、文字列はその任意の文字列型に変換されてしまうからです。

    UnicodeString <-> UTF8Stringの例外

UnicodeStringとAnsiStringの代入関係は文字列が失われる可能性があると書きましたが、UTF8Stringのみは例外です。なぜなら、UTF8Stringは型的にはtype AnsiString(65001)となっているため、AnsiStringの一種ですが、UTF-8自体が符号化する文字集合はUnicodeStringと同様にUnicodeです。つまり、UnicodeStringに存在する文字は全てUTF8Stringに存在します。同様に、UTF8Stringに存在する文字は全てUnicodeStringに存在します。したがって、UnicodeStringUTF8Stringでは変換しても文字列が失われることはありません。ただし、変換が伴う以上処理には時間を食います。つまり、無用な変換はいたずらにパフォーマンスを低下させるだけです。

    最良のコーディングは?

これまで延々と暗黙の文字列変換について述べましたが、なかなか複雑でややこしい現象が発生すると言うことがお分かり頂けたと思います。暗黙の文字列変換は便利ではありますが、発生すること事態がパフォーマンスロスになりますし、なによりコードページ次第ではデータロスを引き起こす可能性があります。つまり、暗黙の文字列変換は必要がないのであれば極力発生しないようにすべきです。

Delphi 2009における最良のコーディングは、UnicodeStringだけで文字処理を行うことです。なぜUnicodeStringだけを使うことが最良なのかと言うと、Delphi 2009ではVCL/RTLの全面的なUnicode化が完了しているため、VCLに文字列を渡す際、またはVCLから取得する際の文字列は基本的にUnicodeStringになっています。また、主要なルーチンの引数や戻り値の型にはUnicodeStringが使われています。つまり、UnicodeStringだけ使用していればまず変換が起こらなくて済むことになるわけです。変換が発生しないと言うことはパフォーマンスをロスせずに済みますし、データロスの心配もありません。また、予期せぬエラーの発生も最小限に抑えられます。したがって、特別な事情がない限りAnsiStringやUTF8String等を使用しないようにすれば、暗黙の文字列変換に頭を悩ませずに済みます。

次からのサーバー応答:: ETNASC03