ひがきの日記

2012-07-23

演習問題回答例

演習問題1

属性として身長と体重を追加しよう。体重は秘密にしよう。

仕様を決める。

  • initialize で height, weight を渡す
  • 省略時は nil
class Person
  @@variables = [:@name, :@born, :@height, :@weight]

  def initialize name, born = nil, height = nil, weight = nil
    @name, @born, @height, @weight = name, born, height, weight
  end

  attr_accessor :height
  attr_writer :weight

  def hash
    @@variables.map{|var| instance_variable_get(var)}.hash
  end

  def eql? o
    @@variables.all? do |var|
      instance_variable_get(var).eql? o.instance_variable_get(var)
    end
  end
end

属性が増えたので、それに合わせて hash, eql? も変更した。


演習問題2

BMI を計算するメソッドを追加しよう。
BMI = ¥frac{w}{t^2}
w = 体重[kg]
t = 身長[m]

仕様を決める。

  • height は Float で、単位は cm
  • weight は Float で、単位は kg
  • @height, @weight は Float でなくても、あるていど動くようにする
  • 計算できない場合は NaN を返す
class Person
  def bmi
    Float(@weight) / (Float(@height) / 100.0) ** 2
  rescue
    Float::NAN
  end
end

演習問題3

Person#<=> を書き直そう。
その妥当な仕様は?
p0 = Person.new('matz')
p1 = Person.new('Matz', Time.local(1965, 4, 14))

p0 <=> p1   # => ?

仕様を決める。

  • 単純に全ての属性に対して <=> を評価する
  • 比較対象が Person のインスタンスでなければ nil を返す
class Person
  def <=> o
    return nil unless o.kind_of? Person

    @@variables.each do |var|
      lhs = instance_variable_get(var)
      cmp = lhs <=> o.instance_variable_get(var)

      next if cmp == 0

      return cmp unless cmp.nil?
      return  -1 if lhs.nil?
      return   1
    end

    0
  end
end

演習問題4

クラス Person には、いくつかのバグがある。 それを見つけ出して修正しよう。

バグを探す。

  • @born に strftime メソッドがないと age で例外が発生する
    • 計算できなければ nil を返す
  • to_s が age を使用しているので、以下同文
    • age が nil なら、@name のみとする
class Person
  def age
    (Time.now.strftime('%Y%m%d').to_i -
     @born.strftime('%Y%m%d').to_i) / 1_00_00
  rescue
    nil
  end

  def to_s
    if a = age
      "#{name}(#{a})"
    else
      name
    end
  end
end

こればバグか?

matz = Person.new('matz')
matz.freeze
matz.name                       # => "matz"

matz.name.upcase!
matz.name                       # => "MATZ"

グループワークのときに質問してみたら、みんなこれは放置しているそうだ。

あえて対応するなら、

class Person
  def name
    @name.dup
  end
end

matz.name.upcase!       # => "MATZ"
matz.name               # => "matz"

間違いがあれば、指摘くださると助かります。

2012-07-21

第55回 Ruby/Rails 勉強会@関西で初級者向けレッスンやってきた

初級者向けレッスンを担当したので、以下スライドを解説。*1 *2

クラスとは

f:id:mas-higa:20120722174048p:image

クラスを作ってみよう

class Person; end

obj = Person.new
  # => #<Person:0x10102718>

obj.class           # => Person
Person.superclass   # => Object

f:id:mas-higa:20120722174047p:image

  • Person という名前のクラスを作る。
  • class から end までがクラス。
  • クラス名は大文字で始める (キャメルケース)

属性を持たせてみよう

class Person
  def initialize name
    @name = name
  end
end

matz = Person.new('matz')
  # => #<Person:0x10138598 @name="matz">

f:id:mas-higa:20120722174045p:image

属性にアクセスしてみよう

class Person
  attr_reader :name
end

matz.name       # => "matz"

f:id:mas-higa:20120722174044p:image

attr_writer
セッター
attr_reader
ゲッター
attr_accessor
セッター&ゲッター

変数・定数のおさらい

ローカル変数
person
インスタンス変数
@person
クラス変数
@@person
グローバル変数
$person
定数
Person

f:id:mas-higa:20120722174042p:image

  • 最初の数文字を見れば区別できる
  • クラス名は定数

属性を増やしてみよう

class Person
  def initialize name, born = nil
    @name, @born = name, born
  end
  attr_accessor :born
end

matz.methods.map(&:to_s).grep(/born/)
  # => ["born", "born="]

f:id:mas-higa:20120722173746p:image

属性を増やしてみよう (2)

  • アクセスしてみる
matz.born = Time.local(1965, 4, 14)
dhh = Person.new('dhh',
        Time.local(1979, 10, 15))

matz.born
  # => 1965-04-14 00:00:00 +0900
dhh.born
  # => 1979-10-15 00:00:00 +0900

f:id:mas-higa:20120722173744p:image

  • 代入のような表記で born= メソッドが呼ばれる

メソッドを作ってみよう

class Person
  def age
    (Time.now.strftime('%Y%m%d').to_i -
     @born.strftime('%Y%m%d').to_i) /
      10000
  end
end
matz.age    # => 47
dhh.age     # => 32

f:id:mas-higa:20120722173743p:image

メソッドを上書きしてみよう

matz.to_s  # => "#<Person:0x10138598>"

class Person
  def to_s
    "#{@name}(#{age})"
  end
end
matz.to_s  # => "matz(47)"
dhh.to_s   # => "dhh(32)"

f:id:mas-higa:20120722173741p:image

インスタンスを比較してみると……

person = Marshal.load(Marshal.dump matz)

person == dhh   # => false
person == matz  # => false # おかしい

f:id:mas-higa:20120722173739p:image

  • 深いコピー
    • Marshal.dump して Marshal.load する
  • == はメソッド
    • Person#== を定義しても良いが……
    • ==, !=, >, <, >=, <= 別々に定義するの面倒

順序を決めよう

class Person
  include Comparable
  def <=> o
    @name <=> o.name
  end
end
person == matz  # => true
person == dhh   # => false
matz > dhh      # => true

f:id:mas-higa:20120722173622p:image

  • <=> 演算子で順序を決定
  • Comparable で ==, != , >, <, >=, <= を生成

Array#sort してみよう

  • 順序が決まれば sort できる
people = [matz, dhh]

people.sort  # => [dhh(32), matz(47)]

f:id:mas-higa:20120722173621p:image

Array ときたら、次は ……

Hash のキーにしてみると……

値に入れてもおもしろくないので

h = {matz => "Ruby", dhh => "Rails"}

h[matz]     # => "Ruby"
h[dhh]      # => "Rails"

key = Marshal.load(Marshal.dump matz)

key == matz # => true
h[key]      # => nil # おかしい

f:id:mas-higa:20120722173619p:image

  • 深いコピーをしたオブジェクトをキーにすると
    • 値が取り出せない!

hash 値を計算しよう

class Person
  def hash
    code = 17
    code = 37 * code + @name.hash
    code = 37 * code + @born.hash
  end
end
matz.hash   # => -22068619118
dhh.hash    # => 14923733106

f:id:mas-higa:20120722173617p:image

  • 計算方法はハチドリ本に書いてあった*4
    • 出所は Effective Java らしい

eql? を上書きしよう

  • hash 値がぶつかっていないか調べる必要がある
  • Hash クラスは eql? でキーが正しいか判断する
class Person
  def eql? o
    return false unless @name.eql? o.name
    return false unless @born.eql? o.born
    true
  end
end
key.eql? matz   # => true
key.eql? dhh    # => false

f:id:mas-higa:20120722173616p:image

  • a.eql? b が真の場合 a.hash == b.hash であること
    • そのように hash, eql? を定義する

Hash にアクセスしてみよう

h = {matz => "Ruby", dhh => "Rails"}

h[matz]     # => "Ruby"
h[dhh]      # => "Rails"

h[key]      # => "Ruby"

f:id:mas-higa:20120722173429p:image

  • 最初に作った h (Hash) は Person#hash を定義する前に作成したもの
  • 格納位置がおかしいので作り直す*5

等値性のおさらい

==
内容が等しいか?
===
case 式で使用
eql?
Hash クラスが使用
equal?
同一オブジェクトか?

f:id:mas-higa:20120722173428p:image

アクセス制御してみよう

オマケ

  • public
  • protected
  • private
class Person
  protected :born
end
matz.born
# ~> protected method `born' called for matz(47):Person (NoMethodError)

f:id:mas-higa:20120722173426p:image

  • protected なメソッドを呼ぶと例外が発生する

今日 話さなかったこと

  • Range の始点と終点

f:id:mas-higa:20120722173425p:image

上記を調べて初級者を抜け出そう。

まとめ

f:id:mas-higa:20120722173423p:image

以上、クラスを作る際の決まり事を紹介した。

*1:スライドだけ欲しい人は直接どうぞ。http://higaki-it.jp/ruby/55/slide.pdf

*2:コードが必要な人は gist からどうぞ。

*3Kernel モジュールで定義されている

*4:会場の @nayutaya さんによると [@name, @born].hash とするのが一般的らしい

*5:会場の @no6v さんによると h.rehash するといいらしい