高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Ruby 語法放大鏡之「attr_accessor 是幹嘛的?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

先說結論:

attr_accessor 會幫你在 Ruby 的類別裡產生一對 getter 以及 setter 方法。

不過我想這結論對新手來說有講跟沒講一樣,讓我們繼續往下看。

跟別的程式語言比起來,Ruby 可以省略很多東西,像是呼叫方法的時候可以不用小括號,回傳資料的時候有時候不用特別加 return

我們先來看一段範例:

1
2
3
4
5
6
7
class Girl
  def initialize(age)
    @age = age
  end
end

mary = Girl.new(20)

這是一個簡單的 Ruby 類別,我用 Girl 類別建一個名為 mary 的實體(instance),並且在初始化的時候就設定她的年紀為 20。Girl 類別裡有一個 @age 這個實體變數(instance variable),也許你會猜說如果要知道 mary 的年紀的話,只要:

1
puts mary.age

就行了,但一執行就會發現錯誤訊息:

undefined method `age' for #<Girl:0x007f93a609fa10 @age=20> (NoMethodError)

怪了,我是想要存取 age 這個屬性,為什麼錯誤訊息卻是 undefined method?

在解釋之前,要先說明幾個 Ruby 這個程式語言跟別家程式語言在設計上的不同:

一、Ruby 的方法呼叫,常常會適時的省略小括號:

舉個例子來說:

1
2
3
4
5
def say_hi_to(name)
  puts "hi, #{name}"
end

say_hi_to("eddie")

但我們通常會寫成:

1
say_hi_to "eddie"

在呼叫方法的時候省略小括號,這在 Ruby 是很常見的寫法。

二、Ruby 並沒有「屬性」(property/attribute)這樣的東西:

雖然 mary 看起來有一個 @age 實體變數,但不表示是可以直接存取的屬性。硬是要用 mary.age 問她年紀,或是要用 mary.age = 18 來設定她的年紀,她都會賞你一巴掌,給你錯誤訊息的。

mary.age 你以為是讀取 mary 上的 age 屬性,但事實上是在執行 mary.age() 方法,只是小括號被省略了。mary.age=18 你以為是設定 mary 的 age 屬性,但事實上是執行 mary.age=(18) 方法,只是小括號被省略了。

在 Ruby 裡,很多東西都跟你看到的不太一樣,例如,你以為 1 + 2 是簡單的數學運算嗎? 其實它是 1.+(2),它是對數字物件 1 送了一個 + 的訊息,並且把數字物件 2 當做參數傳給它。

好啦,即然知道它們都是方法,那要怎麼定義它們呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
class Girl
  def initialize(age)
    @age = age
  end

  def age
    return @age  # 這個 return 通常會省略
  end

  def age=(new_age)
    @age = new_age
  end
end

上面這段範例中,第 6 ~ 8 行的方法會回傳 @age,又常稱之 getter;第 10 ~ 12 行的方法它會設定 @age 的值,故又常稱之 setter。

不過…等一下! 為什麼方法後面有個等號?

請把等號當做一般的字母看待。在 Ruby 定義方法的時候,等號跟其它字元一樣都可以是方法名字的一部份,只是這個特殊字元必須要放在方法名稱的結尾(其實包括問號跟驚嘆號也都一樣)。

這個方法就叫做 age=,要使用它就是用 age=(18),沒錯,就是連等號一起呼叫它。所以,其實標準形態應該長這樣:

1
mary.age=(18)

又,因為前面提到,Ruby 可以省略小括號,所以可寫成:

1
mary.age=18

然後,Ruby 又有幫忙加了一些語法糖衣,讓你在中間加一些空白字元也是可以的:

1
mary.age = 18

最後就會讓你看起來像是在設定 age 屬性了。

這…會不會太麻煩了點?

照這樣說,如果每次想要用類似的屬性寫法,就必須要寫一對方法來回傳、設定,會不會有點太麻煩啊。

是的,就是要這麼麻煩。不過請放心,工程師都很懶的,所以有另外設計了幾個方法可以讓你快速的產生前面提到的 getter/setter。

如果你的 getter/setter 很單純,就只是有回傳、設定實體變數的話,那你可用 Ruby 內建的幾個方法:attr_readerattr_writer 以及 attr_accessor

1
2
3
4
5
6
7
8
9
10
11
class Girl
  attr_accessor :age
  def initialize(age)
    @age = age
  end
end

mary = Girl.new(20)
puts mary.age    #=> 20
mary.age = 18
puts mary.age    #=> 18

其中,attr_reader 只會幫你產生 getter,attr_writer 只會幫你產生 setter,而 attr_accessor 則會幫你產生 getter 及 setter。如果不相信,可以執行下面這行看一下:

1
p Girl.instance_methods(false)

應該會看到以下結果:

1
[:age, :age=]

的確是產生了兩個方法,分別是 age 以及 age=

用了 attr_accessor 還能自己寫 getter 或 setter 嗎?

當然是可以的,例如:

1
2
3
4
5
6
7
8
9
10
11
class Girl
  attr_accessor :age

  def age=(new_age)
    @age = (new_age > 18) ? 18 : new_age  # 如果大於 18,就只設定 18..
  end
end

mary = Girl.new
mary.age = 30     # 即使設定為 30 歲...
puts mary.age     # 還是會永保 18 歲 :)

因為你重新定義了 age= 方法,在執行的時候 Ruby 會跳出來跟你碎碎唸說 warning: method redefined; discarding old age=,但程式還是可執行。

所以,如果你只是想要客制化自己的 getter 或 setter 的話,可將 attr_accessor 改為 attr_readerattr_writer,就不會有這個警告訊息了。

以上,希望對大家有幫助 :)

Comments