文字

文字は文字列の構成要素である。文字体系の記号は、アルファベット、数字、句読点、スペース文字など、すべて"文字"と呼ばれる。紛らわしいが、文字の構成要素も"文字"と呼ばれる。

文字の配列が文字列を構成する。配列については前の章で説明した。文字列については2章後に説明する。

他のデータと同様に、文字もビットで構成される整数値として表される。例えば、小文字の'a'の整数値は97、数字の'1'の整数値は49だ。これらの値は、ASCII規格が設計されたときに割り当てられた単なる慣例である。

多くのプログラミング言語では、文字はchar型で表現され、256個の異なる値しか保持できない。他の言語でchar型に慣れている人は、この型では多くの文字体系の記号をサポートするには不十分であることをご存じだろう。Dの3種類の文字型について説明する前に、まずコンピュータシステムにおける文字の歴史について見てみよう。

歴史
ASCIIテーブル

ASCIIテーブルは、現在のシステムに比べてコンピュータのハードウェアが非常に制限されていた時代に設計された。ASCIIテーブルは7ビットを基本としているため、128個の異なるコード値を持つことができる。この数の異なる値があれば、基本的なラテンアルファベットの26文字の大文字と小文字、数字、よく使われる句読点、および一部の端末制御文字を表現するのに十分だ。

例として、文字列 "hello"の文字のASCIIコードは次の通りだ(読みやすくするためにコンマを挿入している):

104101108108111

上記の各コードは、文字列 "hello"の 1文字を表している。例えば、'l'の2文字には2つの108値が対応している。

ASCIIコードのコード数は後に8ビットに拡張され、拡張ASCIIコード表となった。拡張ASCIIコード表には256の異なるコードが含まれる。

IBMコードページ

IBM Corporationは、拡張ASCIIテーブルの128から255までのコードを1つ以上の文字体系に割り当てる一連のテーブルを定義した。これらのコードテーブルにより、より多くのアルファベットの文字をサポートすることが可能になった。例えば、トルコ語のアルファベットの特殊文字は、IBMのコードページ857の一部だ。

ASCIIよりもはるかに便利であるにもかかわらず、コードページにはいくつかの問題と制限がある。テキストを正しく表示するには、そのテキストがもともとどのコードページで記述されているかを知っている必要がある。これは、ほとんどの他のテーブルでは、同じコードが別の文字に対応しているためだ。例えば、テーブル857で'Ğ'を表すコードは、テーブル437では'ª'に対応する。

単一の文書で複数のアルファベットをサポートする難しさに加え、128文字を超える非ASCII文字を含むアルファベットは、IBM テーブルではまったくサポートできない。

ISO/IEC 8859コードページ

ISO/IEC 8859コードページは、国際標準化の結果生まれたものだ。文字にコードを割り当てる方法は、IBMのコードページと似ている。例えば、トルコ語のアルファベットの特殊文字は、コードページ8859-9に記載されている。これらのテーブルには、IBMのテーブルと同じ問題や制限がある。例えば、オランダ語の二重文字ijは、これらのテーブルには一切記載されていない。

Unicode

Unicodeは、これまでの解決策のすべての問題と制限を解決している。Unicodeには、現在および過去の多くの言語の文字体系の 10 万以上の文字と記号が含まれている。(新しい文字は、表に追加するために常に検討されている。) これらの文字にはそれぞれ固有のコードが割り当てられている。Unicodeでエンコードされた文書には、別々の文字体系のすべての文字を、混乱や制限なく含めることができる。

Unicodeエンコーディング

Unicodeは、各文字に一意のコードを割り当ててる。8ビットの値で表現できる文字数よりもUnicode文字の数が多いので、一部の文字は2つ以上の8ビットの値で表現する必要がある。例えば、'Ğ'(286)のUnicode文字コードは、ubyteの最大値よりも大きい。

電子メディアで文字がどのように表現されるかをエンコーディングと呼ぶ。上記で、文字列 "hello"がASCIIでエンコードされる方法を見てきた。ここでは、Dの文字型に対応する3つのUnicodeエンコーディングについて説明する。

UTF-32:このエンコーディングは、各Unicode文字に32ビット(4バイト)を使用する。UTF-32エンコーディングの "hello"はASCIIエンコーディングと類似しているが、各文字は4バイトで表される:

000104
000101
000108
000108
000111

別の例として、のUTF-32エンコーディングは "aĞ"は次の通りだ:

00097
00130

注釈:UTF-32のバイトの順序は、コンピュータシステムによって異なる場合がある。

'a'および'Ğ'は、それぞれ1バイトと2バイトの有効バイトで表現され、他の5バイトの値はすべて0である。これらの0は、各Unicode文字が4バイトずつを占めるようにするためのフィラーバイトと考えることができる。

基本ラテン文字に基づく文書では、このエンコーディングは常にASCIIエンコーディングの4倍のバイト数を使用する。文書内の文字の大部分がASCII相当文字である場合、それらの各文字の3つの埋め込みバイトにより、このエンコーディングは他のエンコーディングに比べて無駄が多くなる。

一方、すべての文字を同じバイト数で表現することには利点もある。例えば、次のUnicode文字は常に正確に4バイト先にある。

UTF-16:このエンコーディングは、ほとんどのUnicode文字を16ビット(2バイト)で表現する。16ビットでは約65,000個の値しか表現できないため、あまり使用されない35,000個のUnicode文字は、追加のバイトを使用して表現する必要がある。

例えば、 "aĞ"はUTF-16で4バイトでエンコードされる:

097
130

注釈:UTF-16のバイトの順序は、コンピュータシステムによって異なる場合がある。

UTF-32と比較すると、このエンコーディングはほとんどの文書でより少ないスペースで済むが、一部の文字は2バイト以上で表現する必要があるため、UTF-16は処理が複雑になる。

UTF-8:このエンコーディングでは、1文字につき1から4バイトを使用する。ASCIIテーブルに同等の文字がある場合、その文字はASCIIテーブルと同じ数値コードで1バイトで表現される。その他のUnicode文字は2、3、または4バイトで表現される。ヨーロッパの文字体系の特殊文字のほとんどは、2バイトで表現される文字のグループに含まれている。

欧米諸国のほとんどの文書では、UTF-8が最も少ないスペースで済むエンコーディングである。UTF-8のもう1つの利点は、ASCIIを使用して作成された文書を、変換せずにUTF-8文書として直接開くことができることだ。また、UTF-8では、すべての文字が意味のあるバイトで表現されるため、フィラーバイトによるスペースの無駄がない。例えば、"aĞ"は次のようになる:

97196158
Dの文字型

文字を表すD型には3種類ある。これらの文字は、上記の3種類のUnicodeエンコーディングに対応している。基本型の章から引用する。

定義初期値
charUTF-8コードユニット0xFF
wcharUTF-16コードユニット0xFFFF
dcharUTF-32コード単位とUnicodeコードポイント0x0000FFFF

他のプログラミング言語とは異なり、Dの文字は異なるバイト数で構成される場合がある。例えば、'Ğ'はUnicodeでは2バイト以上で表現しなければならないため、char型の変数には収まらない。一方、dcharは4バイトで構成されるため、Unicodeの任意の文字を格納することができる。

文字リテラル

リテラルは、ソースコードの一部としてプログラムに記述される定数値だ。Dでは、文字リテラルは単一引用符で囲んで指定する。

char  letter_a = 'a';
wchar letter_e_acute = 'é';
D

ダブルクォートは文字には使用できない。ダブルクォートは文字列を指定する際に使用され、これは後で説明するからだ'a'は文字リテラルであり、 "a"は単一の文字からなる文字列リテラルである。

char型の変数は、ASCIIテーブルにある文字しか保持できない。

コードに文字を挿入する方法はいくつかある:

これらの方法は、文字列内の文字を指定する場合にも使用できる。例えば、次の2行は、同じ文字列リテラルを持っている。

writeln("Résumé preparation: 10.25€");
writeln("\x52\ésum\u00e9 preparation: 10.25\€");
D
制御文字

一部の文字は、テキストのフォーマットにのみ影響し、それ自体には視覚的な表現はない。例えば、出力を新しい行に継続することを指定する改行文字には、視覚的な表現はない。このような文字は、制御文字と呼ばれる。一般的な制御文字のいくつかは、 \control_character構文で指定できる。

構文定義
\n改行表示を新しい行に移動する
\rキャリッジリターン現在の行の先頭に表示を移動する
\tタブ表示を次のタブストップに移動する

例えば、自動的に新しい行を開始しないwrite()関数は、\n文字ごとに新しい行を開始する。次のリテラル内で\n制御文字が現れるたびに、新しい行が開始される。

write("first line\nsecond line\nthird line\n");
D

出力:

最初の行
2行目
3行目
シングルクォートとバックスラッシュ

シングルクォート文字自体はシングルクォート内に記述できない。コンパイラは2つ目のシングルクォートを1つ目のシングルクォートの終了文字と解釈するからだ:'''。最初の2つは開始と終了のシングルクォートとなり、3つ目は無視され、コンパイルエラーが発生する。

同様に、バックスラッシュ文字は制御文字とリテラル構文で特別な意味を持つため、コンパイラはそれをそのような構文の開始と解釈する:'\'。コンパイラは閉じシングルクォート文字を探すが、見つからず、コンパイルエラーを発生させる。

これらの理由から、シングルクォートとバックスラッシュ文字は、前のバックスラッシュ文字でエスケープされる:

構文名前定義
\'シングルクォートシングルクォート文字を指定する: '\''
\\バックスラッシュバックスラッシュ文字を指定可能: '\\' または "\\"
std.uni モジュール

std.uniモジュールには、Unicode文字の処理に便利な関数が含まれている。このモジュールについては、そのドキュメントを参照。

isで始まる関数は、文字に関する特定の質問に答える。結果は、答えが"いいえ"の場合はfalse、"はい"の場合はtrueとなる。これらの関数は、論理式で役立つ。

toで始まる関数は、既存の文字から新しい文字を生成する。

これらの関数をすべて使用するプログラムを以下に示す。

import std.stdio;
import std.uni;

void main() {
    writeln("Is ğ lowercase? ", isLower('ğ'));
    writeln("Is Ş lowercase? ", isLower('Ş'));

    writeln("Is İ uppercase? ", isUpper('İ'));
    writeln("Is ç uppercase? ", isUpper('ç'));

    writeln("Is z alphabetic? ",       isAlpha('z'));
    writeln("Is \€ alphabetic? ", isAlpha('\€'));

    writeln("Is new-line whitespace? ",   isWhite('\n'));
    writeln("Is the underscore whitespace? ", isWhite('_'));

    writeln("The lowercase of Ğ: ", toLower('Ğ'));
    writeln("The lowercase of İ: ", toLower('İ'));

    writeln("The uppercase of ş: ", toUpper('ş'));
    writeln("The uppercase of ı: ", toUpper('ı'));
}
D
characters.1

出力:

ğは小文字か?true
Şは小文字か?false
İは大文字か?true
çは大文字か?false
zはアルファベットか?true
€はアルファベットか?false
改行は空白文字か?true
アンダーバーは空白文字か?false
Ğの小文字はğ
İの小文字はi
şの大文字はŞ
ıの大文字はI
ıとiに対する限定的なサポート

'ı''i'の小文字と大文字は、一部のアルファベット(例: トルコ語アルファベット)では一貫してドット付きまたはドットなしになる。他のほとんどのアルファベットではこの点で一貫性がない: ドット付きの'i'の大文字はドットなしの'I'になる。

コンピュータシステムはASCIIテーブルから始まったため、伝統的に、'i'の大文字は'I''I'の小文字は'i'となっている。そのため、この2つの文字には特別な注意が必要だ。次のプログラムは、この問題を示す。

import std.stdio;
import std.uni;

void main() {
    writeln("The uppercase of i: ", toUpper('i'));
    writeln("The lowercase of I: ", toLower('I'));
}
D
characters.2

出力は基本ラテン文字アルファベットに従っている:

iの大文字はI
Iの小文字はi

文字は、通常、Unicode文字コードによって大文字と小文字に変換される。この方法は、多くのアルファベットでは問題になる。例えば、アゼルバイジャン語やケルト語のアルファベットでは、'I'の小文字が'i'になってしまうという同じ問題がある。

ソートにも同様の問題がある。'ğ''á'のような多くの文字は、基本ラテン文字でも'z'の後にソートされることがある。

文字の読み取りに関する問題

DのUnicode文字の柔軟性とパワーは、入力ストリームから文字を読み込む際に予期しない結果を引き起こす可能性がある。この矛盾は、文字という用語の複数の意味に起因する。これについてさらに説明する前に、この問題を示すプログラムを見てみよう:

import std.stdio;

void main() {
    char letter;
    write("Please enter a letter: ");
    readf(" %s", &letter);
    writeln("The letter that has been read: ", letter);
}
D
characters.3

このプログラムをUnicodeを使用しない環境で実行すると、非ASCII文字も正しく読み込まれて表示されるかもしれない。

一方、同じプログラムをUnicode環境(例:Linuxターミナル)で実行すると、出力に表示される文字が入力した文字と異なる場合がある。これを確認するため、UTF-8エンコーディングを使用するターミナル(ほとんどのLinuxターミナル)に非ASCII文字を入力しよう:

文字を入力してください: ğ
読み込まれた文字:   ← 出力に文字はない

この問題の原因は、'ğ'のような非ASCII文字は2つのコードで表されており、入力からcharを読み込むと、そのうちの最初のコードのみが読み込まれるためだ。この単一のcharではUnicode文字全体を表現できないため、プログラムは表示するための完全な文字を持っていない。

文字を構成するUTF-8コードが実際に1つのcharずつ読み込まれることを示すため、2つのchar変数を読み込み、それらを順番に表示しよう:

import std.stdio;

void main() {
    char firstCode;
    char secondCode;

    write("Please enter a letter: ");
    readf(" %s", &firstCode);
    readf(" %s", &secondCode);

    writeln("The letter that has been read: ",
            firstCode, secondCode);
}
D
characters.4

プログラムは入力から2つのchar変数を読み込み、読み込んだ順に表示する。これらのコードが同じ順序でターミナルに送信されると、ターミナル上でUnicode文字のUTF-8エンコーディングが完了し、今回はUnicode文字が正しく表示される:

文字を入力してください: ğ
読み込まれた文字: ğ

これらの結果は、プログラムの標準入力と標準出力はcharストリームであることとも関連している。

文字列の章で後で見るように、UTFコードを個別に扱うのではなく、文字を文字列として読み込む方が簡単だ。

DのUnicodeサポート

Unicodeは大規模で複雑な標準規格だ。Dはその非常に有用なサブセットをサポートしている。

Unicodeエンコードされた文書は、以下の概念のレベルから構成される。下位から上位の順に:

要約