やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「変数と定数」を読む

 定数が思った以上に面倒くさい。

成果物

情報源

変数と定数

Ruby の変数と定数の種別は変数名の最初の一文字によって、ローカル変数、インスタンス変数、クラス変数、グローバル変数、定数のいずれかに区別されます。通常の変数の二文字目以降は英数字または _ですが、組み込み変数の一部には「`$'+1文字の記号」という変数があります(組み込み変数を参照)。変数名の長さにはメモリのサイズ以外の制限はありません。

接頭辞 種別
$ グローバル変数
@ インスタンス変数
@@ クラス変数
_[1-9] 番号指定パラメータ
[A-Z] 定数
[_a-z] ローカル変数

ローカル変数

例:

foobar

小文字または`_'で始まる識別子はローカル変数またはメソッド呼び出しです。ローカル変数スコープ(クラス、モジュール、メソッド定義の本体)における小文字で始まる識別子への最初の代入はそのスコープに属するローカル変数の宣言になります。宣言されていない識別子の参照は引数の無いメソッド呼び出しとみなされます。

 ローカル変数の説明なはずなのに、メソッド呼び出しと絡めて話されると混乱する。おそらく以下のようなエラーが出ることを指しているのだろう。

undefined local variable or method `b' for main:Object (NameError)

ローカル変数のスコープは、宣言した位置からその変数が宣言されたブロック、メソッド定義、またはクラス/モジュール定義の終りまでです。寿命もそのブロックの終りまで(トップレベルのローカル変数はプログラムの終了まで)ですが、例外としてブロックが手続きオブジェクト化された場合は、そのオブジェクトが消滅するまで存在します。同じスコープを参照する手続きオブジェクト間ではローカル変数は共有されます。

 スコープとライフサイクルについて。まとめると以下。

定義場所 スコープ ライフサイクル
ブロック、メソッド、クラス、モジュール定義 そのブロック終了まで そのブロック終了まで
トップレベ プログラムの終了まで
proc,lambda そのオブジェクトが消滅するまで
# (A) の部分はスコープに入らない
2.times {
  p defined?(v)    # (A)
  v = 1            # ここ(宣言開始)から
  p v              # ここ(ブロックの終り)までが v のスコープ
}

# => nil
     1
     nil           <- これが nil であることに注意
     1

 「<- これが nil であることに注意」とあるので、そこを中心に読み取ってみよう。

 1回目のループでv = 1が宣言されたにもかかわらず、2回目ループの先頭p defined?(v)ではvnilであり定義されていないことになっている。それはvがループごとのブロックに所属したローカル変数であるから。という話かな?

宣言は、たとえ実行されなくても宣言とみなされます。

v = 1 if false # 代入は行われないが宣言は有効
p defined?(v)  # => "local-variable"
p v            # => nil

 これは罠になりえる、のか? ようするにvが宣言されずに参照されたら以下のようにエラーになるはず。でもそうならずnilになる。つまり変数vは宣言(定義)されるが、初期化(代入)はされない。という話か。

undefined local variable or method `v' for main:Object (NameError)

 そんな紛らわしいコードが書けてしまうと。

 ていうか、変数って初期化せずに宣言することは普通できなかったと思うけど、上記のコードでそれができるってことか。そして、上記コードで初期化せず宣言された変数はその値がnilになると。0や空文字ではなくnilであると。

インスタンス変数

@foobar

`@'で始まる変数はインスタンス変数であり、特定のオブジェクトに所属しています。インスタンス変数はそのクラスまたはサブクラスのメソッドから参照できます。初期化されていないインスタンス変数を参照した時の値はnilです。

 初期化されていないインスタンス変数を参照してもエラーにならないのが怖い。ちゃんとテストしないとバグる。

クラス変数

class Foo
  @@foo = 1
  def bar
    puts @@foo
  end
end

@@で始まる変数はクラス変数です。クラス変数はクラス定義の中で定義され、クラスの特異メソッド、インスタンスメソッドなどから参照/代入ができます。

クラス変数と定数の違いは以下の通りです。

  • 再代入可能(定数は警告を出す)
  • クラスの外から直接参照できない(継承されたクラスからは参照/代入可能)

クラス変数はクラス自身のインスタンス変数とは以下の点で異なります。

  • サブクラスから参照/代入が可能
  • インスタンスメソッドから参照/代入が可能

クラス変数は、そのクラスやサブクラス、それらのインスタンスで共有されるグローバル変数であるとみなすことができます。

class Foo
  @@foo = 1
end
class Bar < Foo
  p @@foo += 1          # => 2
end
class Baz < Bar
  p @@foo += 1          # => 3
end

モジュールで定義されたクラス変数(モジュール変数)は、そのモジュールをインクルードしたクラス間でも共有されます。

module Foo
  @@foo = 1
end
class Bar
  include Foo
  p @@foo += 1          # => 2
end
class Baz
  include Foo
  p @@foo += 1          # => 3
end

親クラスに、子クラスで既に定義されている同名のクラス変数を追加した場合には、子クラスのクラス変数が上書きされます。

class Foo
end

class Bar < Foo
  @@v = :bar
end

class Foo
  @@v = :foo
end

class Bar
  p @@v       #=> :foo
end

 これは危険。クラス定義を変更できてしまうRubyだからこそのバグ作り込み。

 クラス変数の使いどころがわからない。アクセス修飾子的にみればprotectedのようだ。おそらく特異メソッドからも参照できるという点がポイント。インスタンス変数よりも参照できる範囲が広い。ただ、そうして広げることの意義がわからない。include, prepend, extendしたModuleのメソッドからも参照できるということか? 仮にそうだとしても、同名のクラス変数をもったクラスにしかinclude, prepend, extendできないModuleになってしまう。微妙では?

クラス変数のスコープ

クラス変数は、その場所を囲むもっとも内側の(特異クラスでない) class 式または module 式のボディをスコープとして持ちます。

class Foo
  @@a = :a
  class << Foo
    p @@a       #=> :a
  end

  def Foo.a1
    p @@a
  end
end

Foo.a1          #=> :a

def Foo.a2
  p @@a
end
Foo.a2          #=> NameError になります。

class << Foo
  p @@a         #=> NameError になります。
end

 irbで動かしたらNameErrorでなくRuntimeErrorになったんだけど?

class variable access from toplevel (RuntimeError)

グローバル変数

$foobar
$/

`$'で始まる変数はグローバル変数で、プログラムのどこからでも参照できます(その分、利用には注意が必要です)。グローバル変数には宣言は必要ありません。初期化されていないグローバル変数を参照した時の値はnilです。

 スパゲッティコードにしたくないならできるだけ使わないほうがいい。でもコードの規模が小さくて簡単な処理のときは使ってもいい。

組み込み変数

グローバル変数には Ruby 処理系によって特殊な意味を与えられているものがあります。これらを組み込み変数と呼びます。

詳細は Kernel の特殊変数を参照してください。

識別子と分類

組み込み変数の一部は、通常の変数としては使用できない特殊な名前を持っています。

例えば、 $' や $& あるいは $1, $2, $3 がそうです。このように 「'$' + 特殊文字一文字」、または「'$' + 10進数字」という名前を持つ変数を特殊変数と呼びます。

 さっき言ってたKernel の特殊変数ってやつね。

また、 $-F や $-I のような変数もあります。これらは Ruby の起動オプションと -F や -I などと対応しており、オプション変数と呼ばれます。

 めんどくせ。もうグローバル変数とか使いたくないや。まあそんな短い名前のグローバル変数なんて使ったら可読性が低くて読めたもんじゃないから、予約されていなくとも使うべきじゃないと思うけど。

スコープ

組み込み変数は文法解析上はグローバル変数として扱われます。しかし、実際のスコープは必ずしもグローバルとは限りません。

 え、グローバル変数ってまだルールがあるの?

組み込み変数には次の種類のスコープがありえます。

 グローバル変数なのにグローバルでないスコープとは、これいかに。それもうグローバル変数じゃないやん。スコープが全域(グローバル)だからグローバル変数なのに。名前だけ使った別物ってことかよ。

グローバルスコープ

通常のグローバル変数と同じ大域的なスコープを持ちます。

ローカルスコープ

通常のローカル変数と同じスコープを持ちます。つまり、 class 式本体やメソッド本体で行われた代入はその外側には影響しません。プログラム内のすべての場所において代入を行わずともアクセスできることを除いて、通常のローカル変数と同じです。

 具体的にどれがそれなんだろう。何も書いてない。

スレッドローカルスコープ

スレッドごとの値を持ちます。他のスレッドで異なる値が割り当てられても影響しません。

また、一部の変数は読み取り専用です。ユーザープログラムから変更することができません。代入しようとすると実行時に例外を生じます。

 スレッド用のグローバル変数()があるってこと?

 雰囲気からして、なんか便利な変数をまとめてグローバル変数って呼んでいる感じだな。

疑似変数

通常の変数以外に疑似変数と呼ばれる特殊な変数があります。

疑似変数 説明
self 現在のメソッドの実行主体。
nil NilClassクラスの唯一のインスタンス。 Object#frozen? は true を返します。
true TrueClassクラスの唯一のインスタンス。真の代表値。 Object#frozen? は true を返します。
false FalseClassクラスの唯一のインスタンスnilとfalseは偽を表します。 Object#frozen? は true を返します。
__FILE__ 現在のソースファイル名。フルパスとは限らないため、フルパスが必要な場合は File.expand_path(FILE) とする必要があります。
__LINE__ 現在のソースファイル中の行番号
__ENCODING__ 現在のソースファイルのスクリプトエンコーディング

疑似変数の値を変更することはできません。擬似変数へ代入すると文法エラーになります。

 truefalseって定数じゃないのか。疑似変数ってやつなんだね。ふーん。実行時にインスタンス生成することで値が定まるから変数ってことかな? まあどうでもいいか。

 「擬似変数へ代入すると文法エラーになります」って、皮肉にも定数より定数っぽいぞ。定数は警告だけで代入できちゃうけど、疑似変数はエラーになる。

true = 1 #=> Can't assign to true (SyntaxError)

 なんかもう、名前と実体と挙動がチグハグな印象。

定数

FOOBAR
FOOBAR

アルファベット大文字 ([A-Z]) で始まる識別子は定数です。他にも、ソースエンコーディングUnicode の時は Unicode の大文字またはタイトルケース文字から始まる識別子も定数です。 Unicode 以外の時は小文字に変換できる文字から始まる識別子が定数です。定数の定義 (と初期化) は代入によって行われますが、メソッドの中では定義できません。一度定義された定数に再び代入を行おうとすると警告メッセージが出ます。定義されていない定数にアクセスすると例外 NameError が発生します。

 ええ、全角アルファベットが使えるのかよ。なんて紛らわしい。いいよ使えなくて。というか漢字やひらがなも使えそうだね。

山 = 11
p 山
=> 11

 でも定数にするには先頭1文字目が全角大文字アルファベットにせねばならないってことね。

 でもさあ、クラス名は1文字目が半角大文字の英字だけしか使えないんだよね。

classend
class/module name must be CONSTANT (SyntaxError)

 だったら日本語でプログラミングすることは難しそう。変数だけユニコード文字を使ってもキモチワルイだけだし。どうにも中途半端だな。

定数はその定数が定義されたクラス/モジュール定義の中(メソッド本体やネストしたクラス/モジュール定義中を含みます)、クラスを継承しているクラス、モジュールをインクルードしているクラスまたはモジュールから参照することができます。クラス定義の外(トップレベル)で定義された定数は Object に所属することになります。

class Foo
  FOO = 'FOO'       # クラス Foo の定数 FOO を定義(Foo::FOO)
end

class Bar < Foo
  BAR = 'BAR'       # クラス Bar の定数 BAR を定義(Bar::BAR)

  # 親クラスの定数は直接参照できる
  p FOO             # => "FOO"
  class Baz

    # ネストしたクラスはクラスの継承関係上は無関係であるがネス
    # トの外側の定数も直接参照できる
    p BAR           # => "BAR"
  end
end

 トップレベル定数はObjectに所属しちゃうのか。たしかすべてのクラスはオブジェクトを継承しているから、そのクラスからもトップレベル定数が参照できるってわけね。

またクラス定義式はクラスオブジェクトの生成を行うと同時に、名前がクラス名である定数にクラスオブジェクトを代入する動作をします。クラス名を参照することは文法上は定数を参照していることになります。

class C
end
p C    # => C

 マジか。クラス名って定数だったのか。どうりで定数と同じく先頭1文字目が大文字縛りなわけだ。

あるクラスまたはモジュールで定義された定数を外部から参照するためには::'演算子を用います。またObjectクラスで定義されている定数(トップレベルの定数と言う)を確実に参照するためには左辺無しの::'演算子が使えます。

module M
  I = 35
  class C
  end
end
p M::I   #=> 35
p M::C   #=> M::C
p ::M    #=> M

M::NewConst = 777   # => 777

 なるほど。トップレベル定数は接頭辞::をつけたらフルパス参照になって確実ってことか。

定数参照の優先順位

親クラスとネストの外側のクラスで同名の定数が定義されているとネストの外側の定数の方を先に参照します。つまり、定数参照時の定数の探索順序は、最初にネスト関係を外側に向かって探索し、次に継承関係を上位に向かって探索します。

class Foo
  CONST = 'Foo'
end

class Bar
  CONST = 'Bar'
  class Baz < Foo
    p CONST             # => "Bar"      外側の定数
    # この場合、親クラスの定数は明示的に指定しなければ見えない
    p Foo::CONST        # => "Foo"
  end
end

 定数に関しては継承関係よりもネスト構造を優先するってことか。紛らわしいな。インスタンス変数やクラス変数については継承関係に従うはず。定数の参照は変数と異なるってことね。罠になりかねない。

 定数は罠が多いな。警告だけで再代入できてしまうし。

トップレベルの定数定義はネストの外側とはみなされません。したがってトップレベルの定数は、継承関係を探索した結果で参照されるので優先順位は低いと言えます。

class Foo
  CONST = 'Foo'
end

CONST = 'Object'

class Bar < Foo
  p CONST               # => "Foo"
end

# 以下のように明示的にネストしていれば規則通り Object の定数
# (ネストの外側)が先に探索される
class Object
  class Bar < Foo
    p CONST             # => "Object"
  end
end

 絶対にObjectクラスでネストなんてやらないと思う。いろんな意味で超わかりにくいわ。

上位のクラス(クラスの継承関係上、およびネストの関係上の上位クラス)の定数と同名の定数(下の例で CONST) に代入を行うと、上位の定数への代入ではなく、そのクラスの定数の定義になります。

class Foo
  CONST = 'Foo'
end
class Bar < Foo
  p CONST               # => "Foo"
  CONST = 'Bar'         # Bar の定数 CONST を*定義*
  p CONST               # => "Bar"  (Foo::CONST は隠蔽される)
  p Foo::CONST          # => "Foo"  (:: 演算子で明示すれば見える)
end

 まあ、そうじゃないと困るよね。自分のクラスで同名の定数が定義できないことになってしまうから。

 ここで注意すべきことは参照の仕方だろう。フルパスで指定したほうが間違いが起きにくいよって話だと思う。

所感

 定数が思った以上に面倒くさい。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux