文字列

これまで見てきた多くのプログラムで、文字列を使用してきた。文字列は、この3章で説明した2つの機能、つまり文字と配列を組み合わせたものだ。最も単純な定義では、文字列は文字の配列に他ならない。例えば、char[]は文字列の一種だ。

この単純な定義は誤解を招くかもしれない。文字の章で見たように、Dには3種類の文字型がある。これらの文字型の配列は3種類の文字列型になり、そのうちのいくつかは、文字列操作で予想外の結果になる場合がある。

readln stripではなく、readf

ターミナルから文字列を読み込む際にも、意外な結果が生じる場合がある。

文字配列であるため、文字列には '\n'なども含めることができる。入力から文字列を読み込む際、入力の最後に押されたEnterキーに対応する制御文字も文字列の一部として読み込まれる。さらに、readf()に読み込む文字数を指定する方法は存在しないため、入力の最後まで読み込み続ける。これらの理由から、文字列を読み込む際にreadf()は意図したとおりに動作しない:

import std.stdio;

void main() {
    char[] name;

    write("What is your name? ");
    readf(" %s", &name);

    writeln("Hello ", name, "!");
}

ユーザーが名前を入力後に押すEnterキーは入力を終了しない。readf()は文字列に追加する文字を待ち続ける:

お名前はなんですか? メルト
   ← Enter キーが押されたにもかかわらず、入力が終了していない
   ← (ここでEnterキーが2回押されたものと仮定する)

ターミナルで標準入力ストリームを終了する1つの方法は、UnixベースのシステムではCtrl-Dを、WindowsシステムではCtrl-Zを押すことだ。ユーザーが最終的にその方法で入力を終了すると、改行文字も文字列の一部として読み込まれていることがわかる。

こんにちは、メルト
   ← 名前の後に改行文字
!  ← (感嘆符の直前に1つだけ)

これらの文字の後に感嘆符が表示され、名前直後に表示されない。

readln()は、文字列の読み取りに適している。readln()は"read line"の略で、行の終わりまで読み込む。" %s"フォーマット文字列と&オペレーターが不要だからだ:

import std.stdio;

void main() {
    char[] name;

    write("What is your name? ");
    readln(name);

    writeln("Hello ", name, "!");
}

readln()改行文字も格納する。これにより、プログラムは入力が完全な行だったか、入力の終わりに達したかを判断する手段を持つ:

お名前はなんですか? メルト
こんにちは、メルト
!  ← 感嘆符の前に改行文字がある

文字列の両端にある制御文字およびすべての空白文字は、std.string.stripで削除できる:

import std.stdio;
import std.string;

void main() {
    char[] name;

    write("What is your name? ");
    readln(name);
    name = strip(name);

    writeln("Hello ", name, "!");
}

上記のstrip()式は、末尾の制御文字を含まない新しい文字列を返す。その戻り値をnameに再代入すると、意図した出力が得られる。

お名前はなんですか? メルト
こんにちは、メルト!    ← 改行文字なし

readln()は、パラメータなしで使うこともできる。その場合、その関数は、読み込んだ行を返すreadln()の結果をstrip()に連鎖することで、より短く、より読みやすい構文にすることができる。

string name = strip(readln());
D

以下でstring型を紹介した後、この形式を使い始めることにする。

formattedRead文字列のパース用に

入力または他のソースから行が読み込まれると、std.formatモジュール内のformattedRead()を使用して、その行に含まれるデータをパースして変換することができる。最初のパラメーターはデータを含む行で、残りのパラメーターはreadf()とまったく同じように使用される:

import std.stdio;
import std.string;
import std.format;

void main() {
    write("Please enter your name and age," ~
          " separated with a space: ");

    string line = strip(readln());

    string name;
    int age;
    formattedRead(line, " %s %s", name, age);

    writeln("Your name is ", name,
            ", and your age is ", age, '.');
}
お名前と年齢をスペースで区切って入力してください: メルト 30
あなたの名前はメルトで、年齢は30歳です。

readf()formattedRead()は、解析して正常に変換できた項目の数を返す。この値を、予想されるデータ項目の数と比較することで、入力を検証することができる。例えば、上記のformattedRead()2つの項目(stringを名前、intを年齢)を読み込むことを期待しているので、次のチェックでそれが実際にそうであるかどうかを確認する。

uint items = formattedRead(line, " %s %s", name, age);

if (items != 2) {
    writeln("Error: Unexpected line.");

} else {
    writeln("Your name is ", name,
            ", and your age is ", age, '.');
}
D

入力がnameageに変換できない場合、プログラムはエラーを表示する:

お名前と年齢をスペースで区切って入力してください: メルト
Error: 予期しない行。
ダブルクォートではなくシングルクォート

単一引用符は文字リテラルを定義するために使用されることを確認した。文字列リテラルは二重引用符で定義される。 'a'は文字である; "a"は文字である。

stringwstring、およびdstringは不変だ。

3つの文字型に対応する3つの文字列型がある。char[]wchar[]、およびdchar[]だ。

これらの型の不変バージョンには、stringwstringdstring3つのエイリアスがある。これらのエイリアスで定義された変数の文字は変更できない。例えば、wchar[]の文字は変更できるが、wstringの文字は変更できない。(Dの不変性の概念については、後の章で説明する。)

例えば、stringの最初の文字を大文字にしようとした次のコードは、コンパイルエラーになる。

string cannotBeMutated = "hello";
cannotBeMutated[0] = 'H';             // ← コンパイルエラー
D

変数をstringのエイリアスではなくchar[]として定義する方法を考えるかもしれないが、これもコンパイルできない:

char[] a_slice = "hello";  // ← コンパイルエラー
D

今回は、2つの要因が組み合わさってコンパイルエラーが発生している:

  1. 文字列リテラルの型は "hello"stringで、char[]ではないため、不変である。
  2. 左側のchar[]はスライスであり、コードがコンパイルされれば、右側のすべての文字にアクセスできる。

char[]は変更可能であり、stringは変更不可能であるため、不一致が生じる。コンパイラは、変更不可能な配列の文字に、変更可能なスライスを介してアクセスすることを許可しない。

この問題を解決するには、.dupプロパティを使用して、不変文字列のコピーを作成する。

import std.stdio;

void main() {
    char[] s = "hello".dup;
    s[0] = 'H';
    writeln(s);
}

これでプログラムはコンパイル可能になり、変更された文字列が表示される:

こんにちは

同様に、stringが必要な場所では、char[]を使用することはできない。このような場合、.idupプロパティを使用して、変更可能なchar[]変数から不変のstring変数を作成することができる。例えば、schar[]型の変数である場合、次の行はコンパイルに失敗する。

string result = s ~ '.';          // ← コンパイルエラー
D

sの型がchar[]の場合、上記の代入の右側の式の型もchar[]になる。.idupは、既存の文字列から不変の文字列を生成するために使う。

string result = (s ~ '.').idup;   // ← 現在コンパイルできる
D
文字列の長さの潜在的な混乱

一部のUnicode文字は1バイト以上で表現されることをこれまで見てきた。例えば、文字'é'(ラテン文字の'e'にアクセント記号が付いたもの)は、少なくとも2バイトのUnicodeエンコーディングで表現される。この事実は、文字列の.lengthプロパティにも反映されている。

writeln("résumé".length);
D

"résumé"は6文字から構成されているが、stringの長さは、含まれるUTF-8コード単位の数である:

8

のような文字列リテラルの要素の型は "hello"の要素の型はcharで、各char値はUTF-8コード単位を表する。このことで問題となるのは、2コード単位の文字を1コード単位の文字に置き換えようとした場合である。

char[] s = "résumé".dup;
writeln("Before: ", s);
s[1] = 'e';
s[5] = 'e';
writeln("After : ", s);
D

2つの'e'文字は2つの'é'文字を置き換えず、単一のコード単位を置き換えるため、無効なUTF-8エンコーディングになる:

前: résumé
後 : re�sueé    ← 間違い

上記のコードのように、文字、記号、その他のUnicode文字を直接扱う場合、使用する正しい型はdcharだ。

dchar[] s = "résumé"d.dup;
writeln("Before: ", s);
s[1] = 'e';
s[5] = 'e';
writeln("After : ", s);
D

出力:

résumé
resume

新しいコードの2つの違いに注意。

  1. 文字列の型はdchar[]だ。
  2. リテラル"résumé"dの末尾には、その型をdcharの配列として指定するdがある。

いずれにせよ、dchar[]dstringの使用は、Unicode文字の操作に関するすべての問題を解決するわけではないことに注意しよう。例えば、ユーザーが"résumé"というテキストを入力した場合、dchar文字列であっても、文字列の長さが6文字になるとは限らない。例えば、'é'文字の少なくとも1つが単一のコードポイントとしてエンコードされていない場合、組み合わせ文字としてエンコードされている場合、文字列の長さはより長くなる可能性がある。このような問題やその他の多くのUnicode問題を回避するため、プログラムではUnicode対応のテキスト操作ライブラリを使用することを検討。

文字列リテラル

文字列リテラルの後に指定されるオプションの文字は、文字列の要素の型を決定する。

import std.stdio;

void main() {
     string s = "résumé"c;   // "résumé"と同じ
    wstring w = "résumé"w;
    dstring d = "résumé"d;

    writeln(s.length);
    writeln(w.length);
    writeln(d.length);
}

出力:

8
6
6

"résumé"のすべてのUnicode文字は、単一のwcharまたはdcharで表せるため、最後の2つの長さは文字数と等しくなる。

文字列の連結

文字列は実際には配列であるため、すべての配列操作を文字列にも適用できる。~は2つの文字列を連結し、~=は既存の文字列に文字列を付加する:

import std.stdio;
import std.string;

void main() {
    write("What is your name? ");
    string name = strip(readln());

    // 連結:
    string greeting = "Hello " ~ name;

    // 追加:
    greeting ~= "! Welcome...";

    writeln(greeting);
}

出力:

お名前はなんですか? Can
こんにちは、Can! ようこそ...
文字列の比較

注釈:Unicodeは、Unicodeコード以外の文字の順序を定義していない。そのため、以下の例では、予想と異なる結果になる場合がある。

これまで、整数値や浮動小数点値に対して、比較演算子<>=などを使用してきた。これらの演算子は文字列に対しても使用できるが、意味は異なる。文字列は辞書順で並べられる。この順序では、各文字のUnicodeコードが、架空の巨大なUnicodeアルファベットにおけるその文字の位置となる。この架空のアルファベットでは、小さい大きいという概念は、後という概念に置き換えられる。

import std.stdio;
import std.string;

void main() {
    write("      Enter a string: ");
    string s1 = strip(readln());

    write("Enter another string: ");
    string s2 = strip(readln());

    if (s1 == s2) {
        writeln("They are the same!");

    } else {
        string former;
        string latter;

        if (s1 < s2) {
            former = s1;
            latter = s2;

        } else {
            former = s2;
            latter = s1;
        }

        writeln("'", former, "' comes before '", latter, "'.");
    }
}

UnicodeはASCIIテーブルの基本ラテン文字を採用しているため、ASCIIテーブルの文字のみを含む文字列は常に正しく順序付けられる。

小文字と大文字は異なる

各文字には固有のコードがあるため、文字のバリエーションはすべて互いに異なる。例えば、Unicode文字列を直接比較すると、"A"と"a"は異なる文字になる。

さらに、ASCIIコード値の結果として、ラテン文字の大文字はすべて小文字よりも先にソートされる。例えば、"B"は"a"よりも前に来る。std.stringモジュールにあるicmp()関数は、文字列を大文字と小文字を区別せずに比較する必要がある場合に使用できる。このモジュールの関数は、オンラインのドキュメントで確認できる。

文字列は配列(そして当然、範囲)であるため、std.arraystd.algorithmstd.rangeモジュールの関数は、文字列にも非常に役立つ。

演習
  1. std.stringstd.arraystd.algorithm、およびstd.rangeモジュールのドキュメントを参照しよう。
  2. ~演算子を使ったプログラムを書いてみよう。ユーザーは、名と姓をすべて小文字で入力する。名と姓を適切な大文字小文字で表記したフルネームを出力しよう。例えば、文字列が"ebru"と"domates"の場合、プログラムは "Ebru Domates"と出力する必要がある。
  3. 入力から1行を読み込み、その行の最初の"e"と最後の"e"の間の部分を出力する。例えば、行が"this line has five words"の場合、プログラムは"e has five"と出力する。

    スライスを作成するために必要な2つのインデックスを取得するには、indexOf()およびlastIndexOf()関数が便利だ。

    そのドキュメントにも記載されているように、indexOf()およびlastIndexOf()の戻り値の型は、intでもsize_tでもなく、ptrdiff_tである。その正確な型の変数を定義する必要があるかもしれない。

    ptrdiff_t first_e = indexOf(line, 'e');
    D

    autoキーワードを使用して変数を定義することは可能で、これは後述の章で説明する:

    auto first_e = indexOf(line, 'e');
    D