9. メソッド

メソッドとは

メソッドは、オブジェクトに対する一連の処理(手続き)を記述したものです。
繰り返し実行する処理をまとめておいたり、一連の処理に名前をつけてコードを見やすくしたりできます。
例えば、以下の様なコードを考えてみましょう。

メソッドを使用しないと

greet.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 処理A
name_a = "taro"
case name_a
when "taro"
  puts "Hello, 太郎さん!"
when "jiro"
  puts "Hello, 二郎さん!"
when "saburo"
  puts "Hello, 三郎さん!"
end
#=> Hello, 太郎さん!

# 処理B
name_b = "jiro"
case name_b
when "taro"
  puts "Hello, 太郎さん!"
when "jiro"
  puts "Hello, 二郎さん!"
when "saburo"
  puts "Hello, 三郎さん!"
end
#=> Hello, 二郎さん!

# 処理C
name_c = "saburo"
case name_c
when "taro"
  puts "Hello, 太郎さん!"
when "jiro"
  puts "Hello, 二郎さん!"
when "saburo"
  puts "Hello, 三郎さん!"
end
#=> Hello, 三郎さん!

何度も同じような処理を書いていて、疲れそうですね。
もし、"Hello, 〇〇さん!"という文言を"こんにちは,〇〇くん!"に変更したくなったら、9箇所の変更が必要になりめんどくさいですね。
読む側も全部読まなければ、同じ処理だということが分かりません。
そこで、この一連の処理をメソッドを使ってまとめてみましょう。

メソッドを使って処理を共通化しよう

上記のプログラムの共通な部分をまとめて、greetという名前のメソッド(関数)として定義します。
この際、左の3つの処理の中で異なる部分(name_a, name_b, name_c)を、一つの変数 name で表します。
このとき、メソッドの定義には変数を()で囲んだものをメソッド名の後につけて、greet(name)のように表します。この name引数と呼びます。

一度メソッドを作ってしまえば、もとの処理はgreet(name_a)などと記述することができます。
このとき、( )内に値や変数を書くと、その中身がxに代入されるようになっています。
これを、引数に値を渡すなどと呼びます。

ここまでの流れをプログラムにすると、以下のようになります。

greet_method.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# これが関数
def greet(name)
  case name
  when "taro"
    puts "Hello, 太郎さん!"
  when "jiro"
    puts "Hello, 二郎さん!"
  when "saburo"
    puts "Hello, 三郎さん!"
  end
end

# 処理A
name_a = "taro"
greet(name_a) #=> Hello, 太郎さん!

# 処理B
name_b = "jiro"
greet(name_b) #=> Hello, 二郎さん!

# 処理C
name_c = "saburo"
greet(name_c) #=> Hello, 三郎さん!

明らかにコードの量が減り、楽に書けました!
今後表示する文言を修正したい場合も、メソッドの中身の変更だけで済みます。
メソッドの利点はコードを書く側だけでなくコードを見る側にもあります。
同じ処理をまとめたことで、「ここは同じ処理だ」と明確に分かり、読みやすくなります。
さらに、適切な関数名をつけておけば、どういった処理が行われているかが容易に理解できます。

メソッドの利点のまとめ

  • 同様の処理をまとめることで、書く量が減る
  • 同様の処理を他のところでも使いまわせる
  • 処理を変更したいとき、メソッド内だけ変更すればよい(DRY)
  • 適切なメソッド名を付けることでコードの可読性が上がる

メソッドの定義

Rubyにおけるメソッドは、一般的に以下の形式で記述します。

メソッドのフォーマット

1
2
3
4
# arg1, arg2, ...は引数
def method_name(arg1, arg2, ...)
  # 処理
end

実際にメソッドを定義する場合、引数の有無や引数に初期値(デフォルト値)を設定するかなどで、書き方にいくつかバリエーションがあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 引数なし
def hello_world
  puts "Hello World"
end

# 引数あり
def multiplication(num1, num2)  # 引数num1, num2を処理の中で使用する
  num1 * num2
end

# 引数にデフォルト値(引数が与えられなかった場合の値)を設定
def hello_name(name = "World")
  puts "Hello, #{name}"
end

Step Up! : キーワード引数

Ruby2.0以降、キーワード引数というものが追加されました。
これは引数に名前(キーワード)をつけられるというものです。
キーワード引数は、キーワード:の形式で定義します。
:(コロン)の後ろにデフォルトを設定することも可能です。

1
2
3
def keyword_args_method(arg1:, arg2: default_value)
  :
end

キーワード引数で定義されたメソッドに引数を渡す場合には、同じようにキーワードを指定する必要がありますが、その順番は関係ありません。
したがって、引数が多い場合などに引数の順番を気にする必要がなくなるという利点があります。

1
2
3
# 以下は同じ結果になる
result = keyword_args_method(arg1: value1, arg2: value2)
result = keyword_args_method(arg2: value2, arg1: value1)
keyword_args.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def hello_human(name:'名無し', age:0)
  puts "こんにちは、#{age}歳の#{name}さん。"
end

hello_human()
#=> こんにちは、0歳の名無しさん。

hello_human(name: '太郎', age:24)
#=> こんにちは、24歳の太郎さん。

hello_human(age:24, name: '太郎')
#=> こんにちは、24歳の太郎さん。

hello_human('太郎', 24) # これはキーワードの指定がないためエラー
#=> code/chapter9/keyword_args.rb:1:in `hello_human': wrong number of arguments (given 2, expected 0) (ArgumentError)from code/chapter9/keyword_args.rb:11:in `<main>'

メソッドの実行

メソッドを実行したい場合は、関数名(引数)のように記述します。
次のメソッドは、引数で数値xを受け取り、その数値の二乗を返すメソッドsquareを実行する例です。

square.rb
1
2
3
4
5
6
7
8
9
# 引数で与えられた数値の二乗を返すメソッド
# 引数: x(Numeric)
def square(x)
  x * x
end

puts square(3) #=> 9
puts square(5) #=> 25
puts square(1.5) #=> 2.25

もちろん、メソッドの中でメソッドを呼び出すことも可能です。
上記のsquareメソッドを用いてx2+2x+1を求めるメソッドcalc_quadraticを実装すると、次のようになります。

calc_quadratic.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
# 引数で与えられた数値の二乗を返すメソッド
# 引数: x(Numeric)
def square(x)
  x * x
end

# 引数で与えられた数値から x^2 + 2x + 2 を求めるメソッド
# 引数: x(Numeric)
def calc_quadratic(x)
  square(x) + 2*x + 1
end
puts calc_quadratic(3) #=> 16
puts calc_quadratic(5) #=> 36

Step Up! : メソッドの再帰

「メソッドの中で自分自身をを呼び出す」というメソッドを再帰メソッドといいます。
再帰を使うと、処理が簡単にかつわかりやすく書ける場合があります。
次の例は、再帰を利用して引数で受け取った数値の階乗(!)を計算するメソッドcalc_factorialを実装した例です。

calc_factorial.rb
1
2
3
4
5
6
7
8
# 実数値を入れた場合、負の値を入れた場合などの処理は入っていません。
def calc_factorial(num)
  return 1 if num == 0               # 再帰の終了条件:引数が0の場合
  return num * calc_factorial(num-1) # 自分自身に引数から1引いた数を入れる
end

puts calc_factorial(3) #=> 6
puts calc_factorial(5) #=> 120

ただし、自分自身を際限なく呼び出し続けるため、終了条件が正しく設定されていないと無限ループになってしまうことに気をつけましょう。

戻り値

戻り値とは、メソッドを実行したときそのメソッドが返す値です。
例えば、二つの数値の足し算を定義したメソッドを使用した場合、戻り値は「計算結果(和)の数値」になります。
Rubyの場合、戻り値は次のように決まります。(※言語によって異なるので注意)

  • 基本的にメソッド内で最後に実行された処理結果が戻り値になります。
  • 明示的に戻り値を指定したい場合はreturnを使用します。
return_value.rb
1
2
3
4
5
6
7
8
9
10
11
12
# メソッドの内の最後の処理結果が戻り値になる例
def greet
  #何らかの処理
  return "Hello World!" #戻り値("Hello World!"という文字列)
end

def greet2
  #何らかの処理
  "Hello World!" #最後の処理の場合、returnは省いてもよい
end
puts greet   #=> Hello World!
puts greet2  #=> Hello World!

「ある条件の時に処理を途中で中断して特定の値を返したい」というような場合には、どのタイミングでどの値を返すのか明示する必要があります。

conditional_return.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 戻り値を明示したい時の例
def sum(array)
  if array.length == 0
    return nil   #関数の途中で戻り値を指定するときはreturn必須
  end
  result = 0
  array.each do |item|
    result += item
  end
  result
end
p sum([])
#=> nil
p sum([1, 2, 3])
#=> 6

上記の例は、引数に与えた配列が空 [] であれば、戻り値は nil になります。
それ以外であれば、その配列内の要素を足した値が返ってきます。

Step Up! : 多値返却と多重代入

Rubyでは、一つのreturnで複数の値を返すことができます。この場合、返したい値を ,(カンマ) 区切りで列挙します。呼び出したメソッドから複数の戻り値を受け取る場合も同様です。
次のプログラムは、二つの値a, bを受け取り、値を入れ替えて返却するメソッドです。

return_multiple_values.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
# 二つの値を順序を入れ替えて返すメソッド
def swap(a, b)
  return b, a
end

a = 1
b = 2
puts "before swap : a = #{a}, b = #{b}"
#=> before swap : a = 1, b = 2

a_swap, b_swap = swap(a, b)
puts "after swap : a = #{a_swap}, b = #{b_swap}"
#=> after swap : a = 2, b = 1

実はこの複数の値を同時に返す処理は、Rubyの多重代入という機能が実現しています。
多重代入は今の例のように、「複数の値を複数の変数に代入」する機能です。

multiple_values_assignment.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 変数 a, b, c にそれぞれ1, 2, 3をこの順番で代入
a, b, c = 1, 2, 3
p a #=> 1
p b #=> 2
p c #=> 3

# 変数の一つに "*(アスタリスク)" をつけると、余りが配列で渡される
*a, b = 1, 2, 3
p a #=> [1, 2] ※ 余った値が配列で a に
p b #=> 3      ※ 最後の値が b に

a, *b = 1, 2, 3
p a #=> 1      ※ 最初の値が a に
p b #=> [2, 3] ※ 余った値が配列で b に

多重代入は、一貫性のない変数同士で行ってしまうとプログラムの可読性が低くなってしまう可能性もあるので、多用は禁物です。

変数のスコープ

変数には、その「プログラムが参照可能な変数の範囲」というものが決められています。
これを、変数のスコープといいます。

ローカル変数

これまで利用してきた変数は、ローカル変数と呼ばれます。
ローカル変数のスコープは、変数が宣言された場所(メソッドあるいはファイル)の中だけです。
すなわち、異なるメソッドやファイルに書かれた変数は、同じ名前であっても別の変数とみなされます。
以下のプログラムを例として、実行してみましょう。

local_variable.rb
1
2
3
4
5
6
7
8
9
10
def change_status (status)
  status = "爆睡したい"
  puts "(不思議な力で「#{status}」気分に変えらようとしている!)"
end

status = "めっちゃRubyの学習をしたい"
puts "わたしは「#{status}」気分です。"

change_status (status)
puts "それはどうかな、私は 「#{status}」気分だ !"

関数の中でstatusが書き換えられても、関数の外では関係がないことがわかったと思います。
それは、同じstatusという変数でも、スコープ(参照できる範囲)が違うので、すべて変数の宣言になっています。
関数の他にもスコープを区切るものがあります。下記の例を見てみましょう。

1
2
3
4
5
6
[1,2,3,4,5].each do |a|
  x = a
  puts "loop_x: #{x}"
end
puts "x: #{x}"
#=> scope_test.rb:6:in `<main>': undefined local variable or method `x' for main:Object (NameError)

エラーが出ましたね。
このようにブロック do ~ end{ } の中や、if ~ end で囲まれている中は別のスコープです。
※ ローカル変数以外の変数だとこれに当てはまらないことが多いので注意が必要です。

上記のプログラムをエラーが出ないよう、改修してみましょう。

scope.rb
1
2
3
4
5
6
7
8
9
10
11
12
x = 0
[1,2,3,4,5].each do |a|
  x = a
  puts "loop_x: #{x}"
end
puts "x: #{x}"
#=> loop_x: 1
#=> loop_x: 2
#=> loop_x: 3
#=> loop_x: 4
#=> loop_x: 5
#=> x: 5

今度は、do ~ end の外で x が宣言されているので、正しく参照することができました。

グローバル変数

ローカル変数と対をなす変数として、グローバル変数があります。
グローバル変数は、変数名が同一であれば、プログラム中のどこであっても必ず同じ変数として扱われます。

global_variable.rb
1
2
3
4
5
6
7
8
9
10
11
12
# グローバル変数として扱う場合は、変数名の前に$をつける
[1,2,3,4,5].each do |a|
  $x = a
  puts "loop_x: #{$x}"
end
puts "x: #{$x}"
#=> loop_x: 1
#=> loop_x: 2
#=> loop_x: 3
#=> loop_x: 4
#=> loop_x: 5
#=> x: 5

Attention! : グローバル変数の利用

グローバル変数は便利である一方、その性質上プログラムが大きくなるほど意図しない場所で改変されてしまい、プログラムが壊れる危険性が増加します。
研修中では、グローバル変数は使わないようにしましょう(すべての演習がグローバル変数を使わなくても実現できるはずです)。
実務では、それぞれのコード規約に従ってください。


演習問題

問1

円周率を3.14として、以下のメソッドを実装してください。

問1-1

半径 r を引数として、円周の長さを求めるメソッド calc_circumference を実装してください。

1
2
3
4
5
puts calc_circumference(3)
#=> 18.84

puts calc_circumference(12)
#=> 75.36

問1-2

半径 r を引数として、円の面積を計算するメソッド calc_area_of_circle を実装してください。

1
2
3
4
5
puts calc_area_of_circle(3)
#=> 28.26

puts calc_area_of_circle(7)
#=> 153.86

問1-3

半径 r と高さ h を引数として、円柱の体積を計算するメソッド calc_volume_of_cylinder を実装してください。

1
2
3
4
5
puts calc_volume_of_cylinder(3, 10)
#=> 282.6

puts calc_volume_of_cylinder(6, 12)
#=> 1356.48