「実際にオブジェクトをいくつか定義してみる」というパートまで至りました。
コンストラクタ
リテラルコンストラクタ
Rubyでは、文字列(String)・配列(Array)・ハッシュ(Hash)・範囲(Range)などのクラスに対し、当該オブジェクトのインスタンスを暗黙的に作成する記法が存在します。Railsチュートリアルでは、こうした記法を「リテラルコンストラクタ」と呼んでいます。以下はリテラルコンストラクタの例です。
# 文字列のリテラルコンストラクタ
>> s = "foobar"
=> "foobar"
>> s.class
=> String
# 配列のリテラルコンストラクタ
>> ar = ['foo', 'bar']
=> ["foo", "bar"]
>> ar.class
=> Array
%記法も、Railsチュートリアルでいうところのリテラルコンストラクタの一種です。
# %qによる文字列のリテラルコンストラクタ
>> s = %q[foo bar'baz']
=> "foo bar'baz'"
>> s.class
=> String
# %wによる配列のリテラルコンストラクタ
>> ar = %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> ar.class
=> Array
名前付きコンストラクタ
暗黙のリテラルコンストラクタが存在する一方、明示的に同等の名前付きコンストラクタを呼び出して使うことも可能です。名前付きコンストラクタは、クラス名に対してnew
メソッド1を呼び出すことにより実行されます。
# 文字列の名前付きコンストラクタ
>> s = String.new("foobar")
=> "foobar"
>> s.class
=> String
# 配列の名前付きコンストラクタ
>> ar = Array.new(['foo', 'bar'])
=> ["foo", "bar"]
>> ar.class
=> Array
ハッシュの名前付きコンストラクタ
ハッシュの名前付きコンストラクタHash.new
の引数は、ハッシュの内容そのものではなく、ハッシュのデフォルト値(キーが存在しない場合の値)を与えるために用いられます。Hash.new
の引数がない場合、ハッシュのデフォルト値はnil
となります。
>> h = Hash.new()
=> {}
>> h[:foo] # 存在しないキー(:foo)の値を取り出してみる
=> nil
>> h
=> {} # 存在しないキー(:foo)の値を取り出しても、ハッシュの中身に変化はない
>> h = Hash.new(0) # 存在しないキーのデフォルト値を0としてハッシュを作成する
=> {}
>> h[:foo] # 存在しないキー(:foo)の値を取り出してみる
=> 0
>> h
=> {} # 存在しないキー(:foo)の値を取り出しても、ハッシュの中身に変化はない
ハッシュの名前付きコンストラクタにブロックを与えることにより、「存在しないキーの値を取り出すと、指定したキーに対する値を自動でセットする」という動作を定義することも可能です。
>> h = Hash.new(){|h,k| h[k] = 0}
=> {}
>> h[:foo] # 存在しないキー(:foo)の値を取り出してみる
=> 0
>> h
=> {:foo=>0} # :fooというキーに対し、0という値がセットされている
演習 - コンストラクタ
1. 1から10の範囲オブジェクトを生成するリテラルコンストラクタは何でしたか? (復習です)
>> 1..10
=> 1..10
2. 今度はRangeクラスとnewメソッドを使って、1から10の範囲オブジェクトを作ってみてください
>> r = Range.new(1,10)
=> 1..10
>> r.class
=> Range
3. 比較演算子==を使って、上記2つの課題で作ったそれぞれのオブジェクトが同じであることを確認してみてください。
>> rr = 1..10
=> 1..10
>> rr.class
=> Range
>> r == rr
=> true
クラスメソッドとインスタンスメソッド
メソッドがクラス自身に対して呼び出されるとき、当該メソッドをクラスメソッド2といいます。new
メソッドはクラスメソッドの例です。
クラスのnew
メソッドを呼び出した場合、その戻り値は、当該クラスの個別オブジェクトとなります。クラスに属する個別オブジェクトのことをインスタンスと呼びます。
length
やequal?
といった、インスタンスに対して呼び出すメソッドはインスタンスメソッドと呼ばれます。
クラス継承
Class#superclass
メソッドにより、オブジェクトのクラス階層を調べていくことができます。
>> s = String.new("foobar")
=> "foobar"
>> s.class.superclass
=> Object
>> s.class.superclass.superclass
=> BasicObject
>> s.class.superclass.superclass.superclass
=> nil
Rubyにおける全てのクラスは、BasicObject
クラスを継承しています。また、ほとんどのクラスは、BasicObject
を基底クラスとするObject
クラスを継承しています。
とにかくクラスを作ってみる
ある単語を前からと後ろからのどちらから読んでも同じ (つまり回文になっている) ならばtrue
を返すpalindrome?
メソッドを含む、Word
というクラスを作ってみます。
>> class Word
>> def palindrome?(string)
>> string == string.reverse
>> end
>> end
=> :palindrome?
このクラスは、以下のように使うことができます。
>> w = Word.new # Wordオブジェクトを作成する
=> #<Word:0x000055e4debd1320>
>> w.palindrome?("foobar")
=> false
>> w.palindrome?("level")
=> true
>> w.class.superclass
=> Object
Word
クラスがObject
クラスを直接継承していることは注目に値します。Rubyのクラス定義においては、基底クラスを明示しなければ、暗黙的にObject
クラスを継承します。
既存クラスから継承したクラスを作る
…ちょっと待ってください。文字列を引数に取るメソッドを作るためだけに、Object
クラスを直接継承する新しいクラスを定義するのもおかしな話ですよね。単語は文字列なのだから、Word
クラスはString
クラスを継承して作られるべきではないでしょうか…
というわけで、今度はString
クラスを継承したWord
クラスを定義します。
>> class Word < String
>> def palindrome?
>> self == self.reverse
>> end
>> end
=> :palindrome?
このクラスは、以下のように使うことができます。
>> s = Word.new("level") # 新しいWordを作成し、"level"で初期化する
=> "level"
>> s.palindrome? # Wordが回文かどうか調べる
=> true
>> s.length # WordはStringで使える全てのクラスを継承している
=> 5
Word
クラスの継承階層を調べてみましょう。
>> s.class.superclass
=> String
>> s.class.superclass.superclass
=> Object
UMLのクラス図は以下のようになります。
演習 - クラス継承
1.1. Range
クラスの継承階層を調べてみてください。
>> Range.superclass
=> Object
>> Range.superclass.superclass
=> BasicObject
>> Range.superclass.superclass.superclass
=> nil
1.2. 同様にして、Hash
とSymbol
クラスの継承階層も調べてみてください。
>> Hash.superclass
=> Object
>> Hash.superclass.superclass
=> BasicObject
>> Hash.superclass.superclass.superclass
=> nil
>> Symbol.superclass
=> Object
>> Symbol.superclass.superclass
=> BasicObject
>> Symbol.superclass.superclass.superclass
=> nil
余談 - Class
クラスの継承階層
>> Class.superclass
=> Module
>> Class.superclass.superclass
=> Object
>> Class.superclass.superclass.superclass
=> BasicObject
>> Class.superclass.superclass.superclass.superclass
=> nil
Range
、Hash
、Symbol
、Class
の継承階層をひとまとめにすると、以下のようになります。
2. リスト 4.15にあるself.reverse
のself
を省略し、reverse
と書いてもうまく動くことを確認してみてください。
>> class Word < String
>> def palindrome?
>> self == reverse # self.reverseのself.を省略
>> end
>> end
=> :palindrome?
>> s = Word.new("level")
=> "level"
>> s.palindrome?
=> true
>> s.length
=> 5
確かにうまく動いています。
組み込みクラスの変更
Rubyでは、組み込みクラスそのものを拡張したり、組み込みクラスのメソッドの動作を変更することも可能です。
>> class String
>> def palindrome?
>> self == self.reverse
>> end
>> end
=> :palindrome?
>> "deified".palindrome?
=> true
blank?
メソッド
Rails3によって追加されるメソッドの一つです。
>> "".blank?
=> true
>> " ".empty?
=> false
>> " ".blank?
=> true
>> nil.blank?
=> true
-
blank?メソッドのソースコード
- 最も基底では、
Object
クラスに定義されている -
blank?
の否定を返すpresent?
というメソッドも定義されている -
NilClass#blank?
、FalseClass#blank?
は常にtrue
を返す -
TrueClass#blank?
は常にfalse
を返す -
String
、Array
、Hash
、Numeric
、Time
に対するblank?
の定義は上書きされている
- 最も基底では、
Railsによる、配列に対する追加メソッド
実は、配列に対するsecond
、third
、fourth
、fifth
、forty_two
というメソッドも、Rails3によって組み込みクラスArray
に追加されたメソッドです。
>> [*('a'..'z')].second
=> "b"
>> [*('a'..'z')].third
=> "c"
>> [*(1..42)].forty_two
=> 42
演習 - 組み込みクラスの変更
1.1. palindrome?
メソッドを使って、“racecar”が回文であり、“onomatopoeia”が回文でないことを確認してみてください。
>> "racecar".palindrome?
=> true
>> "onomatopoeia".palindrome?
=> false
1.2. 南インドの言葉「Malayalam」は回文でしょうか?
>> "Malayalam".palindrome?
=> false
>> "Malayalam".downcase.palindrome?
=> true
大文字小文字を区別しないようにすれば、回文になるようです。
2. リスト 4.16を参考に、Stringクラスにshuffleメソッドを追加してみてください。
Railsチュートリアルより、リスト4.16(「?」を適切なメソッドに置き換える)
class String
def shuffle
self.?('').?.?
end
end
"foobar".shuffle
=> "borafo"
>> class String
>> def shuffle
>> self.split('').shuffle.join
>> end
>> end
=> :shuffle
>> "foobar".shuffle
=> "obfora" # 結果は実行するごとに変わります
処理順序としては、以下の通りでしたね。
-
split('')
メソッドにより、文字列を1文字ずつの配列に変換する -
shuffle
メソッドにより、元の配列と同じ内容を持ち、要素の順番だけが異なる配列を生成する -
join
メソッドにより、配列を一つの文字列に変換する
3. リスト 4.16のコードにおいて、self.
を削除してもうまく動くことを確認してください。
これまでのshuffle
メソッドの定義を消去するため、一旦Rails Consoleから抜けた上で、再びRails Consoleを起動します。そのうえで、以下のコードを入力していきます。
>> class String
>> def shuffle
>> split('').shuffle.join # self.を削除
>> end
>> end
=> :shuffle
>> "foobar".shuffle
=> "oaobrf" # 結果は実行するごとに変わります
確かにうまく動きますね。
Railsのコントローラクラス
>> controller = StaticPagesController.new
=> #<StaticPagesController:0x000055de496db6e0 @_action_has_layout=true, @_routes=nil, @_request=nil, @_response=nil>
>> controller.class
=> StaticPagesController
>> controller.class.superclass
=> ApplicationController
>> controller.class.superclass.superclass
=> ActionController::Base
>> controller.class.superclass.superclass.superclass
=> ActionController::Metal
>> controller.class.superclass.superclass.superclass.superclass
=> AbstractController::Base
>> controller.class.superclass.superclass.superclass.superclass.superclass
=> Object
>> controller.class.superclass.superclass.superclass.superclass.superclass.superclass
=> BasicObject
>> controller.class.superclass.superclass.superclass.superclass.superclass.superclass.superclass
=> nil
継承関係を表すクラス図は以下のようになります。
演習 - Railsのコントローラクラス
1. 第2章で作ったToyアプリケーションのディレクトリでRailsコンソールを開き、User.new
と実行することでuserオブジェクトが生成できることを確認してみましょう。
>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
2. 生成したuserオブジェクトのクラスの継承階層を調べてみてください。
>> u.class
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
>> u.class.superclass
=> ApplicationRecord(abstract)
>> u.class.superclass.superclass
=> ActiveRecord::Base
>> u.class.superclass.superclass.superclass
=> Object
>> u.class.superclass.superclass.superclass.superclass
=> BasicObject
>> u.class.superclass.superclass.superclass.superclass.superclass
=> nil
ユーザークラス
ここで、Railsコンソールから抜け、sample_app
の開発環境に戻ります。その上で、以下のUser
クラスを、/example_user.rb
に作成します。
class User
attr_accessor :name, :email
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
def formatted_email
"#{@name} <#{@email}>"
end
end
「当該User
クラスでは、DBを扱わない」というのが一つのポイントです。DBを扱うとなると、「DBのマイグレーション処理」等、Rails特有の処理が多数発生し、クラスやメソッドの振る舞いも純粋なRubyからかけ離れていきます。今回は、DBを扱わないので、クラスやメソッドの振る舞いは純粋なRubyに近いものになります。
attr_accesor
メソッド
attr_accessor :name, :email
attr_accessor
というのは、インスタンス変数に対する読み取り・書き込み両方を行うためのメソッドを用意するメソッドです。このコードでは、インスタンス変数@name
と@email
に対する読み取り・書き込み4を行うためのメソッドが用意されるということです。VBAのProperty Get
とProperty Set
をセットにしたもの」に近いものでしょうか。
以下のコードは、先ほど記述したattr_accesor
と同じメソッドを定義するコードです。
def name=(str)
@name = str
end
def name
@name
end
def email=(str)
@email = str
end
def email
@email
end
attr_accessor
については、以下のような説明も参考になるかと思われます。
initialize
メソッド
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
initialize
というのは、Rubyの特殊なクラスメソッドの一つです。クラスメソッドをnew
したとき(今回だとUser.new
したときですね)に自動で呼び出され、メソッド内の記述に基づいてインスタンス変数を初期化します。Java等でいうところのコンストラクタですね。
今回のinitialize
メソッドは、デフォルトの引数として空のハッシュを一つ取ります。当該ハッシュは、initialize
メソッド内部ではattributes
という名前で使われます。Rubyでは、存在しないキーに対するハッシュの値はnil
であるため、attributes[:name]
・attributes[:email]
はいずれもnil
となり、最終的には@name=nil, @email=nil
となります。
formatted_email
メソッド
def formatted_email
"#{@name} <#{@email}>"
end
formatted_email
メソッドは、文字列の式展開を利用し、@name
と@email
に割り当てられた値をユーザのメールアドレスとして構成するメソッドです。@name
と@email
はいずれもUser
クラスのインスタンス変数であり、formatted_email
メソッドでもそのまま使うことができます。
自作したUser
クラスを使ってみる
example_user.rb
を作成したRails環境のルートディレクトリでRailsコンソールを実行します。以降はRailsコンソールでの入力と、その応答です。
>> require './example_user' # example_userのコードを読み込む
=> true
>> example = User.new
=> #<User:0x000055de496ee8d0 @name=nil, @email=nil>
>> example.name # :attributes[:name]は存在しないのでnil
=> nil
>> example.name = "Example User" # 名前を代入する
=> "Example User"
>> example.email = "user@example.com" # メールアドレスを代入する
=> "user@example.com"
>> example.formatted_email
=> "Example User <user@example.com>"
-
./example_user
は、カレントディレクトリのexample_userファイルを指す-
.
は、Unixのカレントディレクトリを指す
-
-
new
メソッドで空のexample_userを作成する- この時点では
@name
・@email
ともnil
- この時点では
- 対応する属性にそれぞれ手動で値を代入している
-
attr_accessor
を定義しているので、このような形で代入できる
-
以上が動作のポイントでしょうか。
マスアサインメント
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User:0x000055de49681780 @name="Michael Hartl", @email="mhartl@example.com">
>> user.formatted_email
=> "Michael Hartl <mhartl@example.com>"
User
クラスに属するuser
オブジェクトが、生成時点で@name="Michael Hartl", @email="mhartl@example.com"
となっていますね。
例えば今回の自作User
クラスの場合、new
メソッドの引数にハッシュを与えることにより、インスタンス変数が定義済みのインスタンスを生成することができます。こうした技法を「マスアサインメント」といいます。Railsアプリケーションでも広く使われる技法だそうです。
この章の学習内容との関係で言えば、「(メソッドの最後の引数なので)ハッシュの波括弧が省略されている」というのは一つのポイントですね。
なお、マスアサインメントに関係して、以下のような概念が後々登場することになりますが、それはまた別の話です。
- マスアサインメント脆弱性
- マスアサインメント脆弱性への対策であるStrong Parameters
演習 - ユーザークラス
1.1. Userクラスで定義されているname属性を修正して、first_name属性とlast_name属性に分割してみましょう。
User#attr_accessor
を書き換えていきます。
attr_accessor :first_name, :last_name, :email
この部分を実装すると、User.new
の挙動が以下のようになります。
>> example = User.new
=> #<User:0x000055de48a8d7a8 @first_name=nil, @last_name=nil, @email=nil>
1.2. また、それらの属性を使って "Michael Hartl" といった文字列を返すfull_nameメソッドを定義してみてください。
1.1.に引き続き、User#full_name
メソッドを追加します。
def full_name
"#{@first_name} #{@last_name}"
end
この部分まで実装したときの、User#full_name
の挙動は以下の通りになります。
>> example.first_name = "Michael"
=> "Michael"
>> example.last_name = "Hartl"
=> "Hartl"
>> example.full_name
=> "Michael Hartl"
最初、{@last_name}
の前の#
を入れ忘れてしまい、"Michael {@last_name}"
のような戻り値が出力されて「???」となりました。こういうイージーミスはやってしまいがちですよね。
1.3. 最後に、formatted_email
メソッドのname
の部分を、full_name
に置き換えてみましょう (元々の結果と同じになっていれば成功です)
1.2.に引き続き、User#formatted_email
メソッドを書き換えていきます。
def formatted_email
"#{full_name} <#{@email}>"
end
この部分まで実装したときの、User#formatted_email
の挙動は以下の通りになります。
>> example.first_name = "Michael"
=> "Michael"
>> example.last_name = "Hartl"
=> "Hartl"
>> example.email = "user@example.com"
=> "user@example.com"
>> example.formatted_email
=> "Michael Hartl <user@example.com>"
2. "Hartl, Michael" といったフォーマット (苗字と名前がカンマ+半角スペースで区切られている文字列) で返すalphabetical_name
メソッドを定義してみましょう。
1.3.に引き続き、User#alphabetical_name
メソッドを追加します。
def alphabetical_name
"#{@last_name}, #{@first_name}"
end
User#alphabetical_name
の挙動は以下の通りになります。
>> require './example_user'
=> true
>> example = User.new
=> #<User:0x000055de49caed60 @first_name=nil, @last_name=nil, @email=nil>
>> example.first_name = "Michael"
=> "Michael"
>> example.last_name = "Hartl"
=> "Hartl"
>> example.alphabetical_name
=> "Hartl, Michael"
期待通りの挙動になりましたね。
3. full_name.split
とalphabetical_name.split(’, ’).reverse
の結果を比較し、同じ結果になるかどうか確認してみましょう。
2.まで完了した後に、Rails Consoleで以下のコードを入力していきます。
>> example.full_name.split == example.alphabetical_name.split(', ').reverse
=> true
>> example.full_name.split
=> ["Michael", "Hartl"]
>> example.alphabetical_name.split(', ').reverse
=> ["Michael", "Hartl"]
同じ結果になっているようです。
-
実際には、
Class#new
メソッド(自身もメソッドである)は、内部でクラスのインスタンスを生成するClass#allocate
メソッドと、インスタンスを初期化するObject#initialize
メソッドを実行しています。このあたりの話は、Rubyの言語仕様についての突っ込んだ話になり、Railsチュートリアルの範疇を超えます。 ↩ -
より厳密に言えば、Rubyにおいて「クラスメソッド」と呼ばれるものは、
Class
というクラスのインスタンスメソッドです。ただ、このあたりの話は、Rubyの言語仕様についての突っ込んだ話になり、Railsチュートリアルの範疇を超えます。 ↩ -
読み取りのみであれば
attr_reader
、書き込みのみであればattr_writer
という名前のメソッドを使います。 ↩