Substring()を使わずに文字列抽出する
文字列の一部を抽出するときはよくSubstringメソッドを使っていましたが、これをインデクサーを使った書き方に置き換えることができます。
Substringメソッドを使っていたこのコードは
string str1 = str.Substring(1, 3);
C#8.0 以降、このようなコードに置きかえることができます!
string str1 = str[1..4];
インデクサー
配列では添え字を指定して要素にアクセスする仕組みになっています。
C#では、配列だけでなく、例えばList型やstring型など、配列と同様の特徴をもつクラス、構造体、インタフェースでも添字を指定して要素にアクセスできる仕組みを備えています。
string[] words1 = new string[] { "foo", "bar", "baz", "hoge", "fuga", "piyo" };
Console.WriteLine(array[2]); // "baz"が出力される。
インデクサーという用語は正確には、配列以外のオブジェクトで配列と同様に添え字指定機能を使える仕組みのことを指します。
ただC#では、配列と配列以外の添え字指定機能は同等なので、本記事では便宜上、両者をまとめてインデクサーと呼びます。
インデクサーの機能が進化した
C#8.0からインデクサーに下記の機能が加わっています。
- 範囲指定
- 降順インデックス指定(「下i桁目」指定)
下記のコードは範囲指定の例です。
出力してみましょう。
string[] words1 = new string[] { "foo", "bar", "baz", "hoge", "fuga", "piyo" };
string[] words2 = words1[1..4]; // 範囲指定の例です。
foreach (var word in words2)
{
Console.WriteLine(word); // "bar", "baz", "hoge"が出力されます。
}
混乱しないインデックスの解釈
先の例の [1..4]
は、添え字1番目~4番目の範囲 という意味になりますが、
「添え字4番目の要素は抽出されず、4番目の直前までが抽出されます。」
こう説明すると少し複雑に感じてしまうかもしれません。
詳しい構文は後述しますが、その準備としてまず混乱しない解釈方法を紹介します。
普段は、1つの要素に1つのインデックス(=添え字)が割り振られているとイメージすることが多いと思いますが…
本件では、イテレータのようにインデックスの間に要素が存在していると解釈をしたほうがスムーズです。
すると、さっきの例の [1..4]
の範囲指定のイメージは下図のようになります。
このような解釈をしていれば「範囲の最初は含むけど最後は含まない…」などの複雑な理解は必要ありません。
string型にもインデクサーが使える
C#では、string型をchar型配列のように扱うことができます。
string str = "Hello world";
char ch = str[8];
Console.WriteLine(ch); // 'r'が出力されます。
string型が存在しないC言語では、文字列をchar型配列で表現します。
C++からはstringクラスが登場し、char型配列(動的なので正確にはポインタ)をその内部で扱えるようになりました。
この流れを汲むC#では、string型の変数に対しても、char型配列であるかのようにインデクサーを使える柔軟性を持っています。
構文
C#8.0から使えるようになったインデクサーの構文を紹介します。
元となる文字列を1つ用意します。
string str = "012345";
1. 範囲指定
範囲演算子..
を使います。
i..j
添え字i~jの範囲を抽出します。
string str2 = str[1..4]; // index=1から、index=4まで -> "123"
string str3 = str[0..3]; // index=0から、index=3まで -> "012"
ぜひSubstring()を使った場合と比べてみてください。
string str1 = str.Substring(1, 3); // index=1から連続する3文字 -> "123"
string str2 = str[1..4]; // index=1から、index=4まで -> "123"
さらに、範囲の開始位置と終了位置は省略することもできます。
i..
添え字iから末尾までの範囲を抽出します。
..i
先頭から添え字iまでの範囲を抽出します。
..
先頭から末尾までの範囲を抽出します。
string str4 = str[2..]; // index=2から(末尾まで) -> "2345"
string str5 = str[..3]; // (先頭から)index=3まで -> "012"
string str6 = str[..]; // (先頭から末尾まで) -> "012345"
2. 末尾からのインデックス指定
末尾から数えた位置を指定することもできます。
index from end 演算子^
を使います。
一般的な「下◯桁目」の感覚で使うことができるので分かりやすいと思います。
^i
末尾からi番目の要素を抽出します。(= 下i桁目)
char char1 = str[^2]; // 下2桁目の要素 -> "4"
char char2 = str[^1]; // 下1桁目の要素 -> "5"
char char3 = str[^0]; // 下0桁目の要素? -> 実行時エラー
// System.IndexOutOfRangeException: 'Index was outside the bounds of the array.'
char3では実行時エラーが発生します。
配列の添え字は0から始まるのに、末尾から数えるときは0を指定できないの?と混乱する方がいるでしょうか。
ここで、先述した「混乱しないインデックスの解釈」をこの場合にも当てはめると下記のようになります。単純にインデックスの振り方が降順に変わっただけというイメージです。進行方向は通常通り先頭(左端)からとなります。
^0
の次(右隣)には要素が存在しないから[^0]
はエラーになる、と解釈すればスムーズだと思います。
3. 組み合わせ
- 先頭からのインデックス指定
- 末尾からのインデックス指定
- 範囲指定
これらは組み合わせて使うことができます。
string str7 = str[^4..^2]; // 降順index=4から、降順index=2まで -> "23"
string str8 = str[2..^1]; // 昇順index=2から、降順index=1まで -> "234"
string str9 = str[2..^0]; // 昇順index=2から、降順index=0まで -> "2345"
^0
は、単体指定ではエラーとなりますが、範囲の終了位置としては問題なく使えます。
使いどころの例
いろいろあるとは思いますが、私が経験した実務では…
- 複数の区分コードが複合したコードの特定桁を抽出して商材の種類を特定する
- 電文から特定桁の文字列を抽出してオブジェクトに値を設定する
といった用途でSubstringメソッドの代わりに使われていました。
注意点
- LINQ to Entities(EFCore)では文字列にインデクサーが使えない。
SubstringメソッドはSUBSTRING関数に変換されますが、インデクサーは非対応で、コンパイルエラーにはなりませんが実行時エラーとなりました。
DBMSによって異なったり、今後対応されるかもしれませんので、各自でご確認ください。
LINQ to Objects はC#プログラム内で完結する処理なので、問題なく文字列にインデクサーを使えます。
参考
インデックスと範囲(Microsoft)
インデクサー(Microsoft)
インデックス/範囲処理(未確認飛行Cさん)