開放類別 <> 這個星期的我很可以!不管裡面有什麼都給我來一點吧!

[Day20] 接下來要隆重登場的是,鋼!鐵!將!軍!

嗨大家好!這篇要介紹的是開放類別(Open Class)!

過去幾天,我已經學會了類別、繼承、模組、實體方法與類別方法的差異,以及方法存取的限制。而今天再來看一些有趣的東西吧!


類別—合體!

身為一隻金魚,我常常在 coding 時忘記自己有沒有寫過這個類別,一不小心就做出了兩個相同名稱的類別,但裡面各自定義的方法不同:

1
2
3
4
5
6
7
8
9
10
11
class Robot
def eye
puts "發射超強激光!"
end
end

class Robot
def arm
puts "發射拳頭砲彈!"
end
end

這時候如果又做了一個實體 franky 出來:

1
franky = Robot.new

那這個實體 franky 到底是從哪個類別產生的呢? 不是很確定,
那讓它呼叫實體方法看看:

1
2
3
4
5
6
franky.eye
franky.arm

# 印出
發射超強激光!
發射拳頭砲彈!

咦!franky 居然兩招都可以用!
難道是…?

事實上,Ruby 會把我們寫的東西看成這樣:

1
2
3
4
5
6
7
8
9
class Robot
def eye
puts "發射超強激光!"
end

def arm
puts "發射拳頭砲彈!"
end
end

居然合體啦!!!只要是同名的類別,各自定義的方法也會融合在一起!
(同名方法自然另當別論)


類別裡面什麼都不用寫也有方法可以用

之前介紹時好像沒提到 Ruby 這個有趣的特性,就拿剛剛的 Robot 做例子吧!

1
2
3
4
class Robot
end

franky = Robot.new

現在我自己定義了一個新的類別 Robot ,可是裡面真的是超~空~一個方法都沒有。
然後 franky 能呼叫什麼方法嗎?應該不行吧?

還是來確認一下好了:

1
2
3
4
franky.methods

# 印出
[:dup, :itself, :yield_self, :then, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :clone, :display, :hash, :class, :singleton_class, :method, :public_send, :public_method, :singleton_method, :define_singleton_method, :extend, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :nil?, :eql?, :respond_to?, :freeze, :inspect, :object_id, :send, :to_s, :__send__, :!, :==, :__id__, :!=, :equal?, :instance_eval, :instance_exec]

哇啊啊一堆方法!這些是哪裡來的?是誰偷駭進了我的電腦嗎?
不對不對這一定是在做夢,我要來測試一下!

Q1: franky 是什麼類別?

1
2
franky.class
=> Robot

通過第一關…沒關係再來!

Q2: franky 是整數嗎?

1
2
franky.is_a?(Integer)
=> false

好我知道很明顯不是…

啊!等等!就連一開始找方法的:

1
franky.methods

這個 methods 也不是我自己定義的啊!虧我還用得這麼順手!

不管是原生類別還是自定義類別,其實 Ruby 都已經先準備好很多方法著大家去使用了!這部分的原因有興趣的朋友可以先上網查 Ruby 的 Kernel 模組,有時間我會再來仔細說明!


讓我們來加點料

由於這個特性,我們就能為原生類別再添加一些自定義的方法,這個技巧也是 開放類別(Open Class) 的精髓。

譬如 Ruby 原生的 String 類別,裡面已經定義好很多給字串使用的方法:

1
2
3
4
5
6
a = String.new

a.methods

# 印出
[:unicode_normalize, :unicode_normalize!, :ascii_only?, :to_r, :unpack, :unpack1, :encode!, :%, :include?, :*, :+, :count, :partition, :+@, :-@, :<=>, :<<, :to_c, :==, :===, :sum, :=~, :next, :[], :casecmp, :casecmp?, :insert, :[]=, :match, :match?, :bytesize, :empty?, :eql?, :succ!, :next!, :upto, :index, :rindex, :replace, :clear, :chr, :getbyte, :setbyte, :scrub!, :scrub, :undump, :byteslice, :freeze, :inspect, :capitalize, :upcase, :dump, :downcase!, :swapcase, :downcase, :hex, :capitalize!, :upcase!, :lines, :length, :size, :codepoints, :succ, :split, :swapcase!, :bytes, :oct, :prepend, :grapheme_clusters, :concat, :start_with?, :reverse, :reverse!, :to_str, :to_sym, :crypt, :ord, :strip, :end_with?, :to_s, :to_i, :to_f, :center, :intern, :gsub, :ljust, :chars, :delete_suffix, :sub, :rstrip, :scan, :chomp, :rjust, :lstrip, :chop!, :delete_prefix, :chop, :sub!, :gsub!, :delete_prefix!, :chomp!, :strip!, :lstrip!, :rstrip!, :squeeze, :delete_suffix!, :tr, :tr_s, :delete, :each_line, :tr!, :tr_s!, :delete!, :squeeze!, :slice, :each_byte, :each_char, :each_codepoint, :each_grapheme_cluster, :b, :slice!, :rpartition, :encoding, :force_encoding, :valid_encoding?, :hash, :unicode_normalized?, :encode, :clamp, :between?, :<=, :>=, :>, :<, :dup, :itself, :yield_self, :then, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :clone, :display, :class, :singleton_class, :method, :public_send, :public_method, :singleton_method, :define_singleton_method, :extend, :to_enum, :enum_for, :!~, :nil?, :respond_to?, :object_id, :send, :__send__, :!, :__id__, :!=, :equal?, :instance_eval, :instance_exec]

現在我們來把幫 String 類別加上一個 say_hello 方法:

1
2
3
4
5
6
7
8
9
10
11
class String
def say_hello
"晚安, 接下來由#{self}來帶您學習 Ruby~"
end
end

"娜美".say_hello
=> "晚安, 接下來由娜美來帶您學習 Ruby~"

"布魯克".say_hello
=> "晚安, 接下來由布魯克來帶您學習 Ruby~"

這樣的話,剛剛 a.methods 列出來的方法裡,就會多一個我們剛剛定義的 sayhello 方法囉!

另外,在這裡的 self 指的就是呼叫方法的 receiver 本身。


Monkey Patch! 既有的方法居然也能改!

除了像剛剛那樣幫類別添加方法,我們甚至也能覆蓋掉 Ruby 原先寫好的方法,這樣的行為通常被稱作 “Monkey Patch“,這就是 Ruby 語言的一大特點:可動態修改

1 + 1 = 2 是數學上顛撲不破的真理,但我們來看個例子:

1
2
1 + 1
=> 2

上次有提過在 Ruby 裡,其實是這樣運作:

1
2
1.+(1)
=> 2

當我們知道其實是 1 這個物件呼叫了 + 方法,再帶入 1 這個參數後,就可以自己來改寫 + 方法:

1
2
3
4
5
6
7
8
1.class
=> Integer

class Integer
def +(n)
return n
end
end

我讓 + 這個方法變成只會回傳加上去的數字,所以這時候再使用 + 的話,就會變成:

1
2
3
4
5
6
7
8
puts 1 + 1
puts 2 + 1
puts 3 + 1

#印出
1
1
1

得到的通通都是 1 了!數學已死!!!


今天終於來到 20 天了!開放類別就先介紹到這邊啦!後面會再提到類似的概念可能就要到淺談 Ruby 物件導向的篇章了!希望能順利完賽!

最後送大家一句金句:
投資一定有風險基金投資有賺有賠使用 Monkey Patch 前應詳閱 Ruby 官方文件說明書

話已帶到,使用時請小心、保重!

符號 <> 看!又是一塊歷史正文!讀懂這些,通往 ONEPIECE 的路就不遠了!

[Day16] 但是歷史正文上的符號真是複雜難懂啊…

哈囉各位晚安,今天要介紹的是**符號(Symbol)**!

大家對符號應該不陌生,畢竟每天傳訊息都會用到啊!
????????????????????? 啊不是這是表情符號…


符號是什麼?

在 Ruby 裡,符號是一種「帶有名字的物件」。
會用冒號 : 開頭來表示符號,同時符號也具有唯一性(immutable),一旦被建立後就無法改變。

最容易看到符號的地方就是在雜湊(Hash)了!我們經常使用符號來表示雜湊裡的鍵(key):

1
2
3
4
5
hash = {
:cat => 1,
:dog => 2,
:man => 3
}

也可以寫成這樣:

1
2
3
4
5
hash = {
cat: 1,
dog: 2,
man: 3
}

另外,符號也可以拿來指稱方法:

1
2
3
4
def call_me
puts "打電話給我!"
end
=> :call_me

符號跟變數有什麼不同?

首先,雖然看起來和變數超級像,但符號它不是「變數」而是「值」。

如果我們把符號當作變數一樣在用:

1
2
3
4
5
6
7
8
9
10
11
# 把字串指定給符號
:name = 1

=> SyntaxError ((irb):15: syntax error, unexpected '=', expecting end-of-input)
:name = 1

# 把字串指定給符號
:name = "Hi"

=> SyntaxError ((irb):16: syntax error, unexpected '=', expecting `end')
:name = "Hi"

Ruby 會告訴你這樣的語法是錯誤的!因為按照正常的程式邏輯,我們不能把一個值指定給另一個值。


一個特定符號只有一個 Object_id

任何一個符號都有著自己的記憶體位置,並始終指向那個位置,因此會較省記憶體。比起字串,用符號來當作雜湊的鍵(key),存取效能的表現會更好。

為什麼存取效能更好?原因是當程式在比較兩個物件時,若是符號,程式會直接比對 object_id 是否相同。然而在比對字串時,則是會從第一個字元開始,逐一比對是否相同。因此,在效能上字串的時間複雜度會隨著字元數量增加而提升,但符號就沒有這樣的問題。

接著我們來看看字串和符號的差別:

1
2
3
4
5
6
7
8
9
10
11
12
13
"hi".object_id
=> 180
"hi".object_id
=> 200
"hi".object_id
=> 220
"hi".object_id
=> 240

:call_me.object_id
=> 2034908
:call_me.object_id
=> 2034908

然而,符號用太多而沒有歸還記憶體時,會出現 記憶體流失(Memory leak) 的問題(簡單來說,是一種在設計上造成的記憶體浪費)


其他像是數字或 truefalsenil 的 object_id 也會變動嗎?

數字的 object_id 是固定的,被放在記憶體的奇數位置上。譬如數字 n ,位置就是 2n+1:

1
2
3
4
5
6
7
8
9
10
1.object_id
=> 3
2.object_id
=> 5
3.object_id
=> 7
5.object_id
=> 11
8.object_id
=> 17

truefalsenil 則是在記憶體的偶數位置,也是固定的:

1
2
3
4
5
6
true.object_id
=> 20
false.object_id
=> 0
nil.object_id
=> 8

關於 Ruby 的符號今天就介紹到這邊啦!希望之有時間的話能再提一些自己在實際運用上的案例及理解,謝謝大家的觀看!

存取限制 <> 原來是海樓石!惡魔果實能力者的剋星

https://ithelp.ithome.com.tw/upload/images/20200923/20128363iznYQbH4HN.jpg

[Day15] 再厲害的能力者一碰到海樓石就會喪失戰鬥力!

今天要和大家介紹 Ruby 裡的存取控制(Access control)

先來看看維基百科怎麼說:

存取控制 是指允許或禁止某人使用某項資源的能力。

聽起來是不是超強!

不過在 Ruby 裡,存取控制其實就是方法的使用層級。當我們在類別裡定義方法時,還可以再更明確指出這個方法是只有誰才可以取用,共分為三種:

  • public
  • private
  • protected

Public

如果沒有特別寫是哪種存取方式,就會預設用 public 來存取我們定義的方法。

在撰寫存取控制的層級時,與方法定義前綴詞同一行即可:

1
2
3
4
5
6
class Cafe
public # 通常直接省略不寫
def coffee
puts "來杯西西里拿坡里左岸八里漂浮拿鐵!"
end
end

換句話說,平常不寫 public 也沒關係,除非你在定義了 private 方法後又要定義 public 方法才要特別加上去,不過通常我們是不會這樣做的,後面會提到原因。


Private

private 則是指所定義的方法不能有明確的訊息接收者(receiver),意思就是不能用.來呼叫方法

像是 puts 就是一種很常見的 private 方法,不過我們自己定義一個 private 方法 seasoning 試試看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cafe
def coffee
puts "來杯西西里拿坡里左岸八里漂浮拿鐵!"
end

private
def seansoning
puts "還要有絕妙的香氣!"
end
end

my_store = Cafe.new

my_store.coffee
my_store.seasoning

# 印出
來杯西西里拿坡里左岸八里漂浮拿鐵!
NoMethodError (undefined method `seasoning' for #<Cafe:0x00007f903c945538>)

我開的咖啡廳裡面沒有絕妙的香氣!


private 寫的方法要用在哪裡?

看來 private 的方法沒辦法像之前的方式直接呼叫,那它到底是用在哪裡呢?事實上,Ruby 裡用 private 寫的方法是拿來在類別內部呼叫的,像是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cafe
def coffee
puts "來杯西西里拿坡里左岸八里漂浮拿鐵!"
seasoning
end

private
def seasoning
puts "還要有絕妙的香氣!"
end
end

my_store = Cafe.new

my_store.coffee

# 印出
來杯西西里拿坡里左岸八里漂浮拿鐵!
還要有絕妙的香氣!

private 方法在子類別也可以存取

在 Ruby 裡,private 寫的方法不只在類別內部可以存取,繼承自這個類別的子類別也一樣可以!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Father
private
def heritage(gift)
puts "#{gift} 是我留給兒子的財產!"
end
end

class Son < Father
def initialize(item)
heritage(item)
puts "謝謝老爸!"
end
end

Son.new("PS5")

# 印出
PS5 是我留給兒子的財產!
謝謝老爸!

我也想要有個這樣的老爸(羨慕)


來看看 private 方法有哪些!

可以用 self.class.private_methods.sort 來找 :

1
2
self.class.private_methods.sort
=> [:Array, :Complex, :DelegateClass, :Float, :Hash, :Integer, :Rational, :String, :URI, :__callee__, :__dir__, :__method__, :`, :abort, :at_exit, :binding, :block_given?, :caller, :caller_locations, :catch, :eval, :exec, :exit, :exit!, :extended, :fail, :fork, :format, :gem, :gem_original_require, :gets, :global_variables, :included, :inherited, :initialize, :initialize_clone, :initialize_copy, :initialize_dup, :irb_binding, :iterator?, :lambda, :load, :local_variables, :loop, :method_added, :method_missing, :method_removed, :method_undefined, :open, :p, :prepended, :print, :printf, :private, :proc, :protected, :public, :putc, :puts, :raise, :rand, :readline, :readlines, :remove_const, :require, :require_relative, :respond_to_missing?, :select, :set_trace_func, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined, :sleep, :spawn, :sprintf, :srand, :syscall, :system, :test, :throw, :trace_var, :trap, :untrace_var, :using, :warn]

發現了嗎? privatepublic都是 private 方法!


其實也不是這麼 private

那… private 裡面的方法是不是就再也無法拿出來用呢?其實也未必。我們可以用 .send() 來取出 private 方法,只是這樣會破壞封裝的原則,非不得已建議不要這樣做。

1
2
3
4
5
6
7
8
9
10
11
12
class Man
private
def whisper
puts "這是只有 private 才聽得到的悄悄話..."
end
end

man = Man.new
man.send(:whisper)

# 印出
這是只有 private 才聽得到的悄悄話...

果然成功拿出來了!


Protected

這是一個既不 public 又不 private 的東西。

protectedprivate 最大的不同,是 protected 可以允許在呼叫時前面有 receiver,這種承繼自 SmallTalk 的設計理念是 Ruby 的一大特點,不過可能之後才會詳細介紹這段。

但由於實際上現在幾乎沒有人在用 protected 寫 Ruby 的存取控制,就讓它慢慢地成為時代的眼淚吧!

1
2
3
4
5
6
7
8
9
10
11
class Smalltalk

def say_hello_to_myself
self.hello
end

protected
def hello
puts "murmur..."
end
end

如果是用 protected,那麼在內部呼叫時要寫 self.hellohello 都可以,但改成寫 privateself.hello 就會出錯了。


存取控制的另一種寫法

也可以這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Gem
def ruby
end

def emerald
end

def diamond
end

protected :emerald
private :diamond
end

這麽一來,emerald 就變成了 protected 方法,而 diamond 成為了 private 方法


總結

publicprotectedprivate 的特點有三:

  1. 類別內部都可以呼叫 3 種類型的方法。
  2. 只有 public 方法能在外部被呼叫。
  3. 正常情況下 private 的方法不允許有 receiver, protected 則沒有這種限制。不過用 send() 可以取消 private 原本的限制。

在實際撰寫時,我們會把 private 的方法定義在類別的最下方,這樣就能知道 private 上方的通通都是 public 的方法,也能更輕鬆地閱讀程式碼。


關於 Ruby 方法的存取控制,今天就介紹到這邊啦!

參考來源:
高見龍 - Public, Protected and Private Method in Ruby

類別方法 <> 雜魚們!為革命軍獻出你們的心臟吧!

https://ithelp.ithome.com.tw/upload/images/20200923/20128363fMf7riXQNQ.jpg

[Day14] 大家認識這位嗎?革命軍「東軍」軍隊長-貝洛貝蒂,她的鼓舞果實能力可以讓周遭的人全都變成有戰鬥力的單位,簡直就是類別方法啊!

在看完昨天有趣的模組後,今天想要重新回到類別(Class)這個大主題,來跟大家介紹類別方法和實體方法的差異。

那就開始吧!


實體方法

顧名思義,實體方法(instance method)就是作用在實體上的方法。基本上,類別裡面一般都會是實體方法,但我這邊還是重新快速帶一下觀念。

舉個例子,像是知名的電商網站叫 Shopee,在這個網站註冊的會員,都可以使用 Shopee 提供的服務:購買商品、上架販售等等。

我們來看看程式會怎麼寫上面這段話:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Shopee
def buyer
puts "成為買家,在雙11掃光所有的貨!"
end

def seller
puts "成為賣家,所有的錢都進來我的口袋!"
end
end

me = Shopee.new

me.buyer
me.seller

# 印出
成為買家,在雙11掃光所有的貨!"
成為賣家,所有的錢都進來我的口袋!

類別方法

要定義類別方法有好幾種方式,我最常用的是在方法名稱的前加上 self.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Army
def fight # 實體方法
puts "我要打倒天龍人!"
end

def self.inspire # 類別方法
puts "全部的人都起身戰鬥!"
end
end

soldier = Army.new

soldier.fight
Army.inspire

# 印出
我要打倒天龍人!
全部的人都起來戰鬥!

如果要定義很多類別方法,也可以這樣寫:

1
2
3
4
5
6
7
class Army
class << self # 這行以下寫的方法就是類別方法,前面不用再加 self
def all
# ...
end # 一直到這
end
end

為什麼要有類別方法?什麼時候用?

通常我們要直接請類別幫忙時,就會使用類別方法。換句話說,當我們確定這個方法和特定實體不會有關係,而是要在整個類別操作,就會考慮使用類別方法。


初始化

最後,來向大家介紹一個特別的方法「初始化(initialize)」這又是什麼?很像是重灌電腦時才會聽到的單字,是不是覺得有點危險呢?

其實在 Ruby 的類別裡, initialize 指的是:在類別產生實體當下就會立刻執行的方法,也可以想成是寶寶一出生就會做的第一件事(?)

1
2
3
4
5
6
7
8
class Baby
def initialize
puts "Hello World(crying!!!)"
end
end

baby = Baby.new
Hello World(crying!!!)

Wow!寶寶一出生就會 Hello World 了!看來絕對是當工程師的料啊!
(喂!寶寶在哭了!!!)


今天就先到這邊了!希望大家都有看懂類別方法在做什麼,也希望五倍的大家可以順利完賽!(信心喊話)

參考資料:
蒼時弦也 - 自由的 Ruby 類別(一)

模組 <> 想得到夢寐以求的惡魔果實能力嗎?先裝一個吧!

https://ithelp.ithome.com.tw/upload/images/20200921/20128363w0Jr8PDGKz.jpg

[Day12] 啊你說這個不能吃是嗎?


哈囉!今天要跟大家介紹的是**模組(Module)**。

在 Ruby 的世界裡,模組是一個特殊又好用的東西,它就跟大名鼎鼎「惡魔果實」一樣,只要吃下去就能獲得那顆果實的特殊能力(完全是個不用靠爸就能成為主角的神奇道具啊!)

在理解模組時,可以想成是它加裝了一個東西原本沒有的功能,大概就像這樣:

這真是太神奇了傑克!

有了模組,我們就不需要使用昨天介紹到的類別繼承,那麼,要如何做到這件事呢?


include

請先詳閱以下使用說明書:

先定義一個模組 Floatable
模組的命名規則就和類別一樣,也必須是大字英文字母開頭的常數。

1
2
3
4
5
module Floatable
def float
puts "浮起來了!"
end
end

再定義一個 Bicycle 的類別:

1
2
class Bicycle
end

產生一個 your_bike 實體,然後使用 float 方法試試看:

1
2
3
4
your_bike = Bicycle.new
your_bike.float

=> NoMethodError (undefined method `float' for #<Bicycle:0x00007fd90901ba28>)

居然失敗了!為什麼?
因為沒有在腳踏車上裝上可以浮起來的氣墊啊!怎麼可能會成功!

— 需要在類別裡使用 include 來裝模組!—

Bicycle 類別裝上剛剛做好的 Floatable 模組吧!

1
2
3
class Bicycle
include Floatable # 引入模組
end

重新產生一個 my_bike 實體,然後使用 float 方法:

1
2
3
4
5
my_bike = Bicycle.new
my_bike.float

# 印出
浮起來了!

嘿嘿~現在我的腳踏車可以浮在水面上了!


extend

接著,我們再來看看更神奇的 extend

假如你今天有一顆引擎的模組,然後把它裝在腳踏車上,腳踏車從此以後就可以用引擎運轉前進了!不過,如果想把腳踏車改造成氮氣噴射的功能,變成阿姆斯特朗旋風噴射阿姆斯特朗砲 的話呢?

這時候就會用上讓模組擴充模組的 extend 了!
廢話說完了,接著來看 code:

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
module Engine
def run
puts "全速啟動!"
end
end

module Acceleration
def accelerate
puts "氮氣噴射!!!"
end
end

class Bike
include Engine
extend Acceleration
end

max = Bike.new

max.run

# 印出
全速啟動!

Bike.accelerate
# 印出
氮氣噴射!!!

看出來了嗎? 原本的 Engine 模組在被 include 後,只是讓腳踏車可以用引擎運轉,但 Acceleration 模組被 extend 後則徹底改造了腳踏車這個類別,讓它完全升級為一種不同的交通工具了(誤)

但如果讓 max 呼叫 accelerate 方法或是讓 Bike 呼叫 run

1
2
3
4
5
6
7
max.accelerate
# 印出
NoMethodError (undefined method `accelerate' for #<Bike:0x00007fec3f2aaf70>)

Bike.run
# 印出
NoMethodError (undefined method `run' for Bike:Class)

就都找不到方法了,所以…

說人話版本 =>

1
2
Bike.accelerate  # 類別方法 / 用 extend 引入
Bike.new.run # 實體方法 / 用 include 引入

明天會再介紹什麼是類別方法和實體方法!(挖坑)


聽說模組和類別很像?

是的,他們很像。
譬如名字都是大寫字母開頭、裡面會放一些方法。在專案裡,如果只看到一個大寫開頭的常數,並沒有辦法確定現在看到的是模組還是類別!不過,它們還是有一些明顯的差異。

舉個例子,可能很多人在純真的童年時會夢想像鳥類一樣在天上飛,在 20 世紀初,萊特兄弟發明了可以載人的飛機,但直到今天,人們仍然不算是學會了飛行。

如果真的要讓人類可以飛,該怎麼做?

首先,鳥類這個類別應該會有一個 fly 的方法,

1
2
3
4
5
class Bird
def fly
# 可以飛行的秘密
end
end

然後繼承它?
class Human < Bird
end
等等!我們的祖先又不是鳥類,所以不應該使用類別繼承吧!

那掛載一個會可以讓人飛行的模組?

1
2
3
4
5
6
7
8
9
module Flyable
def fly
# 可以飛行的秘密
end
end

class Human
intend Flayable
end

我們終於成功裝上了 Flyable !從此以後,人類就可以飛了!


ClassModule 的方法數量

瞎扯了一堆,接著我們直接進程式裡看看:

1
2
3
4
5
6
7
# 查詢 Class 和 Module 的方法和數量

p Module.instance_methods.count
109

p Class.instance_methods.count
112

instance_methods.count 可以幫我們計算一個類別能使用的方法有多少,可以發現雖然兩者的方法都很多,但 ClassModule 硬是多出了 3 個方法!

來看看多出的是哪幾個:

1
2
p Class.instance_methods(false)
[:new, :allocate, :superclass]

由於 Module 少了 :new, :allocate, :superclass 這些方法 ,所以:

  1. 模組沒辦法 new 一個新的實體。
  2. 模組也 無法繼承 別的模組。
1
2
3
4
m = Module.new

m
=> #<Module:0x00007f97268b6310>
1
2
3
m.new

=> NoMethodError (undefined method `new' for #<Module:0x00007f97268b6310>)

Namespace::

namespace?這又是什麼?想像一下,我的車有一顆引擎,你的車也有一顆引擎,但你開的是 BMW 旗艦 V12 雙門跑車,我開的是 Toyota Camry (Camry 錯了嗎?),那這兩輛車會是同樣的車嗎?很明顯不是!(怒)

那要怎麼解決這種問題呢,簡單來說,namespace 就是為了避免叫到同名類別,導致程式無法判讀的問題。我們在呼叫類別時,可以連名帶姓地呼叫:

譬如專案裡常常會看到的:

1
2
3
4
5
6
7
8
9
# 類別(模組)::類別

class User < ActiveRecord::Base
end

class A
module B
end
end

如此一來,我們就能很清楚地知道 User 類別是繼承自 Base 類別,而這個 Base 類別是屬於 ActiveRecord 這個模組(或類別)裡的,其他地方如果有叫 Base 的類別都不是!

有興趣的朋友可以看看龍哥的這篇大作:
Ruby 語法放大鏡之「有時候會看到有兩個冒號寫法是什麼意思?」


說了這麼多,今天就先到這邊囉!
相信大家應該都認識模組了!明天見囉!

https://ithelp.ithome.com.tw/upload/images/20200921/20128363XrK019kUJO.jpg
旗木卡卡西:是不是忘了介紹我?寫輪眼才是最強的模組啊!

繼承 <> D 的意志!鐵拳卡普、革命家龍、草帽魯夫,這一家真的沒開外掛嗎?

https://ithelp.ithome.com.tw/upload/images/20200920/20128363OKSPnJlEte.jpg

[Day11] 由左至右:海賊五皇魯夫、革命軍首領龍、海軍英雄卡普,這祖孫三代的組合會不會太強。。。

繼承

在 Ruby 裡,上下層的類別之間會有**繼承(inheritance)**關係,那麼,什麼是繼承關係呢?

繼承就是你老爸超有錢叫你不用去工作了(誤)
咳咳。。。我想一般人都沒有這種老爸,

我們先看一張國小自然課本裡的圖片好了,大家還記得林奈嗎?
https://ithelp.ithome.com.tw/upload/images/20200920/20128363M0s2bnw1MX.jpg
看出生物學家都是整理控了嗎?

覺得頭大?還是我找法律條文裡的繼承會不會好一點?(討打)
畢竟地球上的生物有百百種,因此如果不按照物種「共有的生理特徵」進行分類,怎麼知道哪一些生物的關係比較接近呢?(不知道好像也沒關係

而 Ruby 裡的繼承關係也是一樣的,在 Ruby Guide 的文件裡有介紹到繼承的概念,簡而言之就是每個類別向上都還有自己所屬的類別。

舉個例子,譬如蘋果柳橙各是一種水果,水果又是食物的一種:

1
2
3
4
5
6
7
8
9
10
11
class Food
end

class Fruit < Food
end

class Orange < Fruit
end

class Apple < Fruit
end

按 1:在 Ruby 的世界裡,小於符號 「<」 被用來表示「繼承」的意思。
按 2:OrangeApple 這兩個類別都屬於 Fruit 類別,而 Fruit 類別又屬於更上層的 Food 類別。

在物件導向的概念裡,如果這些類別的用途是同一型的,譬如 OrangeApple 都很適合打成果汁(?) 通常我們會把相同功能的方法移到「上一層」的類別,所以我會在 Fruit 這個類別直接定義一個 juicy 方法

1
2
3
4
5
6
7
8
9
10
class Fruit < Food
def juicy
end
end

o1 = Orange.new
a1 = apple.new

o1.juicy
a1.juicy

然後,只要是繼承自 Fruit 的所有類別所產生的實體,都可以使用 juicy 方法。


與其說繼承,不如說這是分類吧?

因此 Ruby 語言裡的繼承,我個人認為用分類這個詞可能會更貼切,也更精準一點,不過既然前面都有這麼多大大的貼文了,就虛(ㄐ一ㄤ)心(ㄐ一ㄡ ˋ)接受這個詞吧!

再來看一些程式碼吧!

先定義一個叫 Animal 的類別,並在這個 Animal 類別裡定義了兩個實體方法::walk:eat

1
2
3
4
5
6
7
8
class Animal
def walk(place)
puts "走去 #{place}!"
end
def eat(food)
puts "#{food} 好吃!"
end
end

再讓 CatDog 類別都繼承自 Animal 類別。

1
2
3
4
5
class Cat < Animal
end

class Dog < Animal
end

CatDog 類別分別創造的實體 kittywoffy

1
2
kitty = Cat.new
woffy = Dog.new

kittywoffy 居然都能使用 Animal 類別裡的方法了,這就是繼承的概念!

1
2
3
4
5
6
7
8
9
10
11
12
13
kitty.eat("罐罐")
woffy.eat("骨頭")

# 印出
罐罐 好吃!
骨頭 好吃!

kitty.walk("教室")
woffy.walk("交誼廳")

# 印出
走去 教室!
走去 交誼廳!

有了繼承以後,我們在撰寫程式碼時,就可以不用在同類型的類別裡重複定義相同的方法,這對工程師真是一大福音啊!


我爸是我爸,但我也有自己的尊嚴!

在類別裡,如果定義了一個方法和繼承自上層類別的方法重複了,那麼在呼叫時原先繼承來的方法就會被蓋掉,優先使用自己類別寫的方法:

讓我們看個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Father
def persist
puts "不放手直到夢想到手"
end
end

class Child < Father
def persist
puts "人生恰恰像馬拉松賽跑一樣"
end
end

daddy = Father.new
me = Child.new

daddy.persist

# 印出
不放手直到夢想到手

me.persist

# 印出
人生恰恰像馬拉松賽跑一樣

如此一來,當 me 在呼叫 persist 方法時,和 daddy 呼叫 persist 兩者得到的是不同的結果。(就算個性再像的父子,也有不同的人生哲學!)


今天就先到這邊啦!明天應該會介紹模組(Module),敬請期待!

類別 <> 前往海底一萬米!想要一睹 Ruby 世界裡的 All Blue 就進來吧!

https://ithelp.ithome.com.tw/upload/images/20200917/20128363omyHRrqOXP.jpg

[Day10] 人魚、魚人和人類雖然是不同的類別,但是也可以好好地生活在一起!


什麼是類別?

今天要介紹的是 Ruby 裡的類別(Class)
這是一個 Ruby 語言裡相當重要的關鍵字

簡單來說,類別是拿來產生實體(Instance)的東西,實體就是昨天有介紹過的物件。

但這樣解釋其實不夠完整,想要弄懂 Ruby 裡的類別其實沒有這麼容易,類別的概念裡隱含著 Ruby 這門程式語言的設計哲學,以及有些我尚未掌握的概念,這是無法一蹴而就的,因此,我預計把它拆成幾個部分,用不同天的篇幅介紹:

  • 類別基礎介紹
  • 繼承
  • 類別方法
  • 開放類別
  • Singleton Class 與繼承鍊(可能)

好的,就先讓我們從頭開始來認識類別吧!


經典的烤盤範例

要了解 Ruby 的類別,龍哥是用烤盤和雞蛋糕來巧妙比喻類別與實體的關係。

不過,我今天想吃章魚燒:
https://ithelp.ithome.com.tw/upload/images/20200920/20128363fkT7ZgjfKa.jpg
看著看著好像餓了…

在 Ruby 裡,那個負責做出章魚燒的烤盤就是類別,一個個熱騰騰的章魚燒則是實體
因此只要烤盤沒壞掉,而且原料足夠的話,每次做出來的應該都會是同樣好吃的章魚燒!(原則上啦,不然會被客訴啊!)

所以在定義一個類別時,可以設定產生新實體時要做什麼,或是在類別裡定義一些方法等等。
然後,一個個的實體就可以透過類別生出來囉!

接著,來看一下類別的定義吧!


類別的名稱必須要是常數!

類別的名稱必須要是常數,也就是第一個字元一定要大寫。
不過,並不是看到開頭大寫的就一定是類別,像是模組,也得用大寫常數命名,過幾天會介紹它,這裡先賣個關子。

實際來看 code 吧!
先定義一個 Cat 的類別:

1
2
3
4
5
class Cat
def walk(place)
puts "我想去 #{place}, 喵~"
end
end

然後用 Cat 類別來產生新的實體 mirunini

1
2
miru = Cat.new
nini = Cat.new

(題外話,miru 和 nini 是我朋友家的兩隻貓,超級可愛!)

可以看到 mirunini 都能呼叫 walk 方法:
這是因為他們都屬於 Cat 這個類別

1
2
3
4
5
6
miru.walk ("公園")
miru.walk("遊樂園")

# 印出:
我想去 公園 喵~
我想去 遊樂園 喵~

Ruby 類別的簡單介紹就先到這邊啦!

祝大家都能找到自己心目中的 All Blue!
https://ithelp.ithome.com.tw/upload/images/20200919/20128363bndnA7GRB5.jpg

物件 <> 最初與最盡責的夥伴,黃金梅利號:我想再航行一次!

https://ithelp.ithome.com.tw/upload/images/20200918/20128363GyjtLaK21y.png

[Day10] 梅利號陪伴著草帽海賊團走過了許多的風風雨雨。。。

鐵人終於來到了第十天了,還記得當初出航的勇氣嗎?
今天想和大家討論一個非常重要的主題,在程式領域的範疇裡,我們無法避開它,那就是物件(object)。

物件是什麼?

按照慣例,先說一下程式語言裡的物件是什麼?

在剛接觸 Ruby 時,常常會聽到或看到一句話:「在 Ruby 裡,幾乎所有東西都是物件。」讓我相當好奇,究竟程式語言裡的物件是什麼?

在開始之前,我們先定義一個 Cat 類別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cat
attr_reader :age

def initialize(age)
@age = age
puts "meows~"
end

def walk
puts "走了一小段...爆衝!"
end

def eat
puts "只有肉泥我才要!"
end
end

(不知道什麼是類別也別緊張!明天就會介紹 類別 了~)

接著,我們來創造一隻屬於 Cat 類別的物件 didi

1
2
didi = Cat.new
meows~

didi 一出生就很有精神地喵喵叫了呢!

試想一下,你我都是 Ruby 程式世界的造物主,就在剛剛,我們已經創造了一個 didi 的物件。 didi 目前在 Ruby 的世界裡已經適應地很好,也領到了自己的身分證:

1
2
3
4
5
didi
=> #<Cat:0x00007fa5dd155920 @age=0>

didi.object_id
=> 180 # ID_card get!

可以先把物件想作是一段活生生的程式碼,或者說是將記憶體的位置 擬物化 的過程。


物件的狀態和行為

物件可以像人、或是像任何一種生物一樣,
它會被賦予一個特定的意義,擁有自己的 狀態行為,不過又和方法(method)的定義不太一樣。

其實這件事一開始並不是很好理解,我是從自身出發去思考這個脈絡的。

譬如說,每個人都會有屬於自己的髮色、年齡等等,這些就是狀態的一種,至於吃飯、睡覺、走路等等,則是人們常見的行為。所以如果把現實世界參照到程式的世界,我們「每一個人」其實就是活在其中的物件,而「人類」就是我們所屬的類別了。(關於類別和實體的關係,明天會有更詳細的介紹。)

現在,再回過頭來看我們剛剛創造的 didi

1
2
3
4
5
6
7
8
9
10
11
# didi 現在幾歲
didi.age
=> 0

# didi 走路
didi.walk
走了一小段...爆衝!

# didi 吃東西
didi.eat
只有肉泥我才要!

didi 也有了屬於它自己的狀態和行為了,是不是很可愛呢!


順帶一提,在了解程式世界與物件的關係後,
我不禁想起 Matrix 和虛擬理論,這邊帶個老高的影片給大家複習複習:

Yes

神也要花上六天才寫完程式啊...


為什麼要使用物件?

在程式的世界裡,我們會對著物件呼叫方法,來完成我們打算讓它執行的事情。

這裡就不得不提**物件導向(Object Oriented)**了(老實說我本來沒預料到要寫這個)
要用文字講解這麼抽象的概念,我自己也有點亂,接下來我們還是一邊看程式碼一邊說明吧!

在剛剛的例子裡:

1
didi.eat

可以看到, 我們對 didi 這個物件使用一個方法叫做 eat ,在一開始理解時,我會說是在這個物件 didi 呼叫了 times 這個方法。

但是在知道更多物件導向的概念後,我開始轉換思考的方式,把這整件事看成是 didi 這個接收者,接受了我傳遞給它的 eat 訊息,在 Ruby 裡,某些特定物件可以理解某些特定訊息,並對應到可執行的方法。

所以,我們並不只是在 didi 這個物件上呼叫了 eat 這個方法這麼單純而已,物件導向其實還偷偷做了很多的事情,不過由於篇幅有限,時間也相當緊迫,在此篇無法完整概述。

相信在不久後的將來,我勢必會再重新說明一次 Object Oriented 的概念,就像學 JavaScript 的人都要寫一篇網誌來說明什麼是 closure 沒有兩樣。


覺得還要更多的篇幅才能講完物件的概念,不過剛剛寫到後來我看了一下時間,發現如果只剩 30 分鐘再不發文就要斷更啦!趕快緊急停筆,不好意思只能先寫到這邊~

另外,其實在寫今天這篇物件之前,我一直在考慮到底要先介紹 類別 還是 物件 較好,苦惱了很久,畢竟 Ruby 語言裡有太多東西都環環相扣啦!最後決定還是先把自己對 物件 的理解寫出來,也梳理一番自己學習上的思緒。

緊接著明天就會介紹 Ruby 裡的類別啦!謝謝大家的觀看!

回傳 <> 受傷了就趕快回船上治療!

https://ithelp.ithome.com.tw/upload/images/20200917/20128363itoyr3s3Sd.png

[Day09] 別小看喬巴特製研發的藍波球!

今天要介紹的是**回傳(return)**!
在 Ruby 的世界裡,所有的方法執行完後都有回傳值(return value),沒有的話則會回傳一個 nil

回傳的特性是:

回傳 = 交回控制權

  1. 控制權就像令牌一樣,手上有令牌,才能執行程式碼。
  2. 執行完之後,會把控制權再交還給當初呼叫他的人。

單看文字可能有點不好理解,我們來看看實際的例子:

1
2
3
4
5
6
7
8
9
def calc_perimeter(radius)
return 2 * Math::PI * radius
# 執行後把控制權交回給呼叫的程式
end

puts calc_perimeter(5)

# 印出:
=> 31.41592653589793

在這短短幾行中,程式執行了這幾件事:

  1. 呼叫 calc_perimeter 方法,並傳回 5 作為引數。
  2. 執行 calc_perimeter 方法。
  3. return 交還控制權,並同時回傳計算結果 => 31.41592653589793
  4. 把計算結果交給 puts方法,並印出內容。

在 Ruby 裡,我們也可以省略 return 不寫,方法會自動回傳最後一行的執行結果!

把結果印出來讓我們更了解回傳值!

在學習程式語言的過程中,把結果印出來是相當常見的行為,而要在 Ruby 裡做到這件事,我們會使用:

  • print
  • puts
  • p

以上三者都可以替我們達成印出結果的目標,但是為什麼有三種寫法?有沒有什麼不同呢?先來看看程式實際是如何運作的吧:

1
2
3
4
5
6
7
8
9
10
print "Ruby"
Ruby => nil

puts "Ruby"
Ruby
=> nil

p "Ruby"
"Ruby"
=> "Ruby"

我們可以發現如果用的是 print 只能印出結果,也不會換行。同時, printputs 回傳的都是 nilnil 代表的意思是沒有回傳值,不過如果是用 p 來印出結果,它還會順便帶一個回傳值回來。

1
2
3
4
5
6
7
puts "aaa"
aaa
=> nil # 意思是沒有結果

> p "aaa"
"aaa"
=> "aaa" #回傳它自己的東西

用簡單的表格來比較三者的差異:
|print |puts |p |
|——–|——–|——–|
|不換行 |換行 |換行 |
|回傳 nil|回傳 nil|回傳帶有型態的值|
|印出內容|印出內容|印出完整型態的內容|


為了更加了解 putsp 的差異,這裡有一組例子:有兩個方法分別為 a, b ,皆使用 p 印出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def a
p "aaa"
end

def b
p "bbb"
end

a && b

# 印出:
"aaa"
"bbb"
=> "bbb"

不過如果把方法 a, b 改成用 put 印出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
def a
puts "aaa"
end

def b
puts "bbb"
end

a && b

# 印出:
aaa
=> nil

就只會印出 aaa 了。

這是因為 && 的特性是左右兩側皆為 true 才會成立。

所以當我們用 p 印出結果時,由於 a , b 兩個方法都有回傳值,因此 a && b 除了會執行方法 a , b 各自要做的事情 => 印出 "aa""bb" 。還會有一個的回傳值為 "bb"(因為 Ruby 會自動回傳最後一個方法的結果)

不過,如果改成用 puts 來印出結果,則會因為方法 a 的回傳值為 nil 而無法執行方法 b ,想當然爾, a && b 最後也沒有回傳值了!


今天就先到這邊啦!

方法 <> 我斑願稱你為最強!站上頂點的方法只有一個

[Day08] 想要變強就要不斷修煉,這是唯一的方法!

啊啦啦標題不小心用了火影梗,常常聽別人說,做事情要用對方法,這件事在 Ruby 的世界裡也是一樣的!

所以說,方法係啥?為什麼要使用方法?程式語言裡的方法是什麼?

什麼是方法?

正常來說,我們不會想把所有的程式碼寫在同一個檔案裡。而且如果是在開發專案,隨著複雜度提升,不同的程式通常會依照功能和類別(也可能只是按英文字母的順序)排列,分佈在專案裡的不同位置。

不過,由於不同的位置也會需要用到共同的程式碼,為了避免重複 coding,這時候就會透過 方法(method) 來解決這個問題,如果是學過 Javascript 函式(function)的朋友們,應該可以很快知道我在說什麼,它與 Ruby 的方法實際上是非常接近的東西。

為什麼要使用方法

  1. 可一直重複使用
  2. 讓程式碼更容易理解

先來看看在 Ruby 如何定義一個方法:

1
2
3
def method_name
# 這個方法要做的事情
end

每個方法都會被賦予一個名稱,會使用 defend 兩個單詞來定義方法,而夾在中間的部分則是要請這個方法做的事情。

一旦定義了一個方法以後,就可以開始使喚它做事…(誤)

無論何時何地,取名字都要小心謹慎!

變數方法 的名稱如果相同時,同名的 變數 會蓋掉同名的 方法,需要特別留意!所以在命名方法時,請花點時間好好思考,取個獨一無二又能看出在做什麼的名字吧!


參數與引數

剛剛已經介紹了如何定義方法,不過在定義方法時,還可以設定這個方法需要哪些**參數(parameter)**:

1
2
3
def walk(place)
puts "我想去#{place}散步!"
end

由於我們設定 walk 需要一個參數 place,所以在呼叫 walk 時,也必須要剛好給一個**引數(argument)**,多了或少了都不行,在 Ruby 的世界裡,只要引數和參數的數量不同,方法就無法順利執行。

重新整理一下:

  • 參數:定義方法使用要帶入的東西,類似規格的概念。
  • 引數:呼叫方法時帶入的東西,引數的數量必須與定義方法時相同。

接下來,我們來呼叫看看這個 walk 方法!


方法!我呼叫你!

呼叫(invoke)方法的意思其實就是使用這個方法,算是技術圈的術語之一吧,總覺得有一種召喚某某方法來幫自己完成一件事的感覺。另外,也有看過其他大大稱為「調用」,只是我更習慣把它稱為「呼叫」。

要執行已經定義的方法的方式很簡單,只要直接呼叫方法的名字即可:

1
2
3
4
walk("公園")

# 印出:
我想去公園散步!

甚至省略了 ( ) 也可以,這是 Ruby 獨有的祕技!

1
2
3
4
walk"公園"

# 印出:
我想去公園散步!

事實上,方法還可以被物件(object)所呼叫,不過那部分就等介紹到類別與物件的時候再一起說明吧!

請放心呼叫方法吧!

比起 Javascript 在呼叫函式時一定要有 () ,Ruby 是不是厲害許多呢!(不小心就引戰了)不過,前提是要記得自己定義過哪些方法…

你的名字是?あなたのお名前は?


方法的參數也可以有預設值

在定義方法的時候,我們可以先給參數一個預設值,這麼一來,如果在呼叫方法時沒有給引數,方法會以預設值去執行,假裝自己有拿到引數

1
2
3
4
5
6
7
8
9
def valentine(gift = "chocolate")
puts "謝謝妳送我#{gift}! 我很開心!"
end

# 呼叫
valentine

# 印出
謝謝妳送我chocolate! 我很開心!

突然想到有一年情人節買巧克力給自己…(哭)

設定一個以上的參數也是 OK 的!

1
2
3
4
5
6
7
8
def play_baseball(pitcher, catcher, batter)
puts "需要 #{pitcher}, #{catcher}, #{batter} 才能打棒球"
end

play_baseball("熱狗", "爆米花", "可樂")

# 印出:
需要 熱狗, 爆米花, 可樂 才能打棒球

聽說在 Ruby 3.0 版本後,方法帶入的參數還會增加型別限制!期待!


Ruby 方法的命名習慣

除了可以用常見的英文、底線及數字的組合替方法命名之外,有時候還會看到帶有問號 ? 、驚嘆號 != 的方法,它們通常代表了某種特殊含義,但這些符號只能放在方法名字的最後面。

問號 表示這個方法會回傳 truefalse

譬如:

1
2
[1, 2, 3, 4, 5].include?(2)
=> true

驚嘆號 ! 表示使用這個方法可能會產生「副作用」!

我直接舉個簡單的例子,先假設有一個陣列 array

1
array = [1, 1, 2, 3, 3]

使用 uniq 這個方法把重複的元素去掉:

1
2
array.uniq
=> [1, 2, 3]

這時候原本的 array 沒有產生改變:

1
2
array
=> [1, 1, 2, 3, 3]

但是如果我用的是 uniq! 這個方法把重複的元素去掉:

1
2
array.uniq!
=> [1, 2, 3]

這時候原本的 array 就變成了:

1
2
array
=> [1, 2, 3]

至於在方法名稱後面加上等號 = 的意義,目前我也還說不出個所以然,只好先把我的參考資料放在這,等日後再回來補齊了:

在 Ruby 或 Rails ,我們經常會使用這樣的命名慣例,以節省大家的工作時間,我覺得這是非常棒的一個共識。


如果想更深入研究 Ruby 的方法,非常推薦大家先拜讀龍哥的這篇文章:如果想要找某個方法的定義,該怎麼找? 今天就先到介紹這邊啦!