【Ruby】ブロックの使い方とブロックメソッドの使い方

  • ブロックって何?
  • どうやって使うの?

Rubyを学習していて、ブロックの概念がいまいち理解できてなかったので再学習。
まとめました。

ブロックとは?

メソッド呼び出しの際に引数と一緒に渡すことの出来る処理の塊です。

ブロック呼び出しの形式

形式は2種類あります。
ブロック変数は、ブロックを実行する際にメソッドから渡されるパラメータです。
ブロック変数がいくつ渡されるかは、メソッドによって異なります。

(1) do~endで囲む形式

オブジェクト.メソッド名(引数リスト) do | ブロック変数 |
繰り返したい処理
end

(2)   {}で囲む形式

オブジェクト.メソッド名(引数リスト) { | ブロック変数 | 繰り返したい処理 }

使い分け方は、繰り返したい処理が複数行になる場合は(1)。1行で収まる場合は(2)を使います。

以下は、どちらも配列の1~5を順番に出力するだけのコードです。

eachメソッドから、配列の1つづつ要素が渡されてブロック変数testに格納してます。
pメソッドで出力しています。

# do~endで囲む形式
[1,2,3,4,5].each do |test|
  p test 
end

#  {}で囲む形式
[1,2,3,4,5].each { |test| p test }

ブロックの主な使い方

繰り返し(イテレータ)

上の例で示したような繰り返し処理にブロックを使うパターンです。

後処理など決まった処理を実行させる

Fileの処理などがいい例です。

Fileは、openメソッドを呼んだ後にcloseメソッドを呼ばないと後続処理に悪影響を及ぼします。
使い終わったら必ずcloseメソッドを呼び出すような後処理をさせるのにもブロックが向いています。

以下は、openメソッドにブロック処理を渡さない場合です。

a
b
c
d
e
file = File.open("sample.txt")
begin
  file.each_line do |line|
    print line
  end
ensure
  file.close
end

出力結果は、a~eが順番に出力されます。

openメソッドにブロックを渡した場合、begin~ensureとclose処理の処理をopenメソッド側で処理してくれます。

File.open("sample.txt") do |file|
  file.each_line do |line|
    print line
  end
end

「ファイルを使い終わったら閉じる」のような決まった処理はメソッド側で行うようにすると、使う側は必要な処理だけをブロック内に書ける上に処理漏れなどのバグを事前に防ぐことが出来ます。記述量も減らせるのもメリットとして上げられます。

目的によって一部の処理を差し替える

共通部分はメソッドで提供して、目的によって異なる処理だけを差し替える場合などにもブロックが使えます。

以下のsortの例では、ブロックでソート順を決めています。
このように、ブロックの中だけ目的によって処理を差し替えるのにブロックが使えます。

p [9,3,5,3,7,2,1].sort {|a,b| a <=> b}
# >= [1, 2, 3, 3, 5, 7, 9]

p [9,3,5,3,7,2,1].sort {|a,b| b <=> a}
# >= [9, 7, 5, 3, 3, 2, 1]

ブロック付きメソッドの作り方

ブロックの主な使い方を見てきたので、実際に自分でブロック付きのメソッドを作ってみます。

メソッド内でブロックを実行する

キーワードは、yield
yieldがメソッドに与えられたブロックを実行します。

def block_test
  yield
end

block_test {p "ブロック付きメソッドだよ"} #>= "ブロック付きメソッドだよ"

# ブロックを渡さないとLocalJumpErrorが発生
#block_test

ブロックを渡さないとLocalJumpErrorが発生します。
発生させたくない場合は、block_given?メソッドを使ってブロックがあるかどうかの判定をして処理を分岐させます。

def block_test
  if block_given?
    yield
  else
    p "ブロックがないよ"
  end
end

block_test {p "ブロック付きメソッドだよ"} #>= "ブロック付きメソッドだよ"

# ブロックを渡さないとnilで返却
block_test #>= "ブロックがないよ"

ブロック変数の受け渡し

ブロック変数の受け渡し方法は、

  1. yeildに引数を持たせる
  2. 呼び出し元で、引数を受け取る
  3. yield側でブロックの処理結果を受け取る

になります。ややこしいので以下例。

def block_test(a)
  result = 0
  if block_given?
    result += yield(a * 2)
  else
    result += a
  end
end

# ブロックあり
result = block_test(1) do |a|
  a * 10
end
p result #>= 20

# ブロック無し
result = block_test(1)
p result #>= 1
  1. block_testメソッド呼び出し。
  2. ブロックがあるのでif文の中に入る。
  3. yieldの引数にメソッド側で受け取った変数a(=1)に2をかけた値をいれる
  4. 呼び出しもとのブロック処理に入る。2が変数aに入る
  5. ブロック処理内で、2 * 10を実行。
  6. 結果の40を変数resultに足しこむ
  7. 処理結果の40をpメソッドで出力。

あっち行ったり、こっち行ったりかなり面倒ですが使いこなせるようになると便利。

ちなみに、yieldの引数の数とブロック変数の数は違っていてもOK

ブロック変数が多い場合は、nilが。
足りない場合は、切り捨てられます。

def block_test
  yield(1,2,3)
end

# ブロック変数が足りない場合はnilが入る。
block_test do |a,b,c,d|
  p a #>= 1
  p b #>= 2
  p c #>= 3
  p d #>= nil
end

# ブロック変数が足りない場合は切り捨てられる
block_test do |a,b|
  p a #>= 1
  p b #>= 2
end

ブロック内でのbreak、nextなどの制御

ブロック内でbreak、nextなどを使う場合は注意が必要です。

例えば以下のようなメソッドの場合。
引数に1が指定されている場合は、breakに入らないため問題なく「1」が出力されます。

def block_test(num)
  yield(num)
end

result = block_test(1) do |num|
  if num == 2
    break
  end
  num
end
p result #>= 1

今度2を指定すると?

nilが返却されます!ここが注意点です。
ブロック付き呼び出しの場所まで一気に戻ってしまうため、処理途中のデータは吹き飛びます。

# breakが途中に入ると?
result = block_test(2) do |num|
  if num == 2
    break
  end
  num
end
p result #>= nil

メソッドの結果にnilではなく何か特定の値を指定した場合は、breakに引数を渡すとOKです。
以下の例では、「0」を返却してます。

# breakが途中に入ると?
result = block_test(2) do |num|
  if num == 2
    break 0
  end
  num
end
p result #>= 0

nextやredoでも気をつける点は同様です。
他はnextやredoと同じ動きになります。

ブロックをオブジェクトとして受け取る

ブロックをオブジェクトとして受け取ることで

  • ブロックを受け取ったメソッドとは別の場所でブロックを実行する
  • ブロックを別のメソッドに与えて実行する

ことが出来るようになる。

Procオブジェクトを使うことで、ブロックをオブジェクトとして持ち運べるようになります。
callメソッドで呼び出せます。

name = Proc.new do |name|
  p "My name is #{name}"
end

name.call("Pepi") #>= "My name is Pepi"
name.call("Zizi") #>= "My name is Zizi"

メソッドからメソッドにブロックを渡す方法

  1. ブロックをProcオブジェクトとして変数で受け取る
  2. 次のメソッドに渡す

メソッドの引数を設定する箇所で、最後の引数を「&引数名」とすると、そのメソッドを呼び出す時に与えられたブロックは自動的にProcオブジェクトに包まれて引数として渡される。

def proc_test(a, b, &proc)
  if proc
    proc.call(a,b)
  else
    a * b
  end
end

result = proc_test(1,2) do |c,d|
  c + d
end
p result #>= 3

result = proc_test(1,2)
p result #>= 2

&をつけて受け取る引数のことをProc引数と言います。
メソッド呼び出し時に、ブロックが渡されていない場合、nilになるのでブロックの有無は容易に判定できます。

一点注意点は、Proc引数は最後の引数にしなければなりません
してなかったらエラーになります。

まとめ

  • ブロックについて
  • ブロックの主な使用方法
  • ブロックメソッドの作り方

Procの他にlambdaなんかもありますが、ここでは割愛します。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です