倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

GDK のList がこんなに可愛いわけがない。 (List 編1) 要素の取得

f:id:waman:20130628013258p:plain

前回までで GDK が Object クラスや Collection クラスに追加しているメソッドを見てきました。 ただ、普段よく使う型はこれらというより今回から見ていく List でしょう。 Object や Collection に追加されていたメソッドのサンプルを書いているときも、基本 List オブジェクトを使ってました。 『プログラミングGROOVY』にチラッと書いてましたが、コレクションとしてのメソッドが Object クラスに定義されているのは通常のオブジェクトとコレクションを区別せずに使えると便利なためだそうで、まぁ言ってみればコレクションとしてのメソッドを便利に扱いたいってことの裏返しですね*1。 ということで、List インターフェースに追加されているメソッドは、基本的にインデックスを伴った操作や順序に関連する操作となっています。

ちなみにトップの画像は『俺妹ジェネレータ』より。 コレクションのときにやろうと思ったんだけど、「コレクション」や「Collection」が長すぎて拒否られたもんで・・・

目次
List 編の予定内容は次の通り:

順調にいけばだけど。 今回は要素の取得に関するメソッド。

Object getAt(int idx)
List getAt(EmptyRange range)
List getAt(Range range)
List getAt(Collection indices)

List withDefault(Closure init)
List withLazyDefault(Closure init)
List withEagerDefault(Closure init)
メソッド 返り値 since 説明
getAt(int) Object 1.0 引数のインデックスで指定された要素を取得。 []演算子を使える。
getAt(EmptyRange)
getAt(Range)
getAt(Collection)
List 1.0 引数のインデックスのコレクションで指定された要素をリストとして取得。 []演算子を使える。
wtihDefault(Closure)
withLazyDefault(Closure)
withEagerDefault(Closure)
List 1.8.7 指定されたインデックスがサイズを超えていたときの挙動を指定する

getAt() メソッド
getAt() メソッドは引数が int もしくはそのコレクションなら、それをインデックスとして指定された要素を返します。 インデックスがコレクションなら、返り値はリストになります(この辺り、型が適当な気も・・・)。 Groovy では getAt() メソッドは Java の配列で要素を取得する [] 演算子を使えます。 ちょっと慣れないとややこしいのは負のインデックスを指定した場合でしょうかね。 「-」をとって末尾から数えればいいだけですけどね。 これに範囲 (Range) が混ざってくると少し大変。 まぁ、とりあえず簡単なサンプル・コードを:

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby']
assert langs.size() == 6

// getAt(int)
assert langs[0] == 'Java'
assert langs[5] == 'JRuby'
assert langs[7] == null    // (サイズ-1)を超えたら null
assert langs[-1] == 'JRuby'    // 負のインデックスは末尾から
assert langs[-2] == 'Jython'

// getAt(EmptyRange)
assert langs[0..<0].isEmpty()    // 0..<0 は空

// getAt(Range)
assert langs[1..3] == ['Groovy', 'Scala', 'Clojure']    // 1, 2, 3番目
assert langs[3..<5] == ['Clojure', 'Jython']    // 3, 4番目。 5番目は入らない
assert langs[0..0] == ['Java']    // 0 だけからなる範囲ね
assert langs[2..-1] == ['Scala', 'Clojure', 'Jython', 'JRuby']    // 2番目から最後まで

// getAt(Collection)
assert langs[1, 5, 2] == ['Groovy', 'JRuby', 'Scala']    // 指定したインデックスの通りに要素を取得

インデックスが0~サイズ-1の範囲を超えた場合でも、例外が投げられません。 上記の例でちょっと分かりにくそうなのは getAt(Range) の最後のもので、インデックスとして「2..-1」を指定しているものでしょうか。 これは2番目から最後の要素までを取得したいといったときに使います。 まぁ、負の境界を持つ範囲はこの定型コード以外にあまり使わない方が身のため(みんなのため?)だと思いますが、一応、挙動としては以下のようになってます:

def ints = (0..5)

assert ints[3..<0]  == [3, 2, 1]
assert ints[3..0]   == [3, 2, 1, 0]
assert ints[3..<-1] == [3, 2, 1, 0]
assert ints[3..-1]  == [3, 4, 5]
assert ints[3..<-2] == [3, 4, 5]
assert ints[3..-2]  == [3, 4]

1~3番目のものは3から逆順に要素を返してますが、4~6番目は3から正順に要素を返してます。 違いは範囲に入っている整数を具体的に考えて、負の数が入っているなら正順に負のインデックスで指定される要素まで、0以上の整数しか含まれていないならそのインデックス群で指定される要素群を返す、といった挙動のようです。 まぁ、あんまり多用しない方がいいでしょう。

withDefault(), withLazyDefault(), withEagerDefault() メソッド
次は、要素取得の際にインデックスが範囲を超えていた場合の挙動を指定できるメソッド。 withDefault() は withLazyDefault() と全く同じ挙動だそうなので、以下では withLazyDefault() と withEagerDefault() を見ていきます。 これらはどちらも指定されたインデックスが範囲を超えていた場合に、そのインデックスの位置にクロージャを実行した結果がセットされてそれがメソッドの返り値として返されるのですが、違いはその要素までの位置の要素を後で初期化するか (lazy)すぐに初期化するか (eager) です。 値の計算にコストがかかる場合は withLazyDefault() を使いましょう。 実際に使ってみるとこんな感じ:

// withLazyDefault(Closure), withDefault(Closure)
def java = ['Java'].withLazyDefault{ i -> "Java$i" }
assert java[8] == 'Java8'
assert java.join(', ') == 'Java, null, null, null, null, null, null, null, Java8'

// withEagerDefault(Closure)
def groovy = ['Groovy'].withEagerDefault{ i -> "Groovy$i" }
assert groovy[2] == 'Groovy2'
assert groovy.join(', ') == 'Groovy, Groovy1, Groovy2'

初期化が行われるのは get(), getAt() を呼び出したときだけのようです。 えー、ちょっと注意が必要なのは withLazyDefault(), withEagerDefault() メソッドが呼び出されたリストは、範囲外の要素を取得しようとしても null を返すけど、返り値のリストが変更されると元のリストも同じように要素が変更されるトコロでしょうか。

def langs0 = ['Java']    // 元リスト
def langs1 = langs0.withLazyDefault{ "Java$it" }    // withLazyDefault() の返り値

// 返り値リストで要素取得
assert langs1[3] == 'Java3'
assert langs1.join(', ') == 'Java, null, null, Java3'
assert langs0.join(', ') == 'Java, null, null, Java3'    // 変わってる!

// 元のリストで要素取得
assert langs0[5] == null    // 取得できない
assert langs0.join(', ') ==  'Java, null, null, Java3'
assert langs1.join(', ') ==  'Java, null, null, Java3'

// 結局のところ
assert !langs0.is(langs1)
assert langs0 == langs1

ようは元リストと返り値リストは一緒に使わないようにしよう(というか返り値リストだけ使おう)ということで。 まぁ、言われなくてもそうするだろうけどw

次回は要素の追加・削除に関するメソッドを見ていく予定。

プログラミングGROOVY

プログラミングGROOVY

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

*1:なんか、前の記事で Object クラスにコレクション用っぽいメソッドがあれこれ定義されているのはコレクションと配列に共通したメソッドを一箇所に集めたい、みたいなことを書いた気もしますが・・・ まぁ、そういうのもあるんちゃう?