Rubyプログラミングの基礎知識
実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング
顧客管理システムの構築を体験しながら、Railsアプリケーション開発のノウハウを習得! この記事は、書籍『実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング』の内容を、Think IT向けに特別にオンラインで公開しているものです。詳しくは記事末尾の書籍紹介欄をご覧ください。インスタンス変数
「インスタンス変数」。単純そうで意外に奥が深い概念です。もう一度おさらいしておきましょう。
インスタンス変数とは
インスタンス変数は、特定のオブジェクトが排他的に所有する変数です。そのオブジェクト自身だけがその値を参照できます。インスタンス変数という名前は、クラス変数との対比で用いられています。クラス変数は、あるクラスのすべてのインスタンスが共有する変数です。インスタンス変数は、それぞれのインスタンスごとに固有の値を持ちます。
Rubyにおいてクラスはオブジェクトの一種ですので、クラスもインスタンス変数を持ちます。その値はクラス自身だけが参照できます。そのクラスのインスタンスからは参照できません。
次のプログラムをご覧ください。
~/chapter03/robot01.rb
class Robot @name = "ROBOT" def name @name end end r = Robot.new p r.name
2行目にあるインスタンス変数@nameはRobotクラス自体と結びついています。他方、5行目のインスタンス変数@nameは、Robotクラスの各インスタンスによって所有されます。同じ名前ですが、まったく別の変数です。この点は非常に重要です。
9行目のローカル変数rはRobotクラスのインスタンスです。rからは5行目の@nameは見えますが、2行目の@nameは見えないのです。したがって、このプログラムの実行結果は次のようになります。
$ ruby robot01.rb nil
もし2行目の意図が、Robotクラスのインスタンスにデフォルトの名前を与えることであるとすれば、本当は次のように書くべきでした。
~/chapter03/robot02.rb
class Robot def initialize @name = "ROBOT" end def name @name end end r = Robot.new p r.name
これならば、プログラムの結果として"ROBOT"という文字列が表示されます。
インスタンス変数はコンストラクタ(initializeメソッド)またはインスタンスメソッドの中で定義し、使用します。クラス定義の直下で定義されたインスタンス変数はそのクラス自体によって所有され、そのクラスの各インスタンスからは参照できません。
属性
インスタンス変数はそれを所有しているオブジェクト以外からは参照できません。値(オブジェクト)を代入することもできません。外部からのアクセスを許すには、次のように参照用と代入用のメソッドを用意する必要があります。
~/chapter03/robot03.rb
class Robot def name @name end def name=(name) @name = name end end
この種のメソッド定義は頻繁に行われるので、もっと簡便なやり方が用意されており、上の例は次のように書き換えられます。
~/chapter03/robot04.rb
class Robot attr_reader :name attr_writer :name end
attr_readerは引数に指定したシンボルに対応するインスタンス変数を参照するメソッドを定義するクラスメソッドです。同様にattr_writerは代入用のメソッドを定義してくれます。参照用と代入用のメソッドを一度に定義したい場合は、attr_accessorが使用できます。
~/chapter03/robot05.rb
class Robot attr_accessor :name end
こうして定義されたnameのことを属性と呼びます。しかし、その実体はnameおよびname=というメソッドに過ぎません。
末尾が等号(=)のメソッド名
Rubyのメソッド名で使用できる文字は英数字とアンダースコア(_)のみですが、特別ルールとして、メソッド名の末尾に疑問符(?)、感嘆符(!)、そして等号(=)が使用できます。
robot09.rbの6~8行ではname=というメソッドが定義されています。名前が等号で終わるメソッドは、さらに特別ルールでrobot.name = "Alice"のように呼び出せることになっています。これはrobot.name=("Alice")と同じ意味です。robot.name = "Alice"という式を「name属性に代入」と表現する場合もありますが、正確に言えばname=メソッドを呼び出しているのです。
なお、7行目の等号の右にあるnameは6行目の丸括弧の中にあるnameと同じものです。つまり、name=メソッドへの引数です。2~4行で定義されているnameメソッドと解釈されてもよさそうですが、そうはなりません。Rubyインタープリタは7行目でnameに出会ったとき、それがローカル変数または引数として定義されていないかどうかを調べます。定義されていない場合だけメソッドとして呼び出します。
遅延初期化
前項のプログラムでインスタンス変数@nameに初期値として"ROBOT"という文字列を与えたい場合、次のように書きます。
~/chapter03/robot06.rb
class Robot attr_accessor :name def initialize @name = "ROBOT" end end
しかし、次のように書き換えることも可能です。
~/chapter03/robot07.rb
class Robot attr_writer :name def name @name ||= "ROBOT" end end
Robotオブジェクトを作るときに@nameを初期化するのではなく、nameメソッドが初めて使用されたときに@nameを初期化しています。その際、すでにname=メソッドによって値がセットされていれば、デフォルト値による初期化は行われません。このような実装方法を遅延初期化(lazy initialization)と呼びます。
遅延初期化の利点は2つあります。1つは、nameメソッドが使用されない場合、@nameを初期化しなくてもよいということです。ここでは"ROBOT"という文字列を作るだけなのでほとんど効果はありませんが、デフォルト値に指定するオブジェクトの生成が重い処理である場合(たとえば、インターネットやデータベースから値を取得する場合)には、リソースの節約になります。
もう1つの利点は、インスタンス変数を初期化するコードと使用するコードが近接する(同一メソッド内にある)ということです。上記の例では1個しかインスタンス変数がないのでメリットを感じにくいですが、インスタンス変数の個数が増えてくるとコードの可読性を上げる効果があります。
なお、nameメソッドの中身は次のコードの省略形です。
@name = @name || "ROBOT"
@nameが偽値、すなわちnilかfalseの場合に@nameに"ROBOT"を代入します。したがって、真偽値を扱うインスタンス変数の場合には、この書き方ではうまくいきません。たとえば、
def activated? @activated ||= true end
のようにactivated?メソッドを定義したとします。しかし、@activatedにfalseを代入してもこのメソッドはtrueを返します。正しくは次のように書きます。
def activated? @activated = true if @activated.nil? end
実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング
顧客管理システムの構築を体験しながら、Railsアプリケーション開発のノウハウを習得! この記事は、書籍『実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング』の内容を、Think IT向けに特別にオンラインで公開しているものです。詳しくは記事末尾の書籍紹介欄をご覧ください。ブロックとProcオブジェクト
この節はやや高度な内容を含みます。難しく感じられたらいったん読み飛ばし、時間をおいて復習してください。
ブロック
ブロックとは
ブロックは、Rubyの初学者を悩ませる概念の筆頭です。おそらく最初に出会うのが配列のeachメソッドに付随するブロックでしょう。たとえば次のような。
[ 2, 3, 5, 7, 11 ].each { |n| puts n ** 2 }
このコードを実行すれば、4、9、25、49、121という数が結果として表示されます。
これはもちろんブロックの由緒正しき利用法です。問題は、ブロックを伴うメソッドを自分で定義するときです。そのようなメソッドを自由自在に定義できるようになると、プログラミングの幅が格段に広がります。もちろんRailsによるアプリケーション開発でも役立ちます。
簡単な使用例を示しましょう。
chapter03/block01.rb
class Robot def job puts "Started." yield puts "Finished." end end robot = Robot.new robot.job { puts "Hello World!" }
最終行の波括弧({})で囲まれた部分がブロックです。ブロックはメソッドにコードを渡すための仕組みです。「メソッドにコードを渡す」とは、どういう意味でしょうか。普通はメソッドに引数としてさまざまなオブジェクトを渡しますね。たとえばr.job(7)のように。この7というオブジェクトの代わりにputs "Hello World!"というコードを渡しているのです。
さて、4行目にyieldと書いてあります。ここでブロックが実行されます。3行目と5行目を見てください。ここに書かれているコードがブロックを実行する前と後に実行されます。
つまり、このプログラムを実行すると次のような結果となります。
$ ruby block01.rb Started. Hello World! Finished.
なお、ブロックの開始と終了は波括弧({})の代わりにdoとendでも示せます。
robot.job do puts "Hello World!" end
通常、私はブロックが1行に収まるときは波括弧({})、収まらないときはdoとendを使うことにしています。
ブロック変数
メソッドからブロックにオブジェクトを渡したいときはブロック変数を利用します。これも実例から学びましょう。
chapter03/block02.rb
class Robot def initialize(name) @name = name end def job puts "Started." yield(@name) puts "Finished." end end robot = Robot.new("Alice") robot.job { |name| puts "Hi, my name is #{name}!" }
ブロックの開始直後にあるパイプ文字(|)で囲まれたnameがブロック変数です。この例では、インスタンス変数@nameの値である"Alice"という文字列がブロック変数nameにセットされます。そして、そのnameの値を用いてputs "Hi, my name is #{name}!"というコードが実行されます。
このプログラムを実行すると次のような結果となります。
$ ruby block02.rb Started. Hi, my name is Alice! Finished.
ブロックの活用法
次に、もう少し複雑なブロックの使用例をお見せしましょう。
chapter03/block03.rb
class Robot attr_reader :x, :y def initialize @x = 0 @y = 0 end def job x0, y0 = x, y yield(self) puts "(#{x0}, #{y0}) => (#{x}, #{y})" end def move(d1, d2) @x += d1 @y += d2 end end robot = Robot.new robot.job do |r| r.move(1, 0) r.move(0, 1) end robot.job do |r| r.move(1, 0) r.move(1, 1) r.move(0, -3) end
jobメソッドの定義に着目してください。
def job x0, y0 = x, y yield(self) puts "(#{x0}, #{y0}) => (#{x}, #{y})" end
まずローカル変数x0とy0に属性xとyの値をセットしています。そして、ブロック変数としてself、つまり自分自身をセットしてブロックを実行します。この間に属性xとyの値は変化します。最後に属性xとyの値がどう変化したかを出力しています。
次に、このメソッドを呼び出す側のコードを読み解きましょう。Robotオブジェクトが生成された後、jobメソッドが2回呼ばれています。1回目の呼び出しはこうです。
robot.job do |r| r.move(1, 0) r.move(0, 1) end
ブロックの内部ではブロック変数rのmoveメソッドが2回呼ばれています。このrの正体は何でしょうか。メソッドjobがyieldを通じて送ってくるselfすなわちRobotオブジェクトです。
上記コードに現れるローカル変数robotとブロック変数rは同じオブジェクトを指しています。しかし、メソッドの中でyield(self)が呼ばれたから同じになっただけで、常にそうなるわけではありません。混乱しやすいところです。注意してください。
ブロックの中でr.move(1, 0)というコードが実行されると、このr(Robotオブジェクト)の属性xに1が加算されます。同様に、次のr.move(0, 1)でrの属性yに1が加算されます。ですから、1回目のjobメソッドコールが終わるとターミナルには次のような結果が表示されていることになります。
$ ruby block03.rb (0, 0) => (1, 1)
続いて2回目のjobメソッドコールに処理が移ります。
robot.job do |r| r.move(1, 0) r.move(1, 1) r.move(0, -3) end
ローカル変数robotは前回と同じRobotオブジェクトを参照していて、そのRobotオブジェクトは自らの状態を記憶していますので、属性xとyの値は1と1です。そして、ブロック変数rのmoveメソッドが3回呼ばれます。属性xの値は2加算されて、属性yの値は2減らされています。
結局のところ、最終的なプログラムの実行結果は次のようになります。
$ ruby block03.rb (0, 0) => (1, 1) (1, 1) => (3, -1)
ところで、このプログラムの要点はどこにあるのでしょうか。単にRobotオブジェクトの属性をmoveメソッドで変更するだけならこんなに複雑なプログラムを書く必要はありません。私がしたかったのは、一連のmoveメソッドコールの前後で属性xとyがどう変化したかを画面に表示することです。ブロックを使うと、その目的をエレガントに達成できます。
通常のメソッド呼び出しはメソッド側で定義されたコードしか実行できません。しかし、ブロック付きでメソッドを呼び出す場合は、メソッド側で定義されたコードの中にメソッドを呼び出す側から別のコード(ブロック)を挿入することができるのです(図3-1)。
Procオブジェクト
Procオブジェクトとは
Procオブジェクトをひと言で説明すれば「名前のない関数」です。プログラミング用語としての関数(function)は、引数を受け取り、何らかの処理をして戻り値を返すようなコードを指します。働きはメソッドによく似ています。
Procというクラス名は「手続き」を意味するprocedureに由来します。
Procオブジェクトを生成するコードの基本形は次のとおりです。
-> { ... }
行頭の->はProcオブジェクトを生成する特別な記号です。続く波括弧({})で囲まれた部分はブロックです。Procオブジェクトが処理したい内容をここに書きます。
Procオブジェクトに引数を渡したい場合は、次のように書きます。
-> (x, y) { ... }
xが第1引数、yが第2引数です。メソッド定義と同様、引数にデフォルト値を与えることも可能です。
-> (x, y, z: nil) { ... }
使用例を見てみましょう。
chapter03/proc01.rb
p = -> (n) do n += 1 if n.odd? n ** 2 end puts p.call(7)
Procオブジェクトを生成して変数pに代入しています。doとendの間に式が2つあります。その内容は「nが偶数ならnの2乗を、nが奇数ならn + 1の2乗を計算する」というものです。2行目のp.call(7)は、7を引数としてこのProcオブジェクトのコードを呼び出す(実行する)ということです。このスクリプトを実行するとターミナルには64と表示されます。
もちろん、メソッドを定義すればこれと同じことが実現できます。
def foo(n) n += 1 if n.odd? n ** 2 end puts foo(7)
なぜProcオブジェクトなどというものが必要なのかと不思議に思われることでしょう。しかし、いったんその疑問は脇に置いてください。Procオブジェクトはメソッドと同様の働きをする、という点だけ理解してください。
Procオブジェクトの活用法
では、Procオブジェクトを用いたプログラムの例をお見せしましょう。
lambda、Proc.new、proc
Procオブジェクトを生成するための->は、Ruby 1.9で導入された比較的新しい記号です。それ以前は、lambdaメソッドまたはprocメソッドでProcオブジェクトを生成していました。次の2行は同じ意味です。
-> (x, y, z: nil) { ... } lambda { |x, y, z: nil| ... }
また、Procオブジェクトは次のように書いても生成できます(2行とも同じ意味です)。
Proc.new { |x, y, z: nil| ... } proc { |x, y, z: nil| ... }
ただし、Proc.newまたはprocによって作られたProcオブジェクトは、->またはlambdaで作られたProcオブジェクトと異なり、次の2点の性質を持ちます。
- 呼び出し側とProcオブジェクト側とで引数の個数が食い違っていてもエラーにならない。
- コードの中でreturnを使えない。
これらの性質を持つProcオブジェクトはあまり使い勝手のよいものではありませんので、本書では常に->記号を用いてProcオブジェクトを生成することにします。
chapter03/proc02.rb
class Robot attr_reader :x, :y attr_writer :handler def initialize @x = 0 @y = 0 @handler = -> { move(1, 0) } end def walk @handler.call puts "(x, y) = (#{@x}, #{@y})" end def move(d1, d2) @x += d1 @y += d2 end end r = Robot.new r.walk r.walk r.handler = -> { r.move(0, 1) } r.walk r.handler = -> { r.x <= 2 ? r.move(1, 1) : r.move(-1, 1) } r.walk r.walk
このプログラムを実行すると、次のような結果となります。
$ ruby proc02.rb (x, y) = (1, 0) (x, y) = (2, 0) (x, y) = (2, 1) (x, y) = (3, 2) (x, y) = (2, 3)
initializeメソッドの中で(7行目)、Procオブジェクトが生成されインスタンス変数@handlerに代入されています。
@handler = -> { move(1, 0) }
この@handlerはwalkメソッドの中で使用されます。
def walk @handler.call puts "(x, y) = (#{@x}, #{@y})" end
単に@handlerが参照するProcオブジェクトを呼び出しているだけです。つまり、walkメソッドを呼び出すとmove(1,0)が呼び出されます。
ただし、2行目でattr_writer :handlerと宣言されていますので、次のようにして@handlerを置き換えることができます(24行目)。
r.handler = -> { r.move(0, 1) }
慣れないうちは読みにくいですが、-> { r.move(0.1) }の部分でProcオブジェクトを生成して、それをhandler=メソッドに引数として渡しています。
@handlerが置き換えられると、walkメソッドの働きが変化します。それまでは属性xに1を加えていたのですが、属性yに1を加えるようになりました。
さらに26行目で@handlerを置き換えています。
r.handler = -> { r.x <= 2 ? r.move(1, 1) : r.move(-1, 1) }
その結果、walkメソッドは属性xの値が2以下なら属性xとyに1を加え、そうでなければ属性xから1を引いて、属性yに1を加える、という処理を行うようになります。
私がこのプログラムを通じて示したかったのは、クラスの定義を変更せずにそのインスタンスの機能を調整する方法です。クラスの定義を変更すればその影響範囲はそのインスタンス全体に及びます。そういう事態は避けたい場合があります。
また、このプログラムのテクニックを使えば、インスタンスの機能を動的に変更できます。比喩的に言えば、Robotクラスのインスタンス変数@handlerはロボットに内蔵された「記憶装置」と見なせます。そして、そこにセットされるProcオブジェクトはロボットを駆動するための「ソフトウェア」です。つまり、このプログラムの目的は、ロボットのソフトウェアを「アップグレード」することだったのです。
デフォルトのエンコーディングがUTF-8に
Rubyのバージョン1.9.0から1.9.3までは、ソースコードをUTF-8で記述する場合でも、ASCIIの範囲から外れる文字(ひらがな、カタカナ、漢字など)を使用するときは、ファイルの1行目に次のような形式のコメント(通称、マジックコメント)を書く必要がありました。
# coding: utf-8
Ruby 2.0以降ではデフォルトのエンコーディングがUTF-8になったので、このコメントは不要です。
この記事のもとになった書籍 | |
---|---|
黒田 努 著 |
実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング本書は、Ruby on Railsの実践的な学習書です。最新のRuby2.0およびRuby on Rails4.1、RSpec3.0に対応しました。1つの企業向け顧客管理システムを作る中でRailsによるWebアプリケーション開発の基礎知識とさまざまなノウハウを習得していきます。各章末には演習問題が設けられているので、理解度を確かめながら確実に読み進められます。 |