DartのStringについて調べる

Dart公式ドキュメント、Language tourページ、Stringsセクション
Dart APIリファレンス、String classページ
これらの2つを読んでいく。

DartのStringはUTF-16のシーケンス、UTF-16がよくわからないけど日本語のようなマルチバイト主体の文字列の場合は効率が良さそう
文字列のリテラルはシングルクオートかダブルクオートでくくる、この辺りはJavaScriptと同じ
var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";
エスケープにはバックスラッシュ\
を使う。これもC言語など他の言語と同じ。

${expression}
を使うことで文字列の中で式を展開できる、これもJavaScriptと同じだがバッククオートを使わずに'
や"
でできるのは嬉しい。
ちなみに式expression
が識別子の場合は{}
を省略できる。
'Dart has $s, which is very handy.'
'${s.toUpperCase()} is very handy!'

試してみるにはDartPadが便利です。

連結するには文字列を続けるか+
を使う。
void main() {
print('1' '2' + '3');
}

比較するには==
を使う。2つの文字列が同一のシーケンスであればメモリ上のアドレスが違ってもtrue
となる。
void main() {
final a = "123";
final b = "1" + "2" + "3";
print(a == b); // true
}

'''
か"""
で括ることで複数行の文字列リテラルも作成できる。
void main() {
final str = """
a
b
c
""";
print(str);
print(str.length); // 20 = 6 + 6 + 6 + 2
}
最初の改行は含まれないが各行の先頭の空白や最後の改行は含まれるので注意。

文字列リテラルの前にr
を置くことでraw stringを作成できる。
raw stringを理解しようとすると沼にハマりそうなので何かは今は調べないことにする。

文字列リテラルは定数として扱われる。
${expr}
が使われている場合もexpr
が定数なら文字列リテラルも定数になる。

ここまでDart公式ドキュメント、Language tourページ、Stringsセクション、ここからDart APIリファレンス、String classページ

String classページの最初の方はLanguage tourページと同様の内容だが、追加の内容もある。
まずはcodeUnitAtとcodeUnits、1つの文字や文字列全体のUTF-16コードを取得できる
void main() {
const string = 'Dart';
final firstCodeUnit = string.codeUnitAt(0);
print(firstCodeUnit); // 68, aka U+0044, the code point for 'D'.
final allCodeUnits = string.codeUnits;
print(allCodeUnits); // [68, 97, 114, 116]
}

続いて添字演算子、n番目の文字1つからなる文字列を取得できる。
void main() {
const string = 'Dart';
String charAtIndex = string[0];
print(charAtIndex); // 'D'
}

続いてRune、これGo言語を勉強する時に訳がわからなかったやつだ
RuneとはUTF-16がデコードされたものだろうか
そしてRuneの1つ1つがUnicodeコードポイント(符号点)と呼ばれる、そんな感じ?
void main() {
const string = 'Dart😄';
final runes = string.runes.toList();
print(runes); // [68, 97, 114, 116, 128516]
print(string.codeUnits); // [68, 97, 114, 116, 55357, 56836]
}

関連リソースには下記がある
- StringBuffer: 文字列を生成するときに効率が良い
- RegExp: 正規表現
- 文字列と正規表現
正規表現の時は文字列にr
を前置するらしい、たしかraw stringのことだったよね?
raw stringってエスケープしないってこと?
void main() {
const string = r'\😄/';
print (string); // \😄/
}
どうやらそうらしい

Stringのコンストラクタには次の3つがある
- String.fromCharCode:文字コードから文字列を作る
- String.fromCharCodes:文字コードのシーケンスから文字列を作る
- String.fromEnvironment:環境変数から文字列を作る
文字コードってUnicode符号点のこと?UTF-16コードのこと?多分後者だよね?
void main() {
final string = String.fromCharCode(128516);
print(string); // 😄
}
良かった、あってた。よく考えればそうだよね、UTF-16だと2文字必要な時もあるから。
環境変数のコンストラクタはDartPadでは確認できないけど便利そう。
とはいえDartでバックエンドを書くことは無さそうだからあまり使わなそうではある。

Stringのインスタンスには7つのプロパティがある、すべて読み込み専用。
- codeUnits → List<int>:UTF-16コードのリスト
- hashCode → int:文字列のハッシュ値
- isEmpty → bool:空であるか否か
- isNotEmpty → bool:空ではないか否か、isEmptyの反対
- length → int:文字列の長さ、多分Unicodeとしての
- runes → Runes:Unicode符号点のイテラブル
- runtimeType → Type:ランタイム型、謎
とりあえず全部表示してみよう。
void main() {
final string = "Dart😄";
print(string.codeUnits);
print(string.hashCode);
print(string.isEmpty);
print(string.isNotEmpty);
print(string.length);
print(string.runes);
print(string.runtimeType);
}
実行結果
[68, 97, 114, 116, 55357, 56836]
499740029
false
true
6
(68, 97, 114, 116, 128516)
String
なんとlength
が6だ、Unicode符号点ではなくUTF-8のコードの数なんだ。
そしてハッシュコードってSHA-256のようなハッシュとは違うんだね、知らなかった、興味深い。

今更だけどStringはComparable<String>とPatternの2つのインタフェース(abstract class)を継承していることを知った。

allMatchesメソッドを使って検索できる。引数の方がhaystackになるのはJavaScriptとは反対なので注意が必要。
void main() {
final string = "Dart";
final matches = string.allMatches("Dart Dart Dart");
for (final match in matches) {
print("${match.start} ${match.end} ${match[0]}");
}
}
実行結果
0 4 Dart
5 9 Dart
10 14 Dart

codeUnitAtメソッドを使うとn番目のUTF-16コードユニットを取得できる。
void main() {
final string = "😄";
print(string.codeUnitAt(0)); // 55357
print(string.codeUnitAt(1)); // 56836
}

compareToメソッドを使うと文字列を辞書順で比較できる。
戻り値は下記のようになる。
- thisが引数より前なら-1
- thisが引数と同じなら0
- thisが引数より後なら1
void main() {
print("a".compareTo("b")); // -1
print("b".compareTo("b")); // 0
print("c".compareTo("b")); // 1
}

containsメソッドを使うと引数の文字列が含まれるか否かをチェックできる。
void main() {
print("Dart".contains("art")); // true
}

endsWithメソッドを使うと引数の文字列で終わるか否かをチェックできる。
void main() {
print("Dart".endsWith("art")); // true
}

indexOfメソッドを使うと文字列や正規表現が出現する位置を調べることができる。
見つからない場合は戻り値が-1になる。
void main() {
print("Dart".indexOf(RegExp(r"^[A-Z][a-z]+$"))); // 0
}

lastIndexOfメソッドを使うと文字列や正規表現が最後に出現する位置を調べることができる。
見つからない場合は戻り値が-1になる。
void main() {
print("Dart Dart".lastIndexOf(RegExp(r"[A-Z][a-z]+"))); // 5
}

matchAsPrefixメソッドを使うと文字列や正規表現で検索できる。
AsPrefixの名前通り、指定位置から開始していない場合は一致しているとは見なさない。
void main() {
print("Dart".matchAsPrefix("I love Dart")); // null
print("Dart".matchAsPrefix("I love Dart", 7)); // Instance of 'StringMatch'
}

noSuchMethodはdynamicを使っている時に存在しないプロパティやメソッドにアクセスした時に呼び出されるらしい。
void main() {
dynamic string = "Dart";
string.add("😄");
}
実行結果
Uncaught TypeError: B.JSString_methods.add$1 is not a functionError: TypeError: B.JSString_methods.add$1 is not a function

padLeftを使うと左側に指定した数の文字列をパディングできる
デフォルトではパディング文字は半角スペース
void main() {
print("1".padLeft(5));
print("123".padLeft(5));
print("12345".padLeft(5));
}
実行結果
1
123
12345

padRightを使うと右側に指定した数の文字列をパディングできる
デフォルトではパディング文字は半角スペース
void main() {
print("1".padRight(5, "x"));
print("123".padRight(5, "x"));
print("12345".padRight(5, "x"));
}
実行結果
1xxxx
123xx
12345

replaceAllメソッドを使うと文字列や正規表現でパターンマッチして置換できる。
void main() {
print("Dart is an art.".replaceAll("art", "ART")); // DART is an ART.
}

replaceAllMappedを使うとどのように置換するかを関数を通じて指定できる。
void main() {
print("Dart is an art.".replaceAllMapped("art", (match) {
return match[0]!.toUpperCase();
})); // DART is an ART.
}

replaceFirstとreplaceFirstMappedについてはreplaceAllとreplaceAllMappedの単一版で最初に見つかったものだけを置換する。
最後の引数として検索を開始する位置を指定できる。
void main() {
print("Dart is an art.".replaceFirst("art", "ART")); // DART is an art.
print("Dart is an art.".replaceFirstMapped("art", (match) {
return match[0]!.toUpperCase();
})); // DART is an art.
}

replaceRangeを使うと開始と終了の位置を指定して置換できる
終了位置は省略可
void main() {
print("Dart is an art.".replaceRange(1, 4, "ART")); // DART is an art.
}

splitメソッドを使うと文字列や正規表現でパターンを指定して文字列を分割できる
戻り値の型は文字列のリスト
void main() {
print("Dart is an art.".split(" ")); // [Dart, is, an, art.]
}

splitMapJoinメソッドを使うと分割、マップ、結合を一括でできる
void main() {
print("Dart is an art.".splitMapJoin(
" ",
onNonMatch: (str) => str.toUpperCase(),
)); // DART IS AN ART.
}

startsWithメソッドを使うと指定したパターンから開始しているか否かをチェックできる
第2引数として開始位置を指定できる
void main() {
print("Dart is an art.".startsWith("Dart")); // true
}

substringメソッドを使うと開始位置と終了位置を指定して部分文字列を取得できる
終了位置は省略可
JavaScriptのsliceと一緒だと思いきやマイナスの位置を指定できないので違う
void main() {
print("Dart is an art.".substring(0, 4)); // Dart
}

toLowerCaseとtoUpperCaseメソッドは文字列に含まれるすべての英字をそれぞれ小文字と大文字に置換する
void main() {
print("Dart".toLowerCase()); // dart
print("Dart".toUpperCase()); // DART
}

trimLeft, trimRightはそれぞれ先頭と末尾にある空白やタブなどを取り除く
trimはtrimLeftとtrimRightの合成関数
void main() {
print(" Dart ".trim()); // Dart
}

正直に言うとStringなんてどの言語も同じだろうとたかを括っていたが学びが多かった
確かに共通している点も多いが、少数の相違点がハマる原因になる
文字列について学ぶ時間を設けて良かったと思う

危ない、演算子を忘れるところだった
*
を使うと文字列をリピートできる
void main() {
print("Dart" * 3); // DartDartDart
}
+
を使うと連結できる
void main() {
print("Dart" + "Dart"); // DartDart
}
=
を使うと同一か否かをチェックできる
void main() {
print("Dart" == "Dart"); // true
}
[]
を使うとn番目の文字を文字列として取得できる
void main() {
print("😄"[0].codeUnits); // [55357]
print("😄"[1].codeUnits); // [56836]
}
UTF-16コードユニットを無理に文字列にしようとするので絵文字などを使うときは注意

次はDateTimeについて調べよう