『GDK の List がこんなに可愛いわけがない。』の List 編1、List 編2で、GDK が List インターフェースに追加しているメソッド getAt(), putAt() を見てきました。 そのときに API を見たりサンプルコードを書いたりしていて思ったのですが、インデックスを指定して要素を取得もしくは設定する getAt() と putAt() は、指定できるインデックスについて対応関係がある(というか同じインデックスの型を指定できる)のが自然だと思うんですが、一部(半分)がちょっと指定できる型にズレがあって妙なことになっております。 その辺りについて考察というか、同じインデックスを指定した場合の挙動のズレみたいなの見ていきたいと思います。 普通に使う分には特に問題ないので重箱の隅を突つく感が否めませんが。

GDK が List インターフェースに追加している getAt(), putAt() メソッドには以下のようなシグニチャがあります:
// int Object getAt(int idx) void putAt(int idx, Object value) // EmptyRange List getAt(EmptyRange range) void putAt(EmptyRange range, Object value) void putAt(EmptyRange range, Collection value) // Range / IntRange List getAt(Range range) void putAt(IntRange range, Object value) void putAt(IntRange range, Collection col) // Collection / List List getAt(Collection indices) void putAt(List splice, Object value) void putAt(List splice, List values)
以下では putAt() の第2引数としては Object のものを扱います。
int 値のインデックス
int 値でインデックスを指定するものは問題ないでしょう。 負のインデックスは末尾から数えるとかありましたけど。
def list0 = 'abcd'.collect() assert list0[1] == 'b' list0[2] = 'x' assert list0 == 'abxd'.collect() // サイズ以上のインデックス def list1 = 'abcd'.collect() assert list1[6] == null list1[6] = 'x' assert list1 == ['a', 'b', 'c', 'd', null, null, 'x'] // 負のインデックス def list2 = 'abcd'.collect() assert list2[-1] == 'd' list2[-3] = 'x' assert list2 == 'axcd'.collect()
EmptyRange のインデックス
次の EmptyRange でインデックスを指定するものは、getAt() は空リストを返し、putAt() はその位置への挿入をするのでした。
// getAt(int) def list0 = 'abcd'.collect() assert list0[0..<0] == [] assert list0[1..<1] == [] // putAt(int, Object) list0[0..<0] = 'x' assert list0 == 'xabcd'.collect() list0[2..<2] = 'y' assert list0 == 'xaybcd'.collect()
同じ EmptyRange でも 0..<0, 2..<2 を指定した場合に putAt() の挙動が異なるのが違和感あるような・・・
Range / IntRange のインデックス
さて、この辺りからが getAt() と putAt() でインデックスの型が違ってるメソッド。 ちなみに Range はインターフェースで、IntRange は Range を実装したクラスです。 Range を実装したクラスは他に(groovy.lang パッケージ内で)EmptyRange, ObjectRange というのがあります:
package groovy.lang interface Range extends java.util.List class EmptyRange extends java.util.AbstractList implements Range class IntRange extends java.util.AbstractList implements Range class ObjectRange extends java.util.AbstractList implements Range
これらはコンストラクタで境界 (from, to)*1 の値を指定してインスタンス化します。 また、これらのクラスは不変、すなわち要素を変更することはできません*2。 これを踏まえて、まずは通常の使い方。 次のサンプルでは IntRange オブジェクト (0..2) でインデックスを指定しています。 IntRange は Range のサブクラスなので、これはまぁ意図された普通の使い方でしょう:
assert (0..2) instanceof IntRange def list0 = 'abcd'.collect() // getAt(Range) assert list0[0..2] == 'abc'.collect() // putAt(IntRange, Object) list0[0..2] = 'x' assert list0 == 'xd'.collect()
さて。 getAt() の引数は Range で putAt() の引数が IntRange なので、Integer を要素に持つ ObjectRange でインデックスを指定してやるとどうなるでしょう? 実際にやってみましょう:
def index = new ObjectRange(0, 2) assert index instanceof Range assert !(index instanceof IntRange) // IntRange オブジェクトではない // getAt(Range) が呼び出されている def list0 = 'abcd'.collect() assert list0[index] == 'abc'.collect() // putAt(List, Object) が呼び出されている! list0[index] = 'x' assert list0 == 'xxxd'.collect() // さっきは 'xd'.collect() になってた
getAt() の方はインデックスの型が Range なので IntRange でも ObjectRange でも挙動は同じでしょうね。 一方、putAt() はインデックスは IntRange 型を要求しているので ObjectRange を渡すとこのメソッドは呼ばれません。 代わりに(Range が List のサブタイプなので) putAt(List, Object) が呼ばれます。 で、putAt(IntRange, Object) はその範囲のサブリストを1つの(第2引数で指定された)オブジェクトに置き換え、putAt(List, Object) はリストで指定された位置の要素をそれぞれ(第2引数で指定された)オブジェクトに置き換えるので、結果が異なります。 今の場合は getAt() の方が正しそう。 putAt() の引数も Range にした方がよいかと*3。
Collection / List のインデックス
この型でインデックスを指定する場合、通常は List として指定する用途を想定してるんでしょう:
def list0 = 'abcd'.collect() // getAt(Collection) assert list0[1, 3] == 'bd'.collect() // putAt(List, Object) list0[1, 3] = 'x' assert list0 == 'axcx'.collect()
これはまぁ、そのままって感じですかね。 putAt() は前の節で出てきたヤツですが、これだけ書いてみると違和感はないかと。 さて、getAt() ではインデックスの型が List ではなく Collection になっているので、例えば Set でインデックスを指定してみましょう:
def list0 = 'abcd'.collect() def index = [1, 3] as HashSet assert list0[index].toSet() == 'bd'.collect().toSet()
これはそのままの挙動といえばそのままなんですが、インデックスを Set で指定したら返り値も List ではなく Set で返して欲しい気がするところ。 ただ、これは Groovy 的には実装が難しいんでしょうね。 そういうメソッドはジェネリクスを使って定義しないといけないけど、Groovy は基本的にジェネリクス無視だし、何より Scala などと違って、List や Set などのインターフェースにデフォルトの実装が伴っていないので、引数の型だけからそれにあったコレクションのインスタンスを作るのが難しい(ユーザー定義のコレクションとか出てきたら基本無理)のが原因でしょう。 今の場合は getAt() のインデックスの型が List でいいんじゃないかと。 ちなみに当然のことながら、putAt() に Set のインスタンスをインデックスとして渡すと MissingMethodException が投げられます。
今回は List に定義されている GDK のメソッド getAt(), putAt() でインデックスの型にズレがあるのを見てきました。 インデックスを範囲のリテラル(1..3 のようなもの)で指定してる分(文)には特に問題がありませんが、シグニチャを見てると違和感が拭えないところ。 このズレが意図的なのか特に気にしてないのかよく分かりませんが、ちょっとコード書いた人を絞めたくなる気分w とは言え、古くからあるメソッドなのでそんなに問題は起きなてないんでしょう。 まぁ、自分が使う分には変な使い方をしないようにしておくのが無難なんでしょうけどね。

- 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
- 出版社/メーカー: 技術評論社
- 発売日: 2011/07/06
- メディア: 単行本(ソフトカバー)
- 購入: 6人 クリック: 392回
- この商品を含むブログ (152件) を見る

- 作者: 西尾維新
- 出版社/メーカー: 講談社
- 発売日: 2008/06/13
- メディア: 文庫
- 購入: 9人 クリック: 125回
- この商品を含むブログ (108件) を見る