Rubyにおいて一部のクラスのオブジェクトは、new
などを使用せずにリテラルとして直接記述できる。ただし条件式の文脈で書いた場合は、必ずしも通常のオブジェクト扱い(false
, nil
以外は真と評価)となるわけではないらしい。
もちろん、変数やメソッドなどを条件式にする分には問題無い。その結果がnilかどうかで条件分岐することはよく行われている。また、リテラルを条件式にすることはふつう無いので、動作を知らなくてもバグを生む可能性は少ない。この記事は注意書きではなく、単に個人的に驚いた仕様を少し触って調べたものである。
警告が出たりするリテラル
確認できた分だけ列挙。
正規表現
マニュアルのif文の説明にさらっと書いてある。組み込み変数 $_
(Kernel.#gets
や Kernel.#readline
で最後に読み込んだ文字列)とのマッチを評価するとのこと。
irb(main):001:0> puts 'true' if /regexp/
(irb):1: warning: regex literal in condition
=> nil
irb(main):002:0> gets # $_に値を入れるため
geregexpxe
=> "geregexpxe\n"
irb(main):003:0> puts 'true' if /regexp/
(irb):3: warning: regex literal in condition
true
=> nil
範囲式
通常は両端に比較可能なオブジェクトを指定して Range
オブジェクトを作るが、「条件式としての範囲式」では両端に条件式を指定する。
irb(main):001:0> puts 'true' if true..false
(irb):1: warning: range literal in condition
true
=> nil
基本的な使い方については、記事「Ruby でフリップフロップ」で詳しく説明されている。実用的なテキスト処理を例にしていて、前節の正規表現リテラルも使われている上に、AWKやPerlの場合も載っていて、これらの機能の価値が分かりやすい。
(文字列)
警告が出るだけで問題は無い?
irb(main):001:0> puts 'true' if 'string'
(irb):1: warning: string literal in condition
true
=> nil
(整数)
「条件式としての範囲式」の中で指定すると警告が出る。true
に判定されない理由がよく分からなかった。また、リテラルを括弧で囲むだけで警告はなくなり評価結果が変わる。
irb(main):001:0> puts 'true' if 0..9
(irb):1: warning: integer literal in conditional range
(irb):1: warning: integer literal in conditional range
=> nil
irb(main):002:0> puts 'true' if (0..9)
true
=> nil
条件式として扱われる場所
試した結果からの推測なので、抜け漏れや誤解があるかもしれない。
- if, while, 条件演算子など
- not
- 条件式中のandなど
- 条件式としての範囲式
irb(main):001:0> puts 'true' while /(?!)/
(irb):1: warning: regex literal in condition
=> nil
irb(main):002:0> puts !/regexp/
(irb):2: warning: regex literal in condition
true
=> nil
irb(main):003:0> puts /a/ && /b/ # これだと単なる正規表現オブジェクト同士の&&
(?-mix:b)
=> nil
irb(main):004:0> puts 'true' if /a/ && /b/ # これなら$_とのマッチ評価の論理積
(irb):4: warning: regex literal in condition
(irb):4: warning: regex literal in condition
=> nil
irb(main):005:0> puts 'true' if /a/../b/
(irb):5: warning: regex literal in condition
(irb):5: warning: regex literal in condition
=> nil
小技?
いま使いたいわけではないが、何かあったときに選択肢のひとつとなるかもしれないので、できそうなことを考えてみる。
評価結果の受け取り
すぐに条件分岐させるのではなく、評価結果を一旦変数に記憶したいことがあるはず。しかしリテラルを普通に代入すればただのオブジェクトになってしまう。
対策は簡単で、if文や条件演算子を使ってtrue/falseを返せば結果を受け取れるし、もっと短くnotを2回かけてもできる。
irb(main):001:0> result = true..false # これだと条件式にならない
ArgumentError: bad value for range
from (irb):1
from C:/Ruby/Ruby-2.1.1/bin/irb.bat:18:in `<main>'
irb(main):002:0> result = if true..false then true else false end
(irb):2: warning: range literal in condition
=> true
irb(main):003:0> result = true..false ? true : false
(irb):3: warning: range literal in condition
=> true
irb(main):004:0> result = !!(true..false)
(irb):4: warning: range literal in condition
=> true
ループ文の条件式で使用
通常はif文の条件式でしか使わなそうだが、whileなどのループ文でも何かできないかなと。
irb(main):001:0> gets; gets until /\Ayes\Z/ # yesと入力するまでループ
(irb):1: warning: regex literal in condition
no
no
yes
=> nil
条件式のネスト
範囲式や論理演算子は入れ子にすることができる。何か面白いことができそうな気はするのだが、評価順序に頭が追いつかなくて全く思いつかない。
irb(main):001:0> (1..10).each { |i| puts ((i%3==0)..(i%2==0))..((i%4==0)..(i%7==0)) ? -i : i }
1
2
-3
-4
-5
-6
7
8
-9
-10
=> 1..10
個々の評価順序と結果は以下の通り。
i | i%3==0 | i%2==0 | i%4==0 | i%7==0 | 結果 |
---|---|---|---|---|---|
1 | false | false | |||
2 | false | false | |||
3 | true | false | false | true | |
4 | true | false | true | ||
5 | false | false | true | ||
6 | true | false | true | ||
7 | false | false | |||
8 | false | false | |||
9 | true | false | false | true | |
10 | true | false | true |