LoginSignup
13
8

C#8.0で導入された配列インデクサの範囲指定がexclusive endな理由

Last updated at Posted at 2023-08-30

C#8.0で、次のように配列の範囲指定での抽出が可能になりました。

string line = "01234567";
string line2 = line[0..4];

Console.WriteLine(line);	// 01234567
Console.WriteLine(line2);	// 0123

この記事がとても分かりやすいです。

array[start..end] の時、startは「包括的(inclusive)」、endは「排他的(exclusive)」となります。
つまり、startは結果に含まれますが、endは結果に含まれません。

"01234567"という文字列に対して[2..4]は"23"です。

どうしてそうなっているのでしょうか?

分かりにくいと思う人もいるかもしれませんが、CやC++と同様、C#の「配列のインデックス」というのは「N番目の要素」を表しているというよりは「配列として確保されたメモリ上のデータの先頭からの距離」を表しているわけで、0から開始する以上、「範囲」を表す為にはこの方が都合がいいから、ということでしょう。

同じように「0から始まる数値の範囲」を表す例として、「時刻」があります。
「10時から17時まで」と聞いた時に、「17:59までだろう」と思う人はほとんどおらず、基本的には「17:00」まで、つまり「17:00にはイベントは終わった後」であると考えるでしょう。また、開始時刻についても、「10:01」からだと思う人はまずおらず、「10:00」から開始、つまり0ベースだと考えるはずです。

また、こうすることで、「10時から17時まで」と「17時から21時まで」といった複数の範囲が切れ目なく連続することも表現しやすくなります。

この例でもわかるように、0ベースで考える場合、その範囲については、startは「包括的(inclusive)」、endは「排他的(exclusive)と考えた方が直感的で分かりやすいのです。

例えば、「範囲0」を表したい場合に、この仕様だとこう書けます。

string line = "01234567";
string empty_string = line[2..2]; // "" が返る

もしこれを、次のように書かないといけないとしたら、むしろ直感的ではないと感じるでしょう。

ArgumentOutOfRangeExceptionが発生
string line = "01234567";
string empty_string = line[2..1]; // "" が返る…? 

次に、「文字列を3文字ずつ切り出す」ような処理を考えてみましょう。

string line = "01234567";
int split_length = 3;
for (int start = 0; start < line.Length; start += split_length )
{
	int end = Math.Min(start + split_length, line.Length);	// end は最大長まで
	string sub = line[start..end];
	Console.WriteLine(sub);
	// 012
	// 345
	// 67
}

開始位置startに、切り出す長さsplit_lengthを足していくだけです。
とても簡潔に書けますね。
「どこまで処理するか?」も、line.Lengthまでやるだけです。長さに-1したりするようなことを考える必要はありません。

また、インデクサの指定方法には、新しく^n(ハット演算子)も加わりました。

string line = "01234567";
char c = line[^1]; // "7"

つまり、ハットには位置ではなく末尾からの長さを指定します。これを見て「どうしてここは0ベースの位置指定じゃないのか?」と思われるかもしれません。

しかし、こうなっていることにより、末尾から3文字欲しい場合には次のように書けます。

string line = "01234567";
string line2 = line[^3..]; // "567"

また、「先頭5文字とそれ以降」に切り分けたい場合、次のように書けます。

string line = "01234567";
int split_length = 5;
string lineA = line[..split_length]; // "01234"
string lineB = line[split_length..]; // "567"

そして「末尾5文字とそれ以前」に切り分けたい場合、次のように書けます。

string line = "01234567";
int split_length = 5;
string lineA = line[..^split_length]; // "012"
string lineB = line[^split_length..]; // "34567"

長々と書きましたが、つまるところ、「そのほうが範囲を表現しやすい」ということでした。

参考

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8