🙆

【Ruby学習記録】クラスの基本・継承・オーバーライド

2022/09/20に公開

お題1:下記の Student クラスを拡張し、任意の名前が出力できるメソッドを定義せよ。また、Student クラスのインスタンスを作成して、定義したメソッドを呼び出せ

example.rb
class Student
end
answer.rb
class Student
  def your_name(name)
    @name = name
    puts @name
  end
end

student = Student.new
student.your_name("Masahiko")
===
$ ruby answer.rb
Masahiko

Ruby でクラスを定義したい場合、Class クラス名 ~ endで定義する。その中にdef メソッド名 ~ endで記載した内容が、そのクラスのメソッドとなる。
また、インスタンス変数は@変数名で定義する。

今回の解答では、Student クラスにdef your_name(name) ~ endで your_name メソッドを作成しており、your_name メソッド内で@name = nameとすることで、受け取った値をインスタンス変数の@nameに設定している。

お題2:下記プログラムの不具合を解消し、export メソッドを呼び出して値が出力される様に間違いを訂正せよ

現状

Text クラスが存在し、そのクラスのインスタンスを作成して export メソッドで値を出力しようとしたが、上手くいかなかった。

example.rb
class Text
  def save(text)
    text = text
  end
  def export
    puts text
  end
end

text = Text.new
text.export
text.save("このテキストが出力できたら正解")

この場合、何が問題かというと、まず save メソッドの方。text = textという記述があるが、ここでのtextはあくまでも save メソッドの引数として受け取ったローカル変数のtextということになる。それを自分自身に再代入しているだけなので、こういう書き方をしても Text クラスのインスタンス変数には何も残らない。

また、export メソッドのputs textの箇所も、ここで書いたtextという変数はどこでも定義されていない未定義の変数となってしまうので、これは実行時エラーとなる。

answer.rb
class Text
  def save(text)
    @text = text
  end
  def export
    puts @text
  end
end

text = Text.new
text.save("このテキストが出力できたら正解")
text.export
===
$ ruby answer.rb
このテキストが出力できたら正解

まず、save メソッドで、ちゃんとインスタンス変数として設定できるようにするために、@text = textとする。
こうすることで、save メソッドの引数textの値をインスタンス変数@textに代入できるので、他のメソッドでこのインスタンス変数を参照できるようになる。

そのうえで、export メソッドの方もputs @textと変更すれば、save メソッドで設定したインスタンス変数の値を出力できる。

なお、この後のお題で登場するattr_accessorattr_readerというメソッドを使った場合、export メソッドの方は修正しなくてもよくなる。
*save メソッド以外でtextと書いた場合、セッター/ゲッターメソッドで@textを参照していることになる。

answer.rb
class Text
  attr_accessor :text
  def save(text)
    @text = text
  end
  def export
    puts text
  end
end

ただ、個人的にはattr_accessorattr_readerを使った場合でも、export メソッドも修正して、一見でインスタンス変数を参照していると分かるようにしておきたい。

attr_accessorattr_readerattr_writerはいわゆるゲッター/セッターメソッドを簡単に定義するためのテクニックなので、「外部からインスタンス変数にアクセスできるようにするためのもの」と理解している。
なので、attr_accessorメソッドによって、クラス定義内でゲッター/セッターが使えると言われてもちょっと腹落ちしにくいし、インスタンス変数なのか、ローカル変数なのか見分けが付かなくなるのが気持ち悪い。

他の言語だと、this とか self といったキーワードによって、変数のスコープを明らかにしているものもあるし、変数のスコープが分かりにくいのは不具合のもとになりやすい。
それ以前に、同じ変数名の使いまわしも不具合や混乱のもとになりやすいので、本来なら save メソッドの引数とインスタンス変数で名前も変えておいた方が無難だろう。

お題3:下記の様に Dog クラスが存在する。

example.rb
class Dog
  attr_accessor :kind

  def initialize(kind)
    @kind = kind
  end

end

お題3-1:このクラスのインスタンス作成時に任意の犬種名(kind)を初期値として渡し、その値を参照して出力せよ。

answer.rb
class Dog
  attr_accessor :kind
  def initialize(kind)
    @kind = kind
  end
end

shiba = Dog.new("柴犬")
puts shiba.kind
===
$ ruby answer.rb
柴犬

お題3-1では、クラスの定義は特にいじる必要はないので、Dog クラスのインスタンスを作って初期値を設定し、その値を参照しましょう、ということ。

Ruby でクラスのインスタンスを生成したい場合、クラス名.newというメソッドでインスタンスを生成する。

で、Ruby の場合、new したときに動く、いわゆる「コンストラクタ」を定義したい場合は、initializeメソッドを定義しましょう、ということになっている。

今回の Dog クラスはinitializeメソッドにkindという引数が定義されていて、その値をインスタンス変数に設定するようになっている。
なので、shiba = Dog.new("柴犬")と書くことで、shibaという名前の変数で Dog クラスのインスタンスが生成され、さらに@kindのインスタンス変数に「柴犬」という値が入った状態になる。

さらに、今回は生成したshibaインスタンスに設定された値を参照しなさい、ということだが、それを出来るようにするために、クラスの定義側でattr_accessor :kindという記述を行っている。

基本的に、Ruby のインスタンス変数はそのままだとインスタンスの外から直接アクセスが出来ない。
そのため、外部からインスタンス変数にアクセスしたい場合にはセッター/ゲッターメソッドを定義する必要があるのだが、それを簡略化するのがattr_accessorattr_readerattr_writerメソッドとなっている。

今回は、お題に最初からattr_accessorメソッドが定義されているので、shibaインスタンスを作成した後であれば、shiba.kind@kindのインスタンス変数にアクセスできるようになる。

お題3-2:Dog クラスを拡張し、下記の対応を行うこと

  • name プロパティを initialze メソッドに定義
  • インスタンス作成時に初期値として犬種名(kind) + 任意の名前(name)を渡す
  • インスタンス作成後に各プロパティを参照して出力
answer.rb
class Dog
  attr_accessor :kind
  attr_accessor :name
  def initialize(kind: , name: )
    @kind = kind
    @name = name
  end
end

shiba = Dog.new(kind: "柴犬", name: "ポチ")
puts shiba.kind
puts shiba.name
===
$ ruby answer.rb
柴犬
ポチ

まず「name プロパティを initialze メソッドに定義」ということなので、initialize メソッドの引数を追加し、追加した引数を@nameのインスタンス変数に代入してやればよい。
解答ではinitialize(kind: , name: )とキーワード引数にしているが、これは必須ではない。
ただ、複数の引数がある時には、積極的にキーワード引数を使った方がプログラムの可読性は良くなる。

次に「インスタンス作成時に初期値として犬種名(kind) + 任意の名前(name)を渡す」の対応だが、これはインスタンスの作成時の記述を変える。
キーワード引数を使うように変えたので、Dog.new("柴犬")からDog.new(kind: "柴犬", name: "ポチ")という記載に変わる。

キーワード引数を使った場合、呼び出す側のコードの記述量がどうしても増えてしまうが、そこは記述量を優先するのか、可読性を優先するのかで決めたらよいと思う。

最後に「インスタンス作成後に各プロパティを参照して出力」だが、これはattr_accessor:nameを追加すれば OK。
解答では 2 行に分けているが、attr_accessor :kind, :nameでも可。
そのうえで、puts shiba.kindputs shiba.nameとすれば、それぞれのプロパティ(=インスタンス変数)を出力することが出来る。

お題4:下記の様に Car クラスが存在する。

example.rb
class Car
  def accelerate
    puts "進む"
  end

  def brake
    puts "止まる"
  end
end

お題4-1:Car クラスを継承した AdvancedCar クラスを定義して、メソッド実行時に "緊急時に止まる"と出力される automatic_braking というメソッドを定義せよ

お題4-2:AdvancedCar クラスを以下のように動作するようにコードを追加せよ

advanced_car = AdvancedCar.new
advanced_car.accelerate
=> "進む"
advanced_car.brake
=> "パッと止まる"
answer.rb
class Car
  def accelerate
    puts "進む"
  end

  def brake
    puts "止まる"
  end
end

class AdvancedCar < Car
  def automatic_braking
    puts "緊急時に止まる"
  end
  def brake
    puts "パッと止まる"
  end
end

advanced_car = AdvancedCar.new
advanced_car.accelerate
advanced_car.brake
advanced_car.automatic_braking
===
$ ruby answer.rb
進む
パッと止まる
緊急時に止まる

まず、クラスの継承だが、Ruby ではclass クラス名 < 継承元クラス名とすると、クラスを継承することが出来る。
今回は「Car クラスを継承して AdvancedCar クラス」なので、class AdvancedCar < Carとしている。

後は、通常のクラス定義と同じ要領なので、AdvancedCar クラスの定義の中でautomatic_brakingメソッドを定義すれば OK。

お題4-2はいわゆる「オーバーライド」というもので、継承元のクラスと同名のメソッドを定義した場合は上書きされて継承先のクラスで定義したものが優先させる、というもの。

今回は、クラスの定義を一通り行った後、AdvancedCar クラスのインスタンスを作って、一連の処理を行っている。
それぞれ見ていくと、以下のようになる。

# AdvancedCarクラスのインスタンスを生成
advanced_car = AdvancedCar.new
advanced_car.accelerate
# => accelerateメソッドはCarクラス(継承元)にだけ定義されているのでそちらを実行
advanced_car.brake
# => accelerateメソッドはCarクラス(継承元)、AdvancedCarクラスの両方に定義されている。
#    オーバーライドによって、継承先が優先されているので継承先のメソッドを実行
advanced_car.automatic_braking
# => automatic_brakingメソッドはAdvancedCarクラスにだけ定義されているのでそちらを実行
GitHubで編集を提案

Discussion