この記事はお気持ち表明です。
取り上げたいこと
文字列内の文字や配列の要素を指定するために使用する「インデックス」を、「文字(要素)の位置」と考えるのをやめたらどうか、という話。
単一要素を示す場合のインデックスの考え方
一般的には、以下のように「インデックス」は「文字(要素)の位置」と説明される。
文字列
String str = "にわとりがにわ";
インデックス | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
文字 | に | わ | と | り | が | に | わ |
配列
String[] ary = {"オレンジ", "りんご", "いちご"};
インデックス | 0 | 1 | 2 |
---|---|---|---|
要素 | オレンジ | りんご | いちご |
単一の文字(要素)を示す場合、この考え方で難なく理解できる。
まったくおかしなところはない。
範囲を説明する場合のインデックス(開始・終了位置)の考え方
これは、APIの設計指針に関する内容となる。
範囲を指定させるAPIの場合、以下の2つのパターンをよく見る
- 開始位置と終了位置を指定する
- JavaのString.substringやPythonなどのsliceなど
- 開始位置と要素数を指定する
- C#のString.IndexOfなど
特に前者「開始位置と終了位置を指定する」において、指定されたインデックス(が指す要素)を含むのか含まないのか、開始位置と終了位置とで異なる。
たいていは、開始位置は含み終了位置は含まない。
しかし、両方「含む」とするAPIもある(あったと思う)。
そのため、範囲を示す場合には、「インデックス=要素」という考え方だと、「含む含まない」も考えなければいけないので、若干わかりづらいと思う。
開始位置と終了位置を指定する
たとえばJavaのString#substring
開始位置は、含む。
終了位置は、含まない。
Javadocにもわざわざそう書いてある。
String str = "にわとりがにわ";
System.out.println(str.substring(1, 4)); // わとり
インデックス | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
文字 | に | わ | と | り | が | に | わ |
開始位置と要素数を指定する
たとえば C#のStirng#IndexOf
開始位置は、含む。
文字数は、含む含まないは関係ない。
(含む含まないで悩む必要がないので、開始位置と終了位置を指定するよりも、わかりやすいと思っている)
// String.IndexOf(string value, int startIndex, int count)
var str = "にわとりがにわ";
Console.WriteLine(str.IndexOf("にわ", 1, 3)); // -1
インデックス | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
文字 | に | わ | と | り | が | に | わ |
文字数 | 1 | 2 | 3 |
持論: 「インデックス=要素の隙間」
// 再掲
String str = "にわとりがにわ";
System.out.println(str.substring(1, 4)); // わとり
インデックス | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
文字 | に | わ | と | り | が | に | わ |
インデックスとは、要素の位置を示すのではなく、要素の隙間(開始位置)を示すと考える。
インデックスをこのように捉えていると、substring に指定する「1から4」とは、1~4の間の要素(わとり)となり、含む含まないを考慮しなくてよくなり、スッキリ。
と常々思っているのだけど、こういう説明をあまり見たことがない。
要素1つを指す場合(array[0])は、便宜上、終了位置が省略されているのであり、開始位置から1つ分の内容を指していると考えられるので矛盾はない。
余談
記事を書くに至った動機は、C#のString.LastIndexOf
これは、持論では説明できず、「要素位置=インデックス」でないと解釈できないAPI。
// String.LastIndexOf(string value, int startIndex, int count)
// value: 見つけたい語
// startIndex: 開始位置
// count: 文字数
var str = "にわとりがにわ";
Console.WriteLine(str.LastIndexOf("にわ", 5, 5)); // -1
LastIndexOfは「開始位置」の文字を含んで、先頭方向に「文字数」分を検索対象にする。
位置5の「に」を含みつつ5文字戻って位置2の「わ」まで(わとりがに)が検索範囲となり、「にわ」は存在しないことになる。
インデックス | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
文字 | に | わ | と | り | が | に | わ |
開始位置は「含まれる」のが一般的な考え方なので全然おかしくはないのだが、
↓のような挙動を期待していて、つまづいた。
インデックス | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
文字 | に | わ | と | り | が | に | わ |