やってみる

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

「クラス/メソッドの定義」を読む2

 メソット内にメソッドを定義できる。新しい知識はそのくらいかな?

成果物

情報源

演算子式の定義

演算子式において、「再定義可能な演算子」に分類された演算子の実装はメソッドなので、定義することが可能です。

これらの演算子式を定義する例を以下に挙げます。

# 二項演算子
def +(other)                # obj + other
def -(other)                # obj - other

# 単項プラス/マイナス
def +@                      # +obj
def -@                      # -obj

# 要素代入
def foo=(value)             # obj.foo = value

# [] と []=
def [](key)                 # obj[key]
def []=(key, value)         # obj[key] = value
def []=(key, key2, value)   # obj[key, key2] = value

# バッククォート記法
def `(arg)                  # `arg` または %x(arg)

バッククォート記法の実装はメソッドなのでこのように再定義が可能です。普通はこのメソッドを再定義するべきではありませんが、まれにOS(シェル)のコマンド実行の挙動に不具合がある場合などに利用できます。

 演算子のオーバーライドはあんまり使わないと思う。使いやすいライブラリを作るときくらいかな?

メソッド定義のネスト

ネスト可能です。ネストされた定義式は、それを定義したメソッドが実行された時に定義されます。このことを除けば、普通のメソッド定義式と同じです。以下の例を参照してください。

class Foo
  def foo
    def bar
      p :bar
    end
  end

  def Foo.method_added(name)
    puts "method \"#{name}\" was added"
  end
end
obj = Foo.new
obj.bar rescue nil # => undefined method `bar' for #<Foo:0x4019eda4>
obj.foo            # => method "bar" was added
obj.foo            # => warning: method redefined; discarding old bar
Foo.new.bar        # => :bar  (他のインスタンスでも定義済み)

 名前重複を避けやすくなるので便利なときもありそう。

メソッドの評価

メソッドが呼び出されると、以下の順で式が評価されます。

  • 指定されていれば引数のデフォルト式
  • メソッドの本体 body
  • 指定されていれば例外の発生の有無によりメソッド定義式の rescue 節または else 節
  • 指定されていれば ensure 節
  • 引数のデフォルト式も含め、すべてそのメソッドのコンテキストで評価されます。

 はい。

メソッドの戻り値は return に渡した値です。return が呼び出されなかった場合は、 body の最後の式の値を返します。 body の最後の式が値を返さない式の場合は nil を返します。

 はい。

またメソッドは定義する前に呼び出すことはできません。例えば

foo          # <- foo は未定義
def foo
  print "foo\n"
end

は未定義メソッドの呼び出しで例外 NameError を発生させます。

 はい。わかりきったことばかり。

特異メソッド定義

例:

def foo.test
  print "this is foo\n"
end

文法:

def`.' 識別子 [`(' [引数 [`=' default]] ... [`,' `*' 引数 ]`)']
  式..
[rescue [error_type,..] [=> evar] [then]
  式..]..
[else
  式..]
[ensure
  式..]
end

特異メソッドとはクラスではなくある特定のオブジェクトに固有のメソッドです。特異メソッドの定義はネストできます。

 ネストしてどうするの? どういうときにネストしたくなるの? 想像できない。

クラスの特異メソッドはそのサブクラスにも継承されます。言い替えればクラスの特異メソッドは他のオブジェクト指向システムにおけるクラスメソッドの働きをすることになります。

 なぜ継承されることとクラスメソッドをつなげるのか。ぜんぜんちがくない?

特異メソッド定義式は、メソッド名を Symbol にしたオブジェクトを返します。

 はい。

 ancestorsによる継承リストの説明は一切なし。これじゃ特異クラスや特異メソッドが何者かよくわからない。オブジェクト固有メソッドなのにクラスメソッドだとも言われているから「は?」と思う。相変わらずよくわからん。

クラスメソッドの定義

Ruby におけるクラスメソッドとはクラスの特異メソッドのことです。Ruby では、クラスもオブジェクトなので、普通のオブジェクトと同様に特異メソッドを定義できます。

 やっと明言してくれた。Rubyにはクラスメソッドなんてない。ほかのプログラミング言語でいうところのクラスメソッドに相当するものは特異メソッドである。

したがって、何らかの方法でクラスオブジェクトにメソッドを定義すれば、それがクラスメソッドとなります。具体的には以下のようにして定義することが出来ます(モジュールも同様です)。

# 特異メソッド方式。
class Hoge
  def Hoge.foo
  end
end

# クラス定義の外でも良い
def Hoge.bar
end

# 以下のようにすればクラス名が変わってもメソッド部の変更が不要
class Hoge
  def self.baz
    'To infinity and beyond!'
  end
end

# 特異クラス方式。複数のメソッドを一度に定義するとき向き
class << Hoge
  def bar
    'bar'
  end
end

# モジュールをクラスに extend すれば、モジュールのインスタンス
# メソッドがクラスメソッドになる
module Foo
  def foo
  end
end
class Hoge
  extend Foo
end

extend については、Object#extend を参照してください。

 クラスメソッドを定義する方法が多すぎる。

 ところでクラスメソッドはクラスオブジェクトがもつ特異メソッド、ということだよね? じゃあインスタンスオブジェクトがもつ特異メソッドはなんて呼ぶの? そっちの呼び分けする名前がないから混乱するんだよね。だってクラスオブジェクトもインスタンスオブジェクトも両方オブジェクトでしょ? で、オブジェクト固有のメソッドのことを特異メソッドと呼ぶ。どっちも特異メソッドと呼ぶから見分けがつかない。でもクラスオブジェクト固有メソッドのほうだけはほかのプログラミング言語にもある「クラスメソッド」という呼び方をしている。だったらほかのプログラミング言語にはないインスタンスオブジェクト固有のメソッドにも名前を付けてほしい。

呼び方 意味
クラスメソッド(特異メソッド) クラスオブジェクト固有メソッド
?(特異メソッド) インスタンスオブジェクト固有メソッド

 インスタンスメソッドはダメ。すでにある。だれか短くて呼びやすい名前つけてよ。

呼び出し制限

メソッドは public、private、protected の三通りの呼び出し制限を持ちます。

 アクセス修飾子のことね。

  • public に設定されたメソッドは制限なしに呼び出せます。
  • private に設定されたメソッドは関数形式でしか呼び出せません。 ただし self.foo のように self. と書かれている場合は呼び出すことができます。
  • protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せ ます。

例: protected の可視性

class Foo
  def foo
   p caller.last
  end
  protected :foo
end

obj = Foo.new

# そのままでは呼べない
obj.foo rescue nil    # => -:11 - protected method `foo' called for #<Foo:0x401a1860> (NameError)

# クラス定義内でも呼べない
class Foo
  Foo.new.foo rescue nil # => -:15 - protected method `foo' called for #<Foo:0x4019eea8>
  # メソッド定義式内で呼べる
  def bar
    self.foo
  end
end
Foo.new.bar             # => ["-:21"]

# 特異メソッド定義式内でも呼べる
def obj.bar
  self.foo rescue nil
end
obj.bar                 # => ["-:27"]

 protectedは特異メソッド定義式内でも呼べるようだ。特異メソッドって継承とは違う気がするけど。まあとにかくそういうものとして覚えるしかない。

デフォルトでは def 式がクラス定義の外(トップレベル)にあれば private、クラス定義の中にあれば public に定義します。これは Module#public、Module#private、 Module#protected を用いて変更できます。ただし Object#initialize という名前のメソッドと Object#initialize_copy という名前のメソッドは定義する場所に関係なく常に private になります。

例:

def foo           # デフォルトは private
end

class C
  def bar         # デフォルトは public
  end

  def ok          # デフォルトは public
  end
  private :ok     # …だが、ここで private に変わる

  def initialize  # initialize は private
  end
end

 initializeprivate? newするときに呼ばれるよね? 直接initializeという名前のメソッドを呼べないという意味かな?

private と protected は同じ目的(そのメソッドを隠し外から呼べないようにする)で使用されますが、以下のような例では、private は使えず、protected を利用する必要があります。正確には、private には関数を定義する目的があるが、呼び出し制限の目的でも(ここに挙げた制限があるにもかかわらず) protected よりは private が使われることの方が多いようです。

class Foo
  def _val
    'val'
  end
  protected :_val

  def op(other)

    # other も Foo のインスタンスを想定
    # _val が private だと関数形式でしか呼べないため
    # このように利用できない

    self._val + other._val
  end
end

 protectedはあまり使わない。デザインパターン的には継承より集約がよい。

定義に関する操作

alias

例:

alias foo bar
alias :foo :bar
alias $MATCH $&

文法:

alias 新メソッド名 旧メソッド名
alias 新グローバル変数名 旧グローバル変数名

メソッドあるいはグローバル変数に別名をつけます。メソッド名には識別子そのものか リテラル/シンボル を指定します(obj.method のような式を書くことはできません)。alias の引数はメソッド呼び出し等の一切の評価は行われません。

 グローバル変数だけ? だったらalias $foo $barじゃないのかよ。ローカル変数で試してみよう。

a = 1
alias b a #=> undefined method `b' for class `Object' (NameError)

 怒られた。ローカル変数に対してもaliasできないっぽい。

 じゃあ次はグローバル変数でやってみる。

$a = 1
alias $b $a
p $a #=> 1
p $b #=> 1

 はいOK。ちなみに順序を間違えると全部nilになる。

$a = 1
p $a #=> 1
alias $a $b
p $a #=> nill
p $b #=> nill

 既存名から別名をつくるのだし、順序的には既存値が先かと思っちゃう。でも新しい名前から先に書くのが正しい。

メソッドの定義内で別名を付けるにはModuleクラスのメソッド Module#alias_method を利用して下さい。

 めんどうくさい。使い分けなきゃいけないのか。そうまでして別名定義したいときってあるかな?

別名を付けられたメソッドは、その時点でのメソッド定義を引き継ぎ、元のメソッドが再定義されても、再定義前の古いメソッドと同じ働きをします。あるメソッドの動作を変え、再定義するメソッドで元のメソッドの結果を利用したいときなどに利用されます。

# メソッド foo を定義
def foo
  "foo"
end

# 別名を設定(メソッド定義の待避)
alias :_orig_foo :foo

# foo を再定義(元の定義を利用)
def foo
  _orig_foo * 2
end

p foo  # => "foofoo"

 ダッグタイピング的には別名定義できたほうが嬉しいときもあるってことなのかな?

グローバル変数の alias を設定するとまったく同じ変数が定義されます。このことは一方の変数への代入は他方の変数にも反映されるようになることを意味します。

# 特殊な変数のエイリアスは一方の変更が他方に反映される
$_ = 1
alias $foo $_
$_ = 2
p [$foo, $_]   # => [2, 2]

$bar = 3
alias $foo $bar
$bar = 4
p [$foo, $bar] # => [4, 4]

 つまり実体は同じものを指すってことか。同一実体のポインタ変数を複数つくるのがaliasってことね。

ただし、正規表現の部分文字列に対応する変数 $1,$2, ... には別名を付けることができません。また、インタプリタに対して重要な意味のあるグローバル変数 (変数と定数 を参照)を再定義すると動作に支障を来す場合があります。

 ふーん。

alias 式は nil を返します。

 へー。

undef

例:

undef bar

文法:

undef メソッド名[, メソッド名[, ...]]

メソッドの定義を取り消します。メソッド名には識別子そのものか リテラル/シンボル を指定します(obj.method のような式を書くことはできません)。 undef の引数はメソッド呼び出し等の一切の評価は行われません。

 できるのはわかった。でも、いつやるの? なぜやりたいの? どんないいことがあるの? メモリが解放されるとか?

メソッドの定義内で定義を取り消すにはModuleクラスのメソッド Module#undef_method を利用して下さい。

 またかよ。使い分けなきゃいけないの面倒。

undef のより正確な動作は、メソッド名とメソッド定義との関係を取り除き、そのメソッド名を特殊な定義と関連づけます。この状態のメソッドの呼び出しはたとえスーパークラスに同名のメソッドがあっても例外 NameError を発生させます。 (一方、メソッド Module#remove_method は、関係を取り除くだけです。この違いは重要です)。

 え、undefremoveがあるの? それ使い分ける理由がどこにあるの? 例外が発生するかどうかの違いがあるように読み取れるけど。例外が発生しないremoveだとどうなっちゃうんだろう。

 Module#undef_methodのコード例に書いてあった。ちょっと変えて試してみると以下のようになった。

class A
  def ok; :A; end
end
class B < A
  def ok; :B; end
end
p B.new.ok   # => :B

# undef_method の場合はスーパークラスに同名のメソッドがあっても
# その呼び出しはエラーになる
class B
  undef_method :ok
end
#p B.new.okundefined method `ok' for #<B:0x01d2c2d8> (NoMethodError)

class C < A
  def ok
    puts 'C'
  end
end
# remove_method の場合はスーパークラスに同名のメソッドがあると
# それが呼ばれる
class C
  remove_method :ok
end
p C.new.ok   # => :A

 スーパークラスにあってもエラーになる。というか、スーパークラスの同名メソッドさえも未定義にしている? いや、ドキュメントの説明では「特殊な関係にしている」だったっけ。まあとにかくこうなる。

alias による別名定義と undef による定義取り消しによってクラスのインタフェースをスーパークラスと独立に変更することができます。ただし、メソッドが self にメッセージを送っている場合もあるので、よく注意しないと既存のメソッドが動作しなくなる可能性があります。

 元のメソッドをaliasで退避させておくってことかな? そんなことしてどうするんだって感じだが。ただただややこしくなるばかりでメリットがなさそう。

undef 式は nil を返します。

defined?

例:

defined? print
defined? File.print
defined?(foobar)
defined?($foobar)
defined?(@foobar)
defined?(Foobar)

文法:

defined?

式が定義されていなければ、偽を返します。定義されていれば式の種別を表す文字列を返します。

 末尾?のメソッドは真偽値を返すんじゃなかったんかーい。さっそく裏切られた。いや、たしかnilfalse以外はすべて真と判断するんだっけ? でもそれとこれとは別の話じゃないのか? そんなこといったらほぼすべてのメソッド末尾に?をつけるハメになりそうだし。

定義されていないメソッド、undef されたメソッド、Module#remove_method により削除されたメソッドのいずれに対しても defined? は偽を返します。

 はい。

特別な用法として以下があります。

defined? yield

yield の呼び出しが可能なら真(文字列 "yield")を返します。 Kernel.#block_given? と同様にメソッドがブロック付きで呼ばれたかを判断する方法になります。

 またブロックかよ。おまえのせいでムダに複雑になりつづけているな。

defined? super

super の実行が可能なら真(文字列 "super")を返します。

 トップレベルでやったらnilが返ったんですけど。たしかにnilfalseと判断するって聞いていたけども。偽のときですらfalseではないのか。もうわざとダイレクトな真偽値を避けていないか?

defined? a = 1
p a # => nil

"assignment" を返します。実際に代入は行いませんがローカル変数は定義されます。

 でたー。宣言だけされて代入されないパターン! 書いているコードとの差異があって罠になりうる。ていうか定義されているかどうかを返すんじゃないの? なんかもうメチャクチャだな。defined?の使いどころもわからないし、使い方もよくわからんし。覚えられなさそうだし。

/(.)/ =~ "foo"
defined? $&  # => "global-variable"
defined? $1  # => "global-variable"
defined? $2  # => nil

$&, $1, $2, などは直前のマッチの結果値が設定された場合だけ真を返します。

 そっすか。

def Foo(a,b)
end
p defined? Foo       # => nil
p defined? Foo()     # => "method"
Foo = 1
p defined? Foo       # => "constant"

大文字で始まるメソッド名に対しては () を明示しなければ定数の判定を行ってしまいます。

 前にもそんなこと言ってたよね? もうメソッド名は小文字でいいじゃんか。

以下は、defined? が返す値の一覧です。

"super"
"method"
"yield"
"self"
"nil"
"true"
"false"
"assignment"
"local-variable"
"local-variable(in-block)"
"global-variable"
"instance-variable"
"constant"
"class variable"
"expression"

 使いどころがわからん。実行時に定義されているかどうか判断できればいいって感じかな? エラーになるのを回避するif文を書いて。というかif文すら書いてない。そういうふうに書けるんだ?

対象環境

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