一个老程式设计师的建议
最后更新于:2022-04-01 05:40:44
你已經完成了這本書而且打算繼續寫程式。也許這會成為你的一門職業,也許你只是作為業餘愛好玩玩。無論如何,你都需要一些建議以保證你在正確的道路上繼續前行,並且讓這項新的愛好為你帶來最大程度的享受。
我從事寫程式這行已經太長時間,長到對我來說寫程式已經是非常乏味的事情了。我寫這本書的時候,已經懂得大約20 種程式語言,而且可以在大約一天或者一個星期內學會一門程式語言(取決於這門語言有多古怪)。現在對我來說寫程式這件事情已經很無聊,已經談不上什麼興趣了。當然這不是說寫程式本身是一件無聊的事情,也不是說你以後也一定會這樣覺得,這只是我個人在當前的感覺而已。
在這麼久的旅程下來我的體會是:程式語言這東西並不重要,重要的是你用這些語言做的事情。事實上我一直知道這一點,不過以前我會周期性地被各種程式語言分神而忘記了這一點。現在我是永遠不會忘記這一點了,你也不應該忘記這一點。
你學到和用到的程式語言並不重要。不要被圍繞某一種語言的宗教把你扯進去,這只會讓你忘掉了語言的真正目的,也就是作為你的工具來實現有趣的事情。
寫程式作為一項智力活動,是唯一一種能讓你創建交互式藝術的藝術形式。你可以建立專案讓別人使用,而且你可以間接地和使用者溝通。沒有其他的藝術形式能做到如此程度的交互性。電影領著觀眾走向一個方向,繪畫是不會動的。而程式碼卻是雙向互動的。
寫程式作為一項職業只是一般般有趣而已。寫程式可能是一份好工作,但如果你想賺更多的錢而且過得更快樂,你其實開一間快餐分店就可以了。你最好的選擇是將你的程式技術作為你其他職業的秘密武器。
技術公司裡邊會寫程式的人多到一毛錢一打,根本得不到什麼尊敬。而在生物學、醫藥學、政府部門、社會學、物理學、數學等行業領域從事寫程式的人就能得到足夠的尊敬,而且你可以使用這項技能在這些領域做出令人驚異的成就。
當然,所有的這些建議都是沒啥意義的。如果你跟著這本書學習寫軟體而且覺得很喜歡這件事情的話,那你完全可以將其當作一門職業去追求。你應該繼續深入拓展這個近五十年來極少有人探索過的奇異而美妙的智力工作領域。若能從中得到樂趣當然就更好了。
最後我要說的是學習創造軟體的過程會改變你而讓你與眾不同。不是說更好或更壞,只是不同了。你也許會發現因為你會寫軟件而人們對你的態度有些怪異,也許會用「怪人「這樣的詞來形容你。也許你會發現因為你會戳穿他們的邏輯漏洞而他們開始討厭和你爭辯。甚至你可能會發現有人因為你懂得電腦怎麼運作而覺得你是個討厭的怪人。
對於這些我只有一個建議: 讓他們去死吧。這個世界需要更多的怪人,他們知道東西是怎麼工作的而且喜歡找到答案。當他們那樣對你時,只要記住這是你的旅程,不是他們的。「與眾不同」不是誰的錯,告訴你「與眾不同是一種錯」的人只是嫉妒你掌握了他們做夢都不能想到的技能而已。
你會寫程式。他們不會。這真他媽的酷。
下一步
最后更新于:2022-04-01 05:40:42
現在還不能說你是一個程式員。這本書的目的相當於給你一個「程式設計師棕帶」。你已經了解了足夠的寫程式基礎,並且有能力閱讀別的寫程式書籍了。讀完這本書,你應該已經掌握了一些學習的方法,並且具備了該有的學習態度,這樣你在閱讀其他 Ruby 書籍時也許會更順利,而且能學到更多東西。
> **Rob says:** 為了更有趣,我推薦你閱讀 Why’s (Poignant) Guide to Ruby: [http://mislav.uniqpath.com/poignant-guide](http://mislav.uniqpath.com/poignant-guide) 大部份的程式內容現在都正在被 review。但是 Why 的心智無比聰明,而且他的書就像是一件藝術品。去看看它的一些 opensource 專案,你可以從他的程式碼裡學到許多東西。
或許,你現在已經可以開始鼓搗一些程式出來了。如果你手上有需要解決的問題,試著寫個程式解決一下。你一開始寫的東西可能很挫,不過這沒有關係。以我為例,我在學每一種語言的初期都是很挫的。沒有哪個初學者能寫出完美的代碼來,如果有人告訴你他有這本事,那他只是在厚著臉皮撒謊而已。
最後,記住學習寫程式是要投入時間的,你可能需要至少每天晚上練習幾個小時。順便告訴你,當你每晚學習 Ruby 的時候,我在努力學習彈吉他。我每天練習2 到4 小時,而且還在學習基本的音階。
每個人都是某一方面的菜鳥。
习题 52: 创造你的网页游戏
最后更新于:2022-04-01 05:40:40
這本書馬上就要結束了。本章的練習對你是一個真正的挑戰。當你完成以後,你就可以算是一個能力不錯的 Ruby 初學者了。為了進一步學習,你還需要多讀一些書,多寫一些程式,不過你已經具備進一步學習的技能了。接下來的學習就只是時間、動力、以及資源的問題了。
在本章習題中,我們不會去創建一個完整的遊戲,取而代之的是我們會為《習題42》中的遊戲建立一個“引擎(engine)”,讓這個遊戲能夠在瀏覽器中運行起來。這會涉及到將《習題42》中的遊戲「重構(refactor)」,將《習題47》中的架構混合進來,添加自動測試程式碼,最後建立一個可以運行遊戲的web 引擎。
這是一節很「龐大」的習題。我預測你要花一周到一個月才能完成它。最好的方法是一點一點來,每晚上完成一點,在進行下一步之前確認上一步有正確完成。
## 重構《習題42》的遊戲
你已經在兩個練習中修改了 `gothonweb` 專案,這節習題中你會再修改一次。這種修改的技術叫做「重構(refactoring)」,或者用我喜歡的講法來說,叫「修修補補(fixing stuff)」。重構是一個程式術語,它指的是清理舊程式碼或者為舊程式碼添加新功能的過程。你其實已經做過這樣的事情了,只不過不知道這個術語而已。這是寫軟體過程的第二個自然屬性。
你在本節中要做的,是將《習題47》中的可以測試的房間地圖,以及《習題42》中的遊戲這兩樣東西歸併到一起,創建一個新的遊戲架構。遊戲的內容不會發生變化,只不過我們會通過“重構”讓它有一個更好的架構而已。
第一步是將 `ex47.rb` 的內容複製到 `gothonweb/lib/map.rb` 中,然後將 `ex47_tests.rb` 的內容複製到 `gothonweb/test/test_map.rb`中,然後再次運行測試,確認他們還能正常運作。
> **Note:** 從現在開始我不會再向你展示運行測試的輸出了,我就假設你回去運行這些測試,而且知道怎樣的輸出是正確的。
將《習題47》的程式碼拷貝完畢後,你就該開始重構它,讓它包含《習題42》中的地圖。我一開始會把基本架構為你準備好,然後你需要去完成`map.rb`和`map_tests.rb` 裡邊的內容。
首先要做的是使用 `Room` 類來構建基本的地圖架構:
~~~
class Room
attr_accessor :name, :description, :paths
def initialize(name, description)
@name = name
@description = description
@paths = {}
end
def go(direction)
@paths[direction]
end
def add_paths(paths)
@paths.update(paths)
end
end
central_corridor = Room.new("Central Corridor",
%q{
The Gothons of Planet Percal #25 have invaded your ship and destroyed
your entire crew. You are the last surviving member and your last
mission is to get the neutron destruct bomb from the Weapons Armory,
put it in the bridge, and blow the ship up after getting into an
escape pod.
You're running down the central corridor to the Weapons Armory when
a Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown costume
flowing around his hate filled body. He's blocking the door to the
Armory and about to pull a weapon to blast you.
})
laser_weapon_armory = Room.new("Laser Weapon Armory",
%q{
Lucky for you they made you learn Gothon insults in the academy.
You tell the one Gothon joke you know:
Lbhe zbgure vf fb sng, jura fur fvgf nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr.
The Gothon stops, tries not to laugh, then busts out laughing and can't move.
While he's laughing you run up and shoot him square in the head
putting him down, then jump through the Weapon Armory door.
You do a dive roll into the Weapon Armory, crouch and scan the room
for more Gothons that might be hiding. It's dead quiet, too quiet.
You stand up and run to the far side of the room and find the
neutron bomb in its container. There's a keypad lock on the box
and you need the code to get the bomb out. If you get the code
wrong 10 times then the lock closes forever and you can't
get the bomb. The code is 3 digits.
})
the_bridge = Room.new("The Bridge",
%q{
The container clicks open and the seal breaks, letting gas out.
You grab the neutron bomb and run as fast as you can to the
bridge where you must place it in the right spot.
You burst onto the Bridge with the netron destruct bomb
under your arm and surprise 5 Gothons who are trying to
take control of the ship. Each of them has an even uglier
clown costume than the last. They haven't pulled their
weapons out yet, as they see the active bomb under your
arm and don't want to set it off.
})
escape_pod = Room.new("Escape Pod",
%q{
You point your blaster at the bomb under your arm
and the Gothons put their hands up and start to sweat.
You inch backward to the door, open it, and then carefully
place the bomb on the floor, pointing your blaster at it.
You then jump back through the door, punch the close button
and blast the lock so the Gothons can't get out.
Now that the bomb is placed you run to the escape pod to
get off this tin can.
You rush through the ship desperately trying to make it to
the escape pod before the whole ship explodes. It seems like
hardly any Gothons are on the ship, so your run is clear of
interference. You get to the chamber with the escape pods, and
now need to pick one to take. Some of them could be damaged
but you don't have time to look. There's 5 pods, which one
do you take?
})
the_end_winner = Room.new("The End",
%q{
You jump into pod 2 and hit the eject button.
The pod easily slides out into space heading to
the planet below. As it flies to the planet, you look
back and see your ship implode then explode like a
bright star, taking out the Gothon ship at the same
time. You won!
})
the_end_loser = Room.new("The End",
%q{
You jump into a random pod and hit the eject button.
The pod escapes out into the void of space, then
implodes as the hull ruptures, crushing your body
into jam jelly.
})
escape_pod.add_paths({
'2' => the_end_winner,
'*' => the_end_loser
})
generic_death = Room.new("death", "You died.")
the_bridge.add_paths({
'throw the bomb' => generic_death,
'slowly place the bomb' => escape_pod
})
laser_weapon_armory.add_paths({
'0132' => the_bridge,
'*' => generic_death
})
central_corridor.add_paths({
'shoot!' => generic_death,
'dodge!'=> generic_death,
'tell a joke' => laser_weapon_armory
})
START = central_corridor
~~~
你會發現我們的 `Room` 類和地圖有一些問題:
1. 在進入一個房間以前會打出一段文字作為房間的描述,我們需要將這些描述和每個房間關聯起來,這樣房間的次序就不會被打亂了,這對我們的遊戲是一件好事。這些描述本來是在 `if-else` 結構中的,這是我們後面要修改的東西。
2. 原版遊戲中我們使用了專門的程式來生成一些內容,例如炸彈的激活鍵碼,艦艙的選擇等,這次我們做遊戲時就先使用預設值好了,不過後面的加分習題裡,我會要求你把這些功能再加到遊戲中。
3. 我為所有的遊戲中的失敗結尾寫了一個 `generic_death`,你需要去補全這個函式。你需要把原版遊戲中所有的失敗結尾都加進去,並確保程式碼能正確運行。
4. 我添加了一種新的轉換模式,以`"*"`為標記,用來在遊戲引擎中實現「catch-all」動作。
等你把上面的程式碼基本寫好以後,接下來就是引導你繼續寫下去的自動測試的內容 `test/test_map.rb` 了:
~~~
require 'test/unit'
require_relative '../lib/map'
class MapTests < Test::Unit::TestCase
def test_room()
gold = Room.new("GoldRoom",
%q{This room has gold in it you can grab. There's a
door to the north.})
assert_equal(gold.name, "GoldRoom")
assert_equal(gold.paths, {})
end
def test_room_paths()
center = Room.new("Center", "Test room in the center.")
north = Room.new("North", "Test room in the north.")
south = Room.new("South", "Test room in the south.")
center.add_paths({'north' => north, 'south' => south})
assert_equal(center.go('north'), north)
assert_equal(center.go('south'), south)
end
def test_map()
start = Room.new("Start", "You can go west and down a hole.")
west = Room.new("Trees", "There are trees here, you can go east.")
down = Room.new("Dungeon", "It's dark down here, you can go up.")
start.add_paths({'west' => west, 'down' => down})
west.add_paths({'east' => start})
down.add_paths({'up' => start})
assert_equal(start.go('west'), west)
assert_equal(start.go('west').go('east'), start)
assert_equal(start.go('down').go('up'), start)
end
def test_gothon_game_map()
assert_equal(START.go('shoot!'), generic_death)
assert_equal(START.go('dodge!'), generic_death)
room = START.go('tell a joke')
assert_equal(room, laser_weapon_armory)
end
end
~~~
你在這部分練習中的任務是完成地圖,並且讓自動測試可以完整地檢查過整個地圖。這包括將所有的 `generic_death` 物件修正為遊戲中實際的失敗結尾。讓你的程式碼成功運行起來,並讓你的測試越全面越好。後面我們會對地圖做一些修改,到時候這些測試將保證修改後的程式碼還可以正常工作。
## 會話(session)和用戶跟踪
在你的 web 程式運行的某個位置,你需要追踪一些信息,並將這些信息和用戶的瀏覽器關聯起來。在HTTP 協議的框架中,web 環境是「無狀態(stateless)」的,這意味著你的每一次請求和你其它的請求都是相互獨立的。如果你請求了頁面A,輸入了一些資料,然後點了一個頁面B 的鏈接,那你在頁面A 輸入的數據就全部消失了。
解決這個問題的方法是為 web 程式建立一個很小的資料儲存功能,給每個瀏覽器賦予一個獨一無二的數字,用來跟踪瀏覽器所作的事情。這個功能通常適用資料庫或者是存儲在磁碟上的檔案來實現。在 Sinatra 這個框架中實現這樣的功能是很容易的,以下就是一個這樣的例子(使用 Rack middleware):
~~~
require 'rubygems'
require 'sinatra'
use Rack::Session::Pool
get '/count' do
session[:count] ||= 0
session[:count] +=1
"Count: #{session[:count]}"
end
get '/reset' do
session.clear
"Count reset to 0."
end
~~~
## 建立引擎
你應該已經寫好了遊戲地圖和它的單元測試程式碼碼。現在我要求你製作一個簡單的遊戲引擎,用來讓遊戲中的各個房間運轉起來,從玩家收集輸入,並且記住玩家到了那一幕。我們將用到你剛學過的會話來製作一個簡單的引擎,讓它可以:
1. 為新使用者啟動新的遊戲。
2. 將房間展示給使用者。
3. 接受使用者的輸入。
4. 在遊戲中處理使用者的輸入。
5. 顯示遊戲的結果,繼續遊戲的下一幕,知道玩家角色死亡為止。
為了建立這個引擎,你需要將我們久經考驗的`lib/gothonsweb.rb` 搬過來,建立一個功能完備的、基於 session 的遊戲引擎。這裡的難點是我會先使用基本的 HTML 檔案創建一個非常簡單的版本,接下來將由你完成它,基本的引擎是這個樣子的:
~~~
require_relative "gothonweb/version"
require_relative "map"
require "sinatra"
require "erb"
module Gothonweb
use Rack::Session::Pool
get '/' do
# this is used to "setup" the session with starting values
p START
session[:room] = START
redirect("/game")
end
get '/game' do
if session[:room]
erb :show_room, :locals => {:room => session[:room]}
else
# why is there here? do you need it?
erb :you_died
end
end
post '/game' do
action = "#{params[:action] || nil}"
# there is a bug here, can you fix it?
if session[:room]
session[:room] = session[:room].go(params[:action])
end
redirect("/game")
end
end
~~~
下一步,你應該刪除 `lib/views/hello_form.erb` 和 `lib/views/index.erb` 然後創作兩個在上述 code 提到的 template,這裡是一個非常簡單的 `lib/views/show_room.erb`:
~~~
<h1><%= room.name %></h1>
<pre>
<%= room.description %>
</pre>
<% if room.name == "death" %>
<p>
<a href="/">Play Again?</a>
</p>
<% else %>
<p>
<form action="/game" method="POST">
- <input type="text" name="action"> <input type="SUBMIT">
</form>
</p>
<% end %>
~~~
這就用來顯示遊戲中的房間的模板。接下來,你需要在使用者跑到地圖的邊界時,用一個模板告訴使用者他的角色的死亡信息,也就是`lib/views/you_died.erb` 這個模板:
~~~
<h1>You Died!</h1>
<p>Looks like you bit the dust.</p>
<p><a href="/">Play Again</a></p>
~~~
準備好了這些文件,你現在可以做下面的事情了:
1. 讓測試代碼 `test/test_gothonsweb.rb` 再次運行起來,這樣你就可以去測試這個遊戲。由於 session 的存在,你可能頂多只能實現幾次點擊,不過你應該可以做出一些基本的測試來。
2. 執行 lib/gothonsweb.rb` 腳本,試玩一下你的遊戲。
3. 你需要和往常一樣刷新和修正你的遊戲,慢慢修改遊戲的HTML 檔案和引擎,直到你實現遊戲需要的所有功能為止。
## 你的期末考試
你有沒有覺著我一下子給了你超多的資訊呢?那就對了,我想要你在學習技能的同時可以有一些可以用來鼓搗的東西。為了完成這節習題,我將給你最後一套需要你自己完成的練習。你將注意到,到目前為止你寫的遊戲並不是很好,這只是你的第一版程式碼而已。你現在的任務是讓遊戲更加完善,實現下面的這些功能:
1. 修正程式碼中所有我提到和沒提到的bug,如果你發現了新的bug,你可以告訴我。
2. 改進所有的自動測試,讓你可以測試更多的內容,直到你可以不用瀏覽器就能測到所有的內容為止。
3. 讓HTML 頁面看上去更美觀一些。
4. 研究一下網頁登錄系統,為這個程式創建一個登錄界面,這樣人們就可以登錄這個遊戲,並且可以保存遊戲高分。
5. 完成遊戲地圖,盡可能地把遊戲做大,功能做全。
6. 給用戶一個「幫助系統」,讓他們可以查詢每個房間裡可以執行哪些命令。
7. 為你的遊戲添加新功能,想到什麼功能就添加什麼功能。
8. 創建多個地圖,讓用戶可以選擇他們想要玩的一張來進行遊戲。你的 `lib/gothonsweb.rb` 應該可以運行提供給它的任意的地圖,這樣你的引擎就可以支持多個不同的遊戲。
9. 最後,使用你在習題 48 和49 中學到的東西來創建一個更好的輸入處理器。你手頭已經有了大部分必要的程式碼,你只需要改進語法,讓它和你的輸入表單以及遊戲引擎掛鉤即可。
祝你好運!
习题 51: 从浏览器中取得输入
最后更新于:2022-04-01 05:40:37
雖然能讓瀏覽器顯示「Hello World」是很有趣的一件事情,但是如果能讓用戶通過表單(form)向你的應用程序提交資訊就更有趣了。這節習題中,我們將使用form 改進你的web 程式,並且搞懂如何為一個網站程式寫自動化測試。
## Web 運作原理
該學點無趣的東西了。在建立 form 前你需要先多學一點關於 web的運作原理。這裡講的並不完整,但是相當準確,在你的程式出錯時,它會幫你找到出錯的原因。另外,如果你理解了form 的應用,那麼建立form 對你來說就會更容易了。
我將以一個簡單的圖示講起,它向你展示了web 請求的各個不同的部分,以及資訊傳遞的大致流程:
![http request diagram](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-06_55c375b7b7812.png)
為了方便講述HTTP 請求(request) 的流程,我在每條線上面加了字母標籤以作區別。
1. 你在瀏覽器中輸入網址http://learnpythonthehardway.org/,然後瀏覽器會通過你的電腦的網路設備發出request(`線路A`)。
2. 你的request 被傳送到網際網路(`線路B`),然後再抵達遠端服務器(`線路C`),然後我的伺服器將接受這個request。
3. 我的伺服器接受 request 後,我的 web 應用程式就去處理這個請求(`線路D`),然後我的軮頁應用程式就會去運行 `/` (index) 這個「處理程序(handler)」。
4. 在程式碼 return 的時候,我的伺服器就會發出響應(response),這個響應會再通過`線路D`傳遞到你的瀏覽器。
5. 這個網站所在的伺服器將響應由`線路D`獲取,然後通過`線路C`傳至網際網路。
6. 響應通過網路網路由`線路B`傳至你的電腦,電腦的網路卡再通過`線路A`將響應傳給你的瀏覽器。
7. 最後,你的瀏覽器顯示了這個響應的內容。
這段詳解中用到了一些術語。你需要掌握這些術語,以便在談論你的 web 應用時你能明白而且應用它們:
### 瀏覽器(browser)
這是你幾乎每天都會用到的軟件。大部分人不知道它真正的原理,他們只會把它叫作「網際網路」。它的作用其實是接收你輸入到地址欄網址(例如[http://learnpythonthehardway.org](http://learnpythonthehardway.org/)),然後使用該資訊向該網址對應的伺服器提出請求(request)。
### IP 位址 ( Address )
通常這是一個像 [http://learnpythonthehardway.org/](http://learnpythonthehardway.org/) 一樣的URL (Uniform Resource Locator,統一資源定位符 ),它告訴瀏覽器該打開哪個網站。前面的 `http` 指出了你要使用的協議(protocol),這裡我們用的是「超文本傳輸協議(Hyper-Text Transport Protocol)」。你還可以試試ftp://ibiblio.org/,這是一個「FTP文件傳輸協議(File Transport Protocol)‘的例子。`learnpythonthehardway.org` 這部分是「主機名(hostname)」,也就是一個便於人閱讀和記憶的字串,主機名會被匹配到一串叫作「IP 位址」的數字上面,這個「IP 位址」就相當於網路中一台電腦的電話號碼,通過這個號碼可以訪問到這台電腦。最後,URL中還可以尾隨一個「路徑「,例如http://learnpythonthehardway.org/book/ 中的 `/book/`,它對應的是伺服器上的某個文件或者某些資源,通過訪問這樣的網址,你可以向伺服器發出請求,然後獲得這些資源。網站地址還有很多別的組成部分,不過這些是最主要的。
### 連接(connection)
一旦瀏覽器知道了協議(http)、伺服器(learnpythonthehardway.org)、以及要獲得的資源,它就要去建立一個連接。這個過程中,瀏覽器讓操作系統(Operating System, OS) 打開計算機的一個「埠號(port)」(通常是80埠號),埠號準備好以後,操作系統會回傳給你的程式一個類似檔案的東西,它所做的事情就是通過網路傳輸和接收資料,讓你的電腦和learnpythonthehardway.org這個網站所屬的伺服器之間實現資料交流。當你使用 http://localhost:4567/ 訪問你自己的站點時,發生的事情其實是一樣的,只不過這次你告訴了瀏覽器要訪問的是你自己的電腦(localhost),要使用的端口不是默認的80,而是 4567 。你還可以直接訪問http://learnpythonthehardway.org:80/,這和不輸入埠號效果一樣,因為HTTP的默認埠號本來就是80。
### 請求(request)
你的瀏覽器通過你提供的地址建立了連接,現在它需要從遠端伺服器要到它(或你)想要的資源。如果你在URL的結尾加了 `/book/`,那你想要的就是`/book/` 對應的檔案或資源,大部分的伺服器會直接為你呼叫`/book/index.html` 這個檔案,不過我們就假裝不存在好了。瀏覽器為了獲得伺服器上的資源,它需要向伺服器發送一個「請求」。這裡我就不講細節了,為了得到伺服器上的內容,你必須先向伺服器發送一個請求才行。有意思的是,「資源」不一定非要是檔案。例如當瀏覽器向你的應用程序提出請求的時候,伺服器返回的其實是你的程式碼生成的一些東西。
### 伺服器(server)
伺服器指的是瀏覽器另一端連接的電腦,它知道如何回應瀏覽器請求的檔案和資源。大部分的 web 伺服器只要發送檔案就可以了,這也是伺服器流量的主要部分。不過你學的是使用 Ruby 組建一個伺服器,這個伺服器知道如何接受請求,然後返回用 Ruby 處理過的字符串。當你使用這種處理方式時,你其實是假裝把檔案發給了瀏覽器,其實你用的都只是程式碼而已。就像你在《習題50》中看到的,要構建一個「響應」其實也不需要多少程式碼。
### 響應(response)
這就是你的伺服器回覆給你的請求,傳回至瀏覽器的HTML,它裡邊可能有css、javascript、或者圖像等內容。以檔案響應為例,伺服器只要從磁碟讀取檔案,發送給瀏覽器就可以了,不過它還要將這些內容包在一個特別定義的「header]」中,這樣瀏覽器就會知道它獲取的是什麼類型的內容。以你的web 應用程式為例,你發送的其實還是一樣的東西,包括 header 也一樣,只不過這些資料是你用 Ruby 程式碼即時生成的。
這個可以算是你能在網上找到的關於瀏覽器如何訪問網站的最快的快速課程了。這節課程應該可以幫你更容易地理解本節的習題,如果你還是不明白,就到處找資料多多了解這方面的資訊,知道你明白為止。有一個很好的方法,就是你對照著上面的圖示,將你在《習題50》中創建的 web 程式中的內容分成幾個部分,讓其中的各部分對應到上面的圖示。如果你可以正確地將程式的各部分對應到這個圖示,你就大致開始明白它的運作原理了。
## 表單(form)的運作原理
熟悉「表單」最好的方法就是寫一個可以接收表單資料的程式出來,然後看你可以對它做些什麼。先將你的`lib/gothonsweb.rb` 修改成下面的樣子:
~~~
require_relative "gothonweb/version"
require "sinatra"
require "erb"
module Gothonweb
get '/' do
greeting = "Hello, World!"
erb :index, :locals => {:greeting => greeting}
end
get '/hello' do
name = params[:name] || "Nobody"
greeting = "Hello, #{name}"
erb :index, :locals => {:greeting => greeting}
end
end
~~~
重啟你的 Sinatra(按CTRL-C後重新運行),確認它有運行起來,然後使用瀏覽器訪問 http://localhost:4567/hello,這時瀏覽器應該會顯示 “I just wanted to say Hello , Nobody.”,接下來,將瀏覽器的地址改成 http://localhost:4567/hello?name=Frank,然後你可以看到頁面顯示為 “Hello, Frank.”,最後將 `name=Frank` 修改為你自己的名字,你就可以看到它對你說 Hello 了。
讓我們研究一下你的程式裡做過的修改。
1. 我們沒有直接為 greeting 賦值,而是使用了 `params` Hash 從瀏覽器獲取數據。這Sinatra 個函數會將一組在 URL `?` 後面的部份的 key / value 組加進 `prarms` Hash 裡。
2. 然後我從 `params[:name]` 中找到 `name` 的值,並為 `greeting` 賦值,這部份相信你已經很熟悉了。
3. 其他的內容和以前是一樣的,我們就不再分析了。
URL中該還可以包含多個參數。將本例的URL改成這樣子: `http://localhost:4567/hello?name=Frank&greet=Hola`。然後修改程式碼,讓它去存取 `prarams[:name]` 和 `params[:greet]`,如下所示:
~~~
greeting = "#{greet}, #{name}"
~~~
## 創建HTML表單
你可以通過URL參數實現表單提交,不過這樣看上去有些醜陋,而且不方便一般人使用,你真正需要的是一個「POST表單」,這是一種包含了`<form>`標籤的特殊 HTML 檔案。這種表單收集使用者輸入並將其傳遞給你的web程式,這和你上面實現的目的基本是一樣的。
讓我們來快速建立一個,從中你可以看出它的運作原理。你需要創建一個新的HTML文件,叫做 `lib/views/hello_form.erb`:
~~~
<html>
<head>
<title>Sample Web Form</title>
</head>
<body>
<h1>Fill Out This Form</h1>
<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">
<br/>
Your Name: <input type="text" name="name">
<br/>
<input type="submit">
</form>
</body>
</html>
~~~
然後將 `lib/gothonsweb.rb`改成這樣:
~~~
require_relative "gothonweb/version"
require "sinatra"
require "erb"
module Gothonweb
get '/' do
greeting = "Hello, World!"
erb :index, :locals => {:greeting => greeting}
end
get '/hello' do
erb :hello_form
end
post '/hello' do
greeting = "#{params[:greet] || "Hello"}, #{params[:name] || "Nobody"}"
erb :index, :locals => {:greeting => greeting}
end
end
~~~
都寫好以後,重啟 web 程式,然後通過你的瀏覽器訪問它。
這回你會看到一個表單,它要求你輸入「一個問候語句(A Greeting)」和「你的名字(Your Name)」,等你輸入完後點擊「提交(Submit)」按鈕,它就會輸出一個正常的問候頁面,不過這一次你的URL還是 http://localhost:4567/hello,並沒有添加參數進去。
在`hello_form.erb` 裡面關鍵的一行是`<form action="/hello" method="POST">`,它告訴你的瀏覽器以下內容:
1. 從表單中的各個欄位收集使用者輸入的資料。
2. 讓瀏覽器使用一種POST類型的請求,將這些資料發送給服務器。這是另外一種瀏覽器請求,它會將表單欄位「隱藏」起來。
3. 將這個請求發送至`/hello` URL,這是由`action="/hello"`告訴瀏覽器的。
4. 你可以看到兩段`<input>`標籤的名字屬性(name)和程式碼中的變數是對應的,另外我們在 class index 中使用的不再只是 GET 方法,而是另一個 POST 方法。
這個新程式的運作原理如下:
1. 瀏覽器訪問到 web 程式的 `/hello` 目錄,它發送了一個 GET 請求,於是我們的 `get '/hello/` 就運行了並傳回了hello_form。
2. 你填好了瀏覽器的表單,然後瀏覽器依照`<form>`中的要求,將資料通過POST 請求的方式發給web程式。
3. Web 程式運行了 `post '/hello'` 而不是不是 `get '/hello/`來處理這個請求。
4. 這個 `post '/hello'`完成了它正常的功能,將 `hello` 頁面返回,這裡並沒有新的東西,只是一個新函式名稱而已。
作為練習,在 `lib/views/index.erb` 中添加一個鏈接,讓它指向 `/hello`,這樣你可以反覆填寫並提交表單查看結果。確認你可以解釋清楚這個鏈接的工作原理,以及它是如何讓你實現在 `lib/views/index.erb` 和`lib/views/hello_form.erb`之間循環跳轉的,還有就是要明白你新修改過的 Ruby 程式碼,你需要知道在什麼情況下會運行到哪一部分程式碼。
## Creating A Layout Template
在你下一節練習建立遊戲的過程中,你需要建立很多的小 HTML 頁面。如果你每次都寫一個完整的網頁,你會很快感覺到厭煩的。幸運的是你可以建立一個「外觀 (layout」模板,也就是一種提供了通用的 headers 和 footers 的外殼模板,你可以用它將你所有的其他網頁包裹起來。好程式設計師會盡可能減少重複動作,所以要做一個好程式設計師,使用外觀模板是很重要的。
將 `lib/views/index.erb` 修改成這樣:
~~~
<% if greeting %>
<p>I just wanted to say <em style="color: green; font-size: 2em;"><%= greeting %></em>.
<% else %>
<em>Hello</em>, world!
<% end %>
~~~
然後把 `lib/views/hello_form.erb` 修改成這樣:
~~~
<h1>Fill Out This Form</h1>
<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">
<br/>
Your Name: <input type="text" name="name">
<br/>
<input type="submit">
</form>
~~~
面這些修改的目的,是將每一個頁面頂部和底部的反覆用到的「樣板 (boilerplate)」程式碼剝掉。這些被剝掉的程式碼會被放到一個單獨的`lib/views/layout.erb` 檔案中,從此以後,這些反覆用到的程式碼就由`lib/views/layout.erb` 來提供了。
上面的都改好以後,建立一個 `lib/views/layout.erb` 檔案,內容如下:
~~~
<html>
<head>
<title>Gothons From Planet Percal #25</title>
</head>
<body>
<%= yield %>
</body>
</html>
~~~
Sinatra 預設會自動去找名字為 `layout` 的外觀模板,並且使用它作為其他模板的「基礎」模板。你也可以修改已經用作任何頁面的基礎模板的 template。重啟你的程式觀察一下,然後試著用各種方法修改你的layout模板,不要修改你別的模板,看看輸出會有什麼樣的變化。
## 為表單撰寫自動測試程式碼
使用瀏覽器測試 web 程式是很容易的,只要點刷新按鈕就可以了。不過畢竟我們是程式設計師嘛,如果我們可以寫一些程式碼來測試我們的程式,為什麼還要重複手動測試呢?接下來你要做的,就是為你的web 程式寫一個小測試。這會用到你在《習題47》學過的一些東西,如果你不記得的話,可以回去複習一下。
我已經為此建立了一個簡單的小函式,讓你判斷(assert) web程序的響應,這個函數有一個很合適的名字,就叫 `assert_response`。創建一個 `tests/tools.rb` 檔案,內容如下:
~~~
require 'test/unit'
def assert_response(resp, contains=nil, matches=nil, headers=nil, status=200)
assert_equal(resp.status, status, "Expected response #{status} not in #{resp}")
if status == 200
assert(resp.body, "Response data is empty.")
end
if contains
assert((resp.body.include? contains), "Response does not contain #{contains}")
end
if matches
reg = Regexp.new(matches)
assert reg.match(contains), "Response does not match #{matches}"
end
if headers
assert_equal(resp.headers, headers)
end
end
~~~
最後,執行 `test/test_gothonsweb.rb` 去測試你的程式:
~~~
$ ruby test/test_gothonweb.rb
Loaded suite test/test_gothonweb
Started
.
Finished in 0.023839 seconds.
1 tests, 9 assertions, 0 failures, 0 errors, 0 skips
Test run options: --seed 57414
~~~
`rack/test` 函式庫包含了一串很簡單的 API 可以讓你處理請求。他們是 `get`, `put`, `post`, `delete`和 `head` 函式,模擬程式會遇到的各類類型請求。
所有假的 (mock) request 函式會有一樣的參數模式:
~~~
get '/path', params={}, rack_env={}
~~~
* `/path` 是 request 路徑,而且可以選擇性的包含一個 query string。
* `params` 是一組 query/post 的 Hash 參數,一個 request body 字串,或者是 nil
* `rack_env` 是一個 Rack 環境值 Hash。這可以用來設置 request 的 header 和其他 request 相關的資訊,例如 session 內的資料。
這樣的運作方式就不用實際運作一個真的 web 伺服器,如此一來你就可以使用自動化測試程式碼去測試,當然同時你也可以使用瀏覽器去測試一個執行中的伺服器。
為了驗證(validate) 函式的響應,你需要使用 `test/tools.rb` 中定義的`assert_response` 函式,裡面內容是:
To validate responses from this function, use the `assert_response` function from `test/tools.rb`which has:
~~~
assert_response(resp, contains=nil, matches=nil, headers=nil, status=200)
~~~
把你呼叫 `get` 或 `post` 得到的響應傳遞給這個函數,然後將你要檢查的內容作為參數傳遞給這個函數。你可以使用 `contains`參數來檢查響應中是否包含指定的值,使用 `status` 參數可以檢查指定的響應狀態。這個小函式其實包含了很多的資訊,所以你還是自己研究一下的比較好。
在 `test/test_gothonsweb.rb` 自動測試腳本中,我首先確認 `/foo` URL 傳回了一個「404 Not Found」響應,因為這個 URL其實是不存在的。然後我檢查了`/hello` 在 GET 和 POST 兩種請求的情況下都能正常運作。就算你沒有弄明白測試的原理,這些測試程式碼應該是很好讀懂的。
花一些時間研究一下這個最新版的web程式,重點研究一下自動測試的運作原理。
## 加分習題
1. 閱讀和HTML 相關的更多資料,然後為你的表單設計一個更好的輸出格式。你可以先在紙上設計出來,然後用HTML 去實現它。
2. 這是一道難題,試著研究一下如何進行檔案上傳,通過網頁上傳一張圖像,然後將其保存到磁碟中。
3. 更難的難題,找到 HTTP RFC 文件(講述HTTP 運作原理的技術文件),然後努力閱讀一下。這是一篇很無趣的文件,不過偶爾你會用到裡邊的一些知識。
4. 又是一道難題,找人幫你設置一個 web 伺服器,例如Apache、Nginx、或者thttpd。試著讓伺服器伺服一下你建立的.html 和.css 文件。如果失敗了也沒關係,web 服務器本來就都有點爛。
5. 完成上面的任務後休息一下,然後試著多建立一些 web 程式出來。你應該仔細閱讀 Sinatra 中關於會話(session)的內容,這樣你可以明白如何存留使用者的狀態資訊。
习题 50: 你的第一个网站
最后更新于:2022-04-01 05:40:35
這節以及後面的習題中,你的任務是把前面建立的遊戲做成網頁版。這是本書的最後三個章節,這些內容對你來說難度會相當大,你要在上面花些時間才能做出來。在你開始這節練習以前,你必須已經成功地完成過了《習題46》的內容,正確安裝了 **RubyGems**,而且學會瞭如何安裝軟體套件以及如何建立專案骨架。如果你不記得這些內容,就回到《習題46》重新複習一遍。
## 安裝 Sinatra
在建立你的第一個網頁應用程式之前,你需要安裝一個「Web框架」,它的名字叫 **Sinatra**。所謂的「框架」通常是指「讓某件事情做起來更容易的軟體套件」。在網頁應用的世界裡,人們建立了各種各樣的「網頁框架」,用來解決他們在建立網站時碰到的問題,然後把這些解決方案用軟體套件的方式發佈出來,這樣你就可以利用它們引導建立你自己的專案了。
可選的框架類型有很多很多,不過在這裡我們將使用 Sinatra 框架。你可以先學會它,等到差不多的時候再去接觸其它的框架,不過 Sinatra 本身挺不錯的,所以就算你一直使用也沒關係。
使用 `gem` 安裝 Sinatra:
~~~
$ gem install sinatra
Fetching: tilt-1.3.2.gem (100%)
Fetching: sinatra-1.2.6.gem (100%)
Successfully installed tilt-1.3.2
Successfully installed sinatra-1.2.6
2 gems installed
Installing ri documentation for tilt-1.3.2...
Installing ri documentation for sinatra-1.2.6...
Installing RDoc documentation for tilt-1.3.2...
Installing RDoc documentation for sinatra-1.2.6...
~~~
## 寫一個簡單的「Hello World」專案
現在你將做一個非常簡單的「Hello World」專案出來,首先你要建立一個專案目錄:
~~~
$ cd projects
$ bundle gem gothonweb
~~~
你最終的目的是把《習題42》中的遊戲做成一個 web 應用,所以你的專案名稱叫做 `gothonweb`,不過在此之前,你需要建立一個最基本的 Sinatra應用,將下面的代碼放到`lib/gothonweb.rb`中:
~~~
require_relative "gothonweb/version"
require "sinatra"
module Gothonweb
get '/' do
greeting = "Hello, World!"
return greeting
end
end
~~~
然後使用下面的方法來運行這個 web 程式:
~~~
$ ruby lib/gothonweb.rb
== Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick
[2011-07-18 11:27:07] INFO WEBrick 1.3.1
[2011-07-18 11:27:07] INFO ruby 1.9.2 (2011-02-18) [x86_64-linux]
[2011-07-18 11:27:07] INFO WEBrick::HTTPServer#start: pid=6599 port=4567
~~~
最後,使用你的網頁瀏覽器,打開 URL `http://localhost:4567/`,你應該看到兩樣東西,首先是瀏覽器裡顯示了 `Hello, world!`,然後是你的命令行終端顯示了如下的輸出:
~~~
127.0.0.1 - - [18/Jul/2011 11:29:10] "GET / HTTP/1.1" 200 12 0.0015
localhost - - [18/Jul/2011:11:29:10 EDT] "GET / HTTP/1.1" 200 12
- -> /
127.0.0.1 - - [18/Jul/2011 11:29:10] "GET /favicon.ico HTTP/1.1" 404 447 0.0008
localhost - - [18/Jul/2011:11:29:10 EDT] "GET /favicon.ico HTTP/1.1" 404 447
- -> /favicon.ico
~~~
這些是 Sinatra 印出的 log 資訊,從這些資訊你可以看出服務器有在運行,而且能了解到程式在瀏覽器背後做了些什麼事情。這些資訊還有助於你發現程式的問題。例如在最後一行它告訴你瀏覽器試圖存取 `/favicon.ico`,但是這個文件並不存在,因此它返回的狀態碼是 `404 Not Found`。
到這裡,我還沒有講到任何 web 相關的工作原理,因為首先你需要完成準備工作,以便後面的學習能順利進行,接下來的兩節習題中會有詳細的解釋。我會要求你用各種方法把你的 Sinatra 應用程式弄壞,然後再將其重新構建起來:這樣做的目的是讓你明白運行 Sinatra 程式需要準備好哪些東西。
## 發生了什麼事情?
在瀏覽器訪問到你的網頁應用程式時,發生了下面一些事情:
1. 瀏覽器通過網路連接到你自己的電腦,它的名字叫做 `localhost`,這是一個標準稱謂,表示的誰就是網路中你自己的這台電腦,不管它實際名字是什麼,你都可以使用 `localhost`來訪問。它使用到`port 4567`。
2. 連接成功以後,瀏覽器對 lib/gothonweb.rb`這個應用程式發出了HTTP請求(request),要求訪問URL`/`,這通常是一個網站的第一個URL。
3. 在`lib/gothonweb.rb` 裡,我們有一個程式碼區段,裡面包含了 URL 的匹配關係。我們這裡只定義了一組匹配,那就是「/」。它的含義是:如果有人使用瀏覽器訪問 `/` 這一級目錄,Sinatra 將找到它,從而用它處理這個瀏覽器請求。
4. Sinatra 呼叫匹配到的程式碼區段,這段程式碼只簡單的回傳了一個字串傳回給瀏覽器。
5. 最後 Sinatra 完成了對於瀏覽器請求的處理將響應(response)回傳給瀏覽器,於是你就看到了現在的頁面。
確定你真的弄懂了這些,你需要畫一個示意圖,來理清資訊是如何從瀏覽器傳遞到 Sinata,再到 `/`區段,再回到你的瀏覽器的。
## 修正錯誤
第一步,把第 6 行的 `greeting` 變數刪掉,然後重新刷瀏覽器。你應該會看到一個錯誤畫面,你可以通過這一頁豐富的資訊看出你的程式崩潰的原因。當然你已經知道出錯的原因是 `greeting`的賦值遺失了,不過 Sinatra還是會給你一個挺好的錯誤頁面,讓你能找到出錯的具體位置。試試在這個錯誤頁面上做以下操作:
1. 看看 `sinatra.error` 變數。
2. 看看 `REQUEST_` 變數裡的資訊。裡面哪些知識是你已經熟悉了的。這是瀏覽器發給你的 gothonweb 應用程式的資訊。這些知識對於日常網頁瀏覽沒有什麼用處,但現在你要學會這些東西,以便寫出web應用程式來。
## 建立基本的模板
你已經試過用各種方法把這個Sinatra 程式改錯,不過你有沒有注意到「Hello World」不是一個好 HTML 網頁呢?這是一個 web 應用,所以需要一個合適的HTML 響應頁面才對。為了達到這個目的,下一步你要做的是將「Hello World」以較大的綠色字體顯示出來。
第一步是建立一個 `lib/views/index.erb` 檔案,內容如下:
~~~
<html>
<head>
<title>Gothons Of Planet Percal #25</title>
</head>
<body>
<% if greeting %>
<p>I just wanted to say <em style="color: green; font-size: 2em;"><%= greeting %></em>.
<% else %>
<em>Hello</em>, world!
<% end %>
</body>
</html>
~~~
什麼是一個 `.erb` 的檔案?ERB 的全名是 **E**mbedded **R**u**b**y。`.erb` 檔案其實是一個內嵌一點 Ruby 程式碼的 HTML。如果你學過HTML的話,這些內容你看上去應該很熟悉。如果你沒學過HTML,那你應該去研究一下,試著用HTML寫幾個網頁,從而知道它的運作原理。既然這是一個 `erb` 模版,Sinatra 就會在模板中找到對應的位置,將參數的內容填充到模板中。例如每一個出現 ` 的位置,內容都會被替換成對應這個變數名的參數。
為了讓你的 `lib/gothonweb.rb` 處理模板,你需要寫一寫程式碼,告訴Sinatra 到哪裡去找到模板進行加載,以及如何渲染(render)這個模板,按下面的方式修改你的檔案:
~~~
require_relative "gothonweb/version"
require "sinatra"
require "erb"
module Gothonweb
get '/' do
greeting = "Hello, World!"
erb :index, :locals => {:greeting => greeting}
end
end
~~~
特別注意我改了 `/` 這個程式碼區段最後一行的內容,這樣它就可以呼叫 `erb` 然後把 greeting 變數傳給它。
改好上面的程式後,刷新一下瀏覽器中的網頁,你應該會看到一條和之前不同的綠色資訊輸出。你還可以在瀏覽器中通過「查看原始碼(View Source)」看到模板被渲染成了標準有效的HTML 原始碼。
這麼講也許有些太快了,我來詳細解釋一下模板的運作原理吧:
1. 在 `lib/gothonweb.rb` 你添加了一個 `erb` 函式呼叫。
2. 這個 `erb` 函式知道怎麼載入 `lib/views` 目錄夾裡的 `.erb` 的檔案。它知道去抓哪些檔案(在這個例子裡是 `index.erb`)。因為你傳了一個參數進去(`erb :index …`)。
3. 現在,當瀏覽器讀取 `/` 且 `lib/gothonweb.eb` 匹配然後執行 `get '/' do` 區段,它再也沒有只是回傳字串 `greeting`,而是呼叫 `erb` 然後傳入 `greeting` 作為一個變數。
4. 最後,你讓 `lib/views/index.erb` 去檢查 `greeting` 這個變數,如果這個變數存在的話,就印出變數裡的內容。如果不存在的話,就會印出一個預設的訊息。
要深入理解這個過程,你可以修改 `greeting 變數以及 HTML ,看看會友什麼效果。然後也創作另外一個叫做`lib/views/foo.erb`的模板。然後把`erb :index`改成`erb :foo`。從這個過程中你也可以看到,你傳入給`erb`的第一個參數只要匹配到`lib/views`下的`.erb` 檔案名稱,就可以被渲染出來了。
## 加分習題
1. 到 [Sinatra](http://www.sinatrarb.com/) 這個框架的官方網站去閱讀更多文件。
2. 實驗一下你在上述網站中看到的所有東西,包括他們的範例程式碼。
3. 閱讀有關於 HTML5 和 CSS3 相關的東西,自己練習寫幾個 `.html` 和 `.css` 文件。
4. 如果你有一個懂 **Rails** 的朋友可以幫你的畫,你可以自己試著使用 Rails 完成一下習題 50,51,52,看看結果會是什麼樣子。
习题 49: 创造句子
最后更新于:2022-04-01 05:40:33
從我們這個小遊戲的詞彙掃描器中,我們應該可以得到類似下面的列表(你的看起來可能格式會不太一樣):
~~~
ruby-1.9.2-p180 :003 > print Lexicon.scan("go north")
[#<struct Lexicon::Pair token=:verb, word="go">,
#<struct Lexicon::Pair token=:direction, word="north">] => nil
ruby-1.9.2-p180 :004 > print Lexicon.scan("kill the princess")
[#<struct Lexicon::Pair token=:verb, word="kill">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="princess">] => nil
ruby-1.9.2-p180 :005 > print Lexicon.scan("eat the bear")
[#<struct Lexicon::Pair token=:verb, word="eat">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="bear">] => nil
ruby-1.9.2-p180 :006 > print Lexicon.scan("open the door and smack the bear in the nose")
[#<struct Lexicon::Pair token=:error, word="open">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="door">,
#<struct Lexicon::Pair token=:error, word="and">,
#<struct Lexicon::Pair token=:error, word="smack">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="bear">,
#<struct Lexicon::Pair token=:stop, word="in">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:error, word="nose">] => nil
ruby-1.9.2-p180 :007 >
~~~
現在讓我們把它轉化成遊戲可以使用的東西,也就是一個 Sentence 類。
如果你還記得學校學過的東西的話,一個句子是由這樣的結構組成的:
> 主語(Subject) + 謂語(動詞Verb) + 賓語(Object)
很顯然實際的句子可能會比這複雜,而你可能已經在英語的語法課上面被折騰得夠嗆了。我們的目的,是將上面的 struct 列表轉換為一個 Sentence 物件,而這個對象又包含主謂賓各個成員。
## 匹配(Match) And 窺視(Peek)
為了達到這個效果,你需要四樣工具:
1. 一個循環存取 struct 列表的方法,這挺簡單的。
2. 「匹配」我們的主謂賓設置中不同種類 struct 的方法。
3. 一個「窺視」潛在struct的方法,以便做決定時用到。
4. 「跳過(skip)」我們不在乎的內容的方法,例如形容詞、冠詞等沒有用處的詞彙。
5. 我們使用 peek 函式查看 struct 列表中的下一個成員,做匹配以後再對它做下一步動作。讓我們先看看這個 peek 函式:
~~~
def peek(word_list)
begin
word_list.first.token
rescue
nil
end
end
~~~
很簡單。再看看 match 函式:
~~~
def match(word_list, expecting)
begin
word = word_list.shift
if word.token == expecting
word
else
nil
end
rescue
nil
end
end
~~~
還是很簡單,最後我們看看 skip 函式:
~~~
def skip(word_list, word_type)
while peek(word_list) == word_type
match(word_list, word_type)
end
end
~~~
以你現在的水準,你應該可以看出它們的功能來。確認自己真的弄懂了它們。
## 句子的語法
有了工具,我們現在可以從 struct 列表來構建句子(Sentence)對象了。我們的處理流程如下:
1. 使用 `peek` 識別下一個單詞。
2. 如果這個單詞和我們的語法匹配,我們就調用一個函式來處理這部分語法。假設函式的名字叫 `parse_subject` 好了。
3. 如果語法不匹配,我們就 `raise` 一個錯誤,接下來你會學到這方面的內容。
4. 全部分析完以後,我們應該能得到一個 Sentence 物件,然後可以將其應用在我們的遊戲中。
演示這個過程最簡單的方法是把程式碼展示給你讓你閱讀,不過這節習題有個不一樣的要求,前面是我給你測試程式碼,你照著寫出程式碼來,而這次是我給你的程序,而你要為它寫出測試程式碼來。
以下就是我寫的用來解析簡單句子的程式碼,它使用了 `ex48` 這個 Lexicon class。
~~~
class ParserError < Exception
end
class Sentence
def initialize(subject, verb, object)
# remember we take Pair.new(:noun, "princess") structs and convert them
@subject = subject.word
@verb = verb.word
@object = object.word
end
end
def peek(word_list)
begin
word_list.first.token
rescue
nil
end
end
def match(word_list, expecting)
begin
word = word_list.shift
if word.token == expecting
word
else
nil
end
rescue
nil
end
end
def skip(word_list, token)
while peek(word_list) == token
match(word_list, token)
end
end
def parse_verb(word_list)
skip(word_list, :stop)
if peek(word_list) == :verb
return match(word_list, :verb)
else
raise ParserError.new("Expected a verb next.")
end
end
def parse_object(word_list)
skip(word_list, :stop)
next_word = peek(word_list)
if next_word == :noun
return match(word_list, :noun)
end
if next_word == :direction
return match(word_list, :direction)
else
raise ParserError.new("Expected a noun or direction next.")
end
end
def parse_subject(word_list, subj)
verb = parse_verb(word_list)
obj = parse_object(word_list)
return Sentence.new(subj, verb, obj)
end
def parse_sentence(word_list)
skip(word_list, :stop)
start = peek(word_list)
if start == :noun
subj = match(word_list, :noun)
return parse_subject(word_list, subj)
elsif start == :verb
# assume the subject is the player then
return parse_subject(word_list, Pair.new(:noun, "player"))
else
raise ParserError.new("Must start with subject, object, or verb not: #{start}")
end
end
~~~
## 關於異常(Exception)
你已經簡單學過關於異常的一些東西,但還沒學過怎樣拋出(raise)它們。這節的程式碼示範了如何 raise。首先在最前面,你要定義好 `ParserException`這個類,而它又是 `Exception` 的一種。另外要注意我們是怎樣使用 `raise`這個關鍵字來拋出異常的。
你的測試程式碼應該也要測試到這些異常,這個我也會示範給你如何實現。
## 你應該測試的東西
為《習題49》寫一個完整的測試方案,確認程式碼中所有的東西都能正常工作,其中異常的測試——輸入一個錯誤的句子它會拋出一個異常來。
使用 `assert_raises` 這個函式來檢查異常,在 Test::Unit 的文件裡查看相關的內容,學著使用它寫針對「執行失敗」的測試,這也是測試很重要的一個方面。從文件中學會使用 `assert_raises`,以及一些別的函式。
寫完測試以後,你應該就明白了這段程式碼的運作原理,而且也學會了如何為別人的程式碼寫測試程式碼。相信我,這是一個非常有用的技能。
## 加分習題
1. 修改 `parse_` method,將它們放到一個類裡邊,而不僅僅是獨立的方法函式。這兩種設計你喜歡哪一種呢?
2. 提高parser 對於錯誤輸入的抵禦能力,這樣即使使用者輸入了你預定義語彙之外的詞語,你的程式碼也能正常運行下去。
3. 改進語法,讓它可以處理更多的東西,例如數字。
4. 想想在遊戲裡你的 Sentence 類可以對使用者輸入做哪些有趣的事情。
习题 48: 更进阶的使用者输入
最后更新于:2022-04-01 05:40:30
你的遊戲可能一路跑得很爽,不過你處理使用者輸入的方式肯定讓你不勝其煩了。每一個房間都需要一套自己的語句,而且只有使用者完全輸入正確後才能執行。你需要一個設備,它可以允許使用者以各種方式輸入語彙。例如下面的幾種表述都應該被支援才對:
* open door
* open the door
* go THROUGH the door
* punch bear
* Punch The Bear in the FACE
也就是說,如果使用者的輸入和常用英語很接近也應該是可以的,而你的遊戲要識別出它們的意思。為了達到這個目的,我們將寫一個模組專門做這件事情。這個模組裡邊會有若干個類,它們互相配合,接受使用者輸入,並且將使用者輸入轉換成你的遊戲可以識別的命令。
英語的簡單格式是這個樣子的:
* 單詞由空格隔開。
* 句子由單詞組成。
* 語法控制句子的含義。
所以最好的開始方式是先搞定如何得到使用者輸入的詞彙,並且判斷出它們是什麼。
## 我們的遊戲語彙
我在遊戲裡建立了下面這些語彙:
* 表示方向: north, south, east, west, down, up, left, right, back.
* 動詞: go, stop, kill, eat.
* 修飾詞: the, in, of, from, at, it
* 名詞: door, bear, princess, cabinet.
* 數字詞: 由 0-9 構成的數字。
說到名詞,我們會碰到一個小問題,那就是不一樣的房間會用到不一樣的一組名詞,不過讓我們先挑一小組出來寫程式,以後再做改進吧。
## 如何斷句
我們已經有了詞彙表,為了分析句子的意思,接下來我們需要找到一個斷句的方法。我們對於句子的定義是「空格隔開的單詞」,所以只要這樣就可以了:
~~~
stuff = gets.chomp()
words = stuff.split()
~~~
目前做到這樣就可以了,不過這招在相當一段時間內都不會有問題。
## 語彙結構
一旦我們知道瞭如何將句子轉化成詞彙列表,剩下的就是逐一檢查這些詞彙,看它們是什麼類型。為了達到這個目的,我們將用到一個非常便利的 Ruby 資料結構「struct」。「struct」其實就是一個可以把一串的 attrbutes 綁在一起的方式,使用 accessor 函式,但不需要寫一個複雜的 class。它的建立方式就像這樣:
~~~
Pair = Struct.new(:token, :word)
first_word = Pair.new("direction", "north")
second_word = Pair.new("verb", "go")
sentence = [first_word, second_word]
~~~
這建立了一對 (TOKEN, WORD) 可以讓你看到 word 和在裡面做事。
這只是一個例子,不過最後做出來的樣子也差不多。你接受使用者輸入,用split 將其分隔成單詞列表,然後分析這些單詞,識別它們的類型,最後重新組成一個句子。
## 掃描輸入資料
現在你要寫的是詞彙掃描器。這個掃描器會將使用者的輸入字符串當做參數,然後返回由多個(TOKEN, WORD) struct 組成的列表,這個列表實現類似句子的功能。如果一個單詞不在預定的詞彙表中,那它返回時 WORD 應該還在,但TOKEN 應該設置成一個專門的錯誤標記。這個錯誤標記將告訴使用者哪裡出錯了。
有趣的地方來了。我不會告訴你這些該怎樣做,但我會寫一個「單元測試(unit test)」,而你要把掃描器寫出來,並保證單元測試能夠正常通過。
## Exceptions And Numbers
有一件小事情我會先幫幫你,那就是數字轉換。為了做到這一點,我們會作一點弊,使用「異常(exceptions)」來做。「異常」指的是你運行某個函數時得到的錯誤。你的函數在碰到錯誤時,就會「提出(raise)」一個「異常」,然後你就要去處理(handle)這個異常。假如你在 IRB 裡寫了這些東西:
~~~
ruby-1.9.2-p180 :001 > Integer("hell")
ArgumentError: invalid value for Integer(): "hell"
from (irb):1:in `Integer'
from (irb):1
from /home/rob/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `<main>'
~~~
這個 `ArgumentError` 就是 `Integer()` 函式拋出的一個異常。因為你給`Integer()` 的參數不是一個數字。`Integer()`函數其實也可以傳回一個值來告訴你它碰到了錯誤,不過由於它只能傳回整數值,所以很難做到這一點。它不能返回-1,因為這也是一個數字。 `Integer()`沒有糾結在它「究竟應該返回什麼」上面,而是提出了一個叫做`TypeError`的異常,然後你只要處理這個異常就可以了。
處理異常的方法是使用 `begin` 和 `rescue` 這兩個關鍵字:
~~~
def convert_number(s)
begin
Integer(s)
rescue ArgumentError
nil
end
end
~~~
你把要試著運行的程式碼放到「begin」的區段裡,再將出錯後要運行的程式碼放到「except」區段裡。在這裡,我們要試著呼叫 `Integer()` 去處理某個可能是數字的東西,如果中間出了錯,我們就「rescue」這個錯誤,然後返回 「nil」。
在你寫的掃描器裡面,你應該使用這個函數來測試某個東西是不是數字。做完這個檢查,你就可以聲明這個單詞是一個錯誤單詞了。
## What You Should Test
這裡是你應該使用的測試檔案 `test/test_lexicon.rb`:
~~~
require 'test/unit'
require_relative "../lib/lexicon"
class LexiconTests < Test::Unit::TestCase
Pair = Lexicon::Pair
@@lexicon = Lexicon.new()
def test_directions()
assert_equal([Pair.new(:direction, 'north')], @@lexicon.scan("north"))
result = @@lexicon.scan("north south east")
assert_equal(result, [Pair.new(:direction, 'north'),
Pair.new(:direction, 'south'),
Pair.new(:direction, 'east')])
end
def test_verbs()
assert_equal(@@lexicon.scan("go"), [Pair.new(:verb, 'go')])
result = @@lexicon.scan("go kill eat")
assert_equal(result, [Pair.new(:verb, 'go'),
Pair.new(:verb, 'kill'),
Pair.new(:verb, 'eat')])
end
def test_stops()
assert_equal(@@lexicon.scan("the"), [Pair.new(:stop, 'the')])
result = @@lexicon.scan("the in of")
assert_equal(result, [Pair.new(:stop, 'the'),
Pair.new(:stop, 'in'),
Pair.new(:stop, 'of')])
end
def test_nouns()
assert_equal(@@lexicon.scan("bear"), [Pair.new(:noun, 'bear')])
result = @@lexicon.scan("bear princess")
assert_equal(result, [Pair.new(:noun, 'bear'),
Pair.new(:noun, 'princess')])
end
def test_numbers()
assert_equal(@@lexicon.scan("1234"), [Pair.new(:number, 1234)])
result = @@lexicon.scan("3 91234")
assert_equal(result, [Pair.new(:number, 3),
Pair.new(:number, 91234)])
end
def test_errors()
assert_equal(@@lexicon.scan("ASDFADFASDF"), [Pair.new(:error, 'ASDFADFASDF')])
result = @@lexicon.scan("bear IAS princess")
assert_equal(result, [Pair.new(:noun, 'bear'),
Pair.new(:error, 'IAS'),
Pair.new(:noun, 'princess')])
end
end
~~~
記住你要使用你的專案骨架來建立新專案項目,將這個 Test Case 寫下來(不許複製貼上!),然後編寫你的掃描器,直至所有的測試都能通過。注意細節並確認結果一切工作良好。
## 設計提示
集中一次實現一個測試,盡量保持簡單,只要把你的 `lexicon.rb` 詞彙表中所有的單詞放那裡就可以了。不要修改輸入的單詞表,不過你需要建立自己的新列表,裡邊包含你的語彙元組。另外,記得使用 `include?` 函式來檢查這些語彙陣列,以確認某個單詞是否在你的語彙表中。
## 加分習題
1. 改進單元測試,讓它覆蓋到更多的語彙。
2. 向語彙列表添加更多的語彙,並且更新單元測試程式碼。
3. 讓你的掃描器能夠識別任意大小寫的詞彙。更新你的單元測試以確認其功能。
4. 找出另外一種轉換為數字的方法。
5. 我的解決方案用了37 行程式碼,你的是更長還是更短呢?
习题 47: 自动化测试
最后更新于:2022-04-01 05:40:28
為了確認遊戲的功能是否正常,你需要一遍一遍地在你的遊戲中輸入命令。這個過程是很枯燥無味的。如果能寫一小段程式碼用來測試你的程式碼豈不是更好?然後只要你對程序做了任何修改,或者添加了什麼新東西,你只要「跑一下你的測試」,而這些測試能確認程序依然能正確運行。這些自動測試不會抓到所有的bug,但可以讓你無需重複輸入命令運行你的程式碼,從而為你節約很多時間。
從這一章開始,以後的練習將不會有「你應該看到的結果」這一節,取而代之的是一個「你應該測試的東西」一節。從現在開始,你需要為自己寫的所有程式碼寫自動化測試,而這將讓你成為一個更好的程序員。
我不會試圖解釋為什麼你需要寫自動化測試。我要告訴你的是,你想要成為一個程式設計師,而程序的作用是讓無聊冗繁的工作自動化,測試軟件毫無疑問是無聊冗繁的,所以你還是寫點程式碼讓它為你測試的更好。
這應該是你需要的所有的解釋了。因為你寫單元測試的原因是讓你的大腦更加強健。你讀了這本書,寫了很多程式碼讓它們實現一些事情。現在你將更進一步,寫出懂得你寫的其他程式碼的程式碼。這個寫程式碼測試你寫的其他程式碼的過程將強迫你清楚的理解你之前寫的程式碼。這會讓你更清晰地了解你寫的程式碼實現的功能及其原理,而且讓你對細節的注意更上一個台階。
## 撰寫 Test Case
我們將拿一段非常簡單的程式碼為例,寫一個簡單的測試,這個測試將建立在上節我們創建的項目骨架上面。
首先從你的專案骨架創建一個叫做 `ex47` 的專案。確認該改名稱的地方都有改過,尤其是 `tests/ex47_tests.rb` 這處不要寫錯。
接下來建立一個簡單的 `ex47/lib/game.rb` 檔案,裡邊放一些用來被測試的程式碼。我們現在放一個傻乎乎的小 class 進去,用來作為我們的測試對象:
~~~
class Room
attr_accessor :name, :description, :paths
def initialize(name, description)
@name = name
@description = description
@paths = {}
end
def go(direction)
@paths[direction]
end
def add_paths(paths)
@paths.update(paths)
end
end
~~~
一旦你有了這個檔案,修改你的 unit test 骨架變成這樣:
~~~
require 'test/unit'
require_relative '../lib/ex47'
class MyUnitTests < Test::Unit::TestCase
def test_room()
gold = Room.new("GoldRoom",
"""This room has gold in it you can grab. There's a
door to the north.""")
assert_equal(gold.name, "GoldRoom")
assert_equal(gold.paths, {})
end
def test_room_paths()
center = Room.new("Center", "Test room in the center.")
north = Room.new("North", "Test room in the north.")
south = Room.new("South", "Test room in the south.")
center.add_paths({:north => north, :south => south})
assert_equal(center.go(:north), north)
assert_equal(center.go(:south), south)
end
def test_map()
start = Room.new("Start", "You can go west and down a hole.")
west = Room.new("Trees", "There are trees here, you can go east.")
down = Room.new("Dungeon", "It's dark down here, you can go up.")
start.add_paths({:west => west, :down => down})
west.add_paths({:east => start})
down.add_paths({:up => start})
assert_equal(start.go(:west), west)
assert_equal(start.go(:west).go(:east), start)
assert_equal(start.go(:down).go(:up), start)
end
end
~~~
這個文件 require 了你在 `lib/ex47.rb` 裡建立的 `Room`這個類,接下來我們要做的就是測試它。於是我們看到一系列的以 `test_` 開頭的測試函式,它們就是所謂的「Test Case」,每一個Test Case裡面都有一小段程式碼,它們會建立一個或者一些房間,然後去確認房間的功能和你期望的是否一樣。它測試了基本的房間功能,然後測試了路徑,最後測試了整個地圖。
這裡最重要的函數時 `assert_equal`,它保證了你設置的變數,以及你在`Room` 裡設置的路徑和你的期望相符。如果你得到錯誤的結果的話,Ruby 的 `Test::Unit` 模組將會印出一個錯誤信息,這樣你就可以找到出錯的地方並且修正過來。
## 測試指南
在寫測試程式碼時,你可以照著下面這些不是很嚴格的指南來做:
1. 測試腳本要放到 `tests/` 目錄下,並且命名為 `test_NAME.rb`。這樣做還有一個好處就是防止測試程式碼和別的程式碼互相混掉。
2. 為你的每一個模組寫一個測試。
3. Test Cast 函式保持簡短,但如果看上去不怎麼整潔也沒關係,Test Cast一般都有點亂。
4. 就算Test Cast有些亂,也要試著讓他們保持整潔,把裡邊重複的程式碼刪掉。建立一些輔助函數來避免重複的程式碼。當你下次在改完程式碼需要改測試的時候,你會感謝我這一條建議的。重複的程式碼會讓修改測試變得很難操作。
5. 最後一條是別太把測試當做一回事。有時候,更好的方法是把程式碼和測試全部刪掉,然後重新設計程式碼。
## 你應該看到的結果
~~~
$ ruby test_ex47.rb
Loaded suite test_ex47
Started
...
Finished in 0.000353 seconds.
3 tests, 7 assertions, 0 failures, 0 errors, 0 skips
Test run options: --seed 63537
~~~
That’s what you should see if everything is working right. Try causing an error to see what that looks like and then fix it.
## 加分習題
1. 仔細閱讀 `Test::Unit`相關的文件,再去了解一下其他的替代方案。
2. 了解一下 `Rspec`,看看它是否可以幹得更出色。
3. 改進你遊戲裡的 Room,然後用它重建你的遊戲。這次重寫,你需要一邊寫程式碼,一般把單元測試寫出來。
习题 46: 一个专案骨架
最后更新于:2022-04-01 05:40:26
這裡你將學會如何建立一個專案「骨架」目錄。這個骨架目錄具備讓專案跑起來的所有基本內容。它裡邊會包含你的專案檔案佈局、自動化測試程式碼,模組,以及安裝腳本。當你建立一個新專案的時候,只要把這個目錄複製過去,改改目錄的名字,再編輯裡面的檔案就行了。
## 骨架內容: Linux/OSX
首先使用下述命令創建你的骨架目錄:
~~~
$ mkdir -p projects
$ cd projects/
$ mkdir skeleton
$ cd skeleton
$ mkdir bin lib lib/NAME test
~~~
我使用了一個叫 projects 的目錄,用來存放我自己的各個專案。然後我在裡邊建立了一個叫做 skeleton 的檔案夾,這就是我們新專案的基礎目錄。其中叫做 NAME 的檔案夾是你的專案的主檔案夾,你可以將它任意取名。
接下來我們要配置一些初始檔案:
~~~
$ touch lib/NAME.rb
$ touch lib/NAME/version.rb
~~~
然後我們可以建立一個 `NAME.gemspec` 的檔案在我們的專案的根目錄,這個檔案在安裝專案的時候我們會用到它:
~~~
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "NAME/version"
Gem::Specification.new do |s|
s.name = "NAME"
s.version = NAME::VERSION
s.authors = ["Rob Sobers"]
s.email = ["rsobers@gmail.com"]
s.homepage = ""
s.summary = %q{TODO: Write a gem summary}
s.description = %q{TODO: Write a gem description}
s.rubyforge_project = "NAME"
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
end
~~~
編輯這個檔案,把自己的聯絡方式寫進去,然後放到那裡就行了。
最後你需要一個簡單的測試專用(我們將會在下一節中提到 Test )的骨架檔案叫 `test/test_NAME.rb`:
~~~
require 'test/unit'
class MyUnitTests < Test::Unit::TestCase
def setup
puts "setup!"
end
def teardown
puts "teardown!"
end
def test_basic
puts "I RAN!"
end
end
~~~
## 安裝 Gems
Gems 是 Ruby 的套件系統,所以你需要知道怎麼安裝它和使用它。不過問題就來了。我的本意是讓這本書越清晰越乾淨越好,不過安裝軟體的方法是在是太多了,如果我要一步一步寫下來,那10 頁都寫不完,而且告訴你吧,我本來就是個懶人。
所以我不會提供詳細的安裝步驟了,我只會告訴你需要安裝哪些東西,然後讓你自己搞定。這對你也有好處,因為你將打開一個全新的世界,裡邊充滿了其他人發佈的軟體。
接下來你需要安裝下面的軟體套件:
* git - [http://git-scm.com/](http://git-scm.com/)
* rake - [http://rake.rubyforge.org/](http://rake.rubyforge.org/)
* rvm - [https://rvm.beginrescueend.com/](https://rvm.beginrescueend.com/)
* rubygems - [http://rubygems.org/pages/download](http://rubygems.org/pages/download)
* bundler - [http://gembundler.com/](http://gembundler.com/)
不要只是手動下載並且安裝這些軟體套件,你應該看一下別人的建議,尤其看看針對你的操作系統別人是怎樣建議你安裝和使用的。同樣的軟體套件在不一樣的操作系統上面的安裝方式是不一樣的,不一樣版本的 Linux 和 OSX 會有不同,而 Windows 更是不同。
我要預先警告你,這個過程會是相當無趣。在業內我們將這種事情叫做「yak shaving(剃犛牛)」。它指的是在你做一件有意義的事情之前的一些準備工作,而這些準備工作又是及其無聊冗繁的。你要做一個很酷的 Ruby 專案,但是創建骨架目錄需要你安裝一些軟體到件,而安裝軟體套件之前你還要安裝package installer (軟件套件安裝工具),而要安裝這個工具你還得先學會如何在你的操作系統下安裝軟體,真是煩不勝煩呀。
無論如何,還是克服困難吧。你就把它當做進入程式俱樂部的一個考驗。每個程式設計師都會經歷這條道路,在每一段「酷」的背後總會有一段「煩」的。
## 使用這個骨架
剃犛牛的事情已經做的差不多了,以後每次你要新建一個專案時,只要做下面的事情就可以了:
1. 拷貝這份骨架目錄,把名字改成你新專案的名字。
2. 再將 `NAME`模組和 `NAME.rb` 更名為你需要的名字,它可以是你專案的名字,當然別的名字也行。
3. 編輯你的 `NAME.gemspec` 檔案,讓它包含你新專案的相關資訊。
4. 重命名 `test/test_NAME.rb`,讓它的名字匹配到你模組的名字。
5. 開始寫程式吧。
## 小測驗
這節練習沒有加分習題,不過需要你做一個小測驗:
1. 找文件閱讀,學會使用你前面安裝了的軟體套件。
2. 閱讀關於`NAME.gemspec` 的文件,看它裡邊可以做多少配置。
3. 建立一個專案,在 `NAME.rb` 裡寫一些程式碼。
4. 在 `bin` 目錄下放一個可以運行的腳本,找材料學習一下怎樣建立可以在系統下運行的 Ruby 腳本。
5. 確定你建立的 `bin` 教本,有在 `NAME.gemspec` 中被參照到,這這樣你安裝時就可以連它安裝進去。
6. 使用你的 `NAME.gemspec` 和 `gem build`、`gem install` 來安裝你寫的程式和確定它能用。然後使用`gem uninstall` 去移除它。
7. 弄懂如何使用 Bundler 來自動建立一個骨架目錄。
习题 45: 物件、类和从属关系
最后更新于:2022-04-01 05:40:24
有一個重要的概念你需要弄明白,那就是 `Class`「類」和 `Object`「物件」的區別。問題在於,class 和 object 並沒有真正的不同。它們其實是同樣的東西,只是在不同的時間名字不同罷了。我用禪語來解釋一下吧:
`魚(Fish)和鮭魚(Salmon)有什麼區別?`
這個問題有沒有讓你有點暈呢?說真的,坐下來想一分鐘。我的意思是說,魚和鮭魚是不一樣,不過它們其實也是一樣的是不是?泥鰍是魚的一種,所以說沒什麼不同,不過泥鰍又有些特別,它和別的種類的魚的確不一樣,比如鮭魚和比目魚就不一樣。所以鮭魚和魚既相同又不同。怪了。
這個問題讓人暈的原因是大部分人不會這樣去思考問題,其實每個人都懂這一點,你無須去思考魚和鮭魚的區別,因為你知道它們之間的關係。你知道鮭魚是魚的一種,而且魚還有別的種類,根本就沒必要去思考這類問題。
讓我們更進一步,假設你有一隻水桶,裡邊有三條鮭魚。假設你的好人卡多到沒地方用,於是你給它們分別取名叫Frank,Joe,Mary。現在想想這個問題:
`Mary 和鮭魚有什麼區別?`
這個問題一樣的奇怪,但比起魚和鮭魚的問題來還好點。你知道 Mary是一條鮭魚,所以他並沒什麼不同,他只是鮭魚的一個「實例(instance)」。Joe 和Frank 一樣也是鮭魚的實例。我的意思是說,它們是由鮭魚創建出來的,而且代表著和鮭魚一樣的屬性。
所以我們的思維方式是(你可能會有點不習慣):魚是一個「類(class)」,鮭魚是一個「類(class)」,而 Mary 是一個「物件(object)」。仔細想想,然後我再一點一點慢慢解釋給你。
魚是一個「類」,表示它不是一個真正的東西,而是一個用來描述具有同類屬性的實例的概括性詞彙。你有鰭?你有鰾?你住在水裡?好吧那你就是一條魚。
後來一個博士路過,看到你的水桶,於是告訴你:「小伙子,你這些魚是鮭魚。」 專家一出,真相即現。並且專家還定義了一個新的叫做「鮭魚」的「類」,而這個「類」又有它特定的屬性。長鼻子?紅肉?體型大?住在海裡或是乾淨新鮮的水裡?吃起來味道不錯?那你就是一條鮭魚。
最後一個廚師過來了,他跟博士說:「非也非也,你看到的是鮭魚,我看到的是Mary,而且我要把 Mary 淋上美味醬料做一道小菜。 」於是你就有了一隻叫做Mary 的鮭魚的「實例(instance)」(鮭魚也是魚的一個「實例」),並且你使用了它(把它塞到你的胃裡了),這樣它就是一個「物件(object)」。
這會你應該了解了:Mary 是鮭魚的成員,而鮭魚又是魚的成員。這裡的關係式:物件屬於某個類,而某個類又屬於另一個類。
## 寫成程式碼是什麼樣子
這個概念有點詭異,不過實話說,你只要在建立和使用class的時候操心一下就可以了。我來給你兩個區分 `Class` 和 `Object`的小技巧。
首先針對類和物件,你需要學會兩個說法,「is-a(是啥)」和「has-a(有啥)」。「是啥」要用在談論「兩者以類的關係互相關聯」的時候,而「有啥」要用在「兩者無共同點,僅是互為參照」的時候。
接下來,通讀這段程式碼,將每一個註解為`##??`的位置標明他是「is-a」還是「has-a」的關係,並講明白這個關係是什麼。在程式碼的開始我還舉了幾個例子,所以你只要寫剩下的就可以了。
記住,「是啥」指的是魚和鮭魚的關係,而「有啥」指的是鮭魚和烤肉架的關係。
~~~
## Animal is-a object (yes, sort of confusing) look at the extra credit
class Animal
end
## ??
class Dog < Animal
def initialize(name)
## ??
@name = name
end
end
## ??
class Cat < Animal
def initialize(name)
## ??
@name = name
end
end
## ??
class Person
attr_accessor :pet
def initialize(name)
## ??
@name = name
## Person has-a pet of some kind
@pet = nil
end
end
## ??
class Employee < Person
def initialize(name, salary)
## ?? hmm what is this strange magic?
super(name)
## ??
@salary = salary
end
end
## ??
class Fish
end
## ??
class Salmon < Fish
end
## ??
class Halibut < Fish
end
## rover is-a Dog
rover = Dog.new("Rover")
## ??
satan = Cat.new("Satan")
## ??
mary = Person.new("Mary")
## ??
mary.pet = satan
## ??
frank = Employee.new("Frank", 120000)
## ??
frank.pet = rover
## ??
flipper = Fish.new
## ??
crouse = Salmon.new
## ??
harry = Halibut.new
~~~
## 加分習題
1. 有沒有辦法把 `Class` 當作 `Object` 使用呢?
2. 在習題中為 animals、fish、還有people 添加一些函式,讓它們做一些事情。看看當函數在 Animal 這樣的「基類(base class)」裡和在 Dog 裡有什麼區別。
3. 找些別人的程式碼,理清裡邊的「是啥」和「有啥」的關係。
4. 使用 Array 和 Hash 建立一些新的一對應多的「has-many」的關係。
5. 你認為會有一種「has-many」的關係嗎?閱讀一下關於「多重繼承(multiple inheritance)」的資料,然後儘量避免這種用法。
习题 44: 评估你的游戏
最后更新于:2022-04-01 05:40:21
這節練習的目的是檢查評估你的遊戲。也許你只完成了一半,卡在那裡沒有進行下去,也許你勉強做出來了。不管怎樣,我們將串一下你應該弄懂的一些東西,並確認你的遊戲裡有使用到它們。我們將學習如何用正確的格式構建class,使用class 的一些通用習慣,另外還有很多的「書本知識」讓你學習。
為什麼我會讓你先行嘗試,然後才告訴你正確的做法呢?因為從現在開始你要學會「自給自足」,以前是我牽著你前行,以後就得靠你自己了。後面的習題我只會告訴你你的任務,你需要自己去完成,在你完成後我再告訴你如何可以改進你的作業。
一開始你會覺得很困難並且很不習慣,但只要堅持下去,你就會培養出自己解決問題的能力。你還會找出創新的方法解決問題,這比從課本中拷貝解決方案強多了。
## 函式的風格
以前我教過的怎樣寫好函式的方法一樣是適用的,不過這裡要添加幾條:
* 由於各種各樣的原因,程序員將 class (類)裡邊的函式稱作method(方法)。很大程度上這只是個市場策略(用來推銷OOP),不過如果你把它們稱作「函式」的話,是會有囉嗦的人跳出來糾正你的。如果你覺得他們太煩了,你可以告訴他們從數學方面示範一下「函式」和「方法」究竟有什麼不同,這樣他們會很快閉嘴的。
* 在你使用class的過程中,很大一部分時間是告訴你的 class如何「做事情」。給這些函式命名的時候,與其命名成一個名詞,不如命名為一個動詞,作為給class的一個命令。就和陣列中 的 `pop` 函式一樣,它相當於說:「嘿,陣列,把這東西給我 `pop`出去。」它的名字不是 `remove_from_end_of_list`,因為即使它的功能的確是這樣,這一個字串也不是一個命令。
* 讓你的函式保持簡單小巧。由於某些原因,有些人開始學習 class 後就會忘了這一條。
## Classh (類) 的風格
* 你的 class 應該使用「camel case(駝峰式大小寫)」,例如你應該使用`SuperGoldFactory` 而不是 `super_gold_factory`
* 你的 `initialize` 不應該做太多的事情,這會讓 class 變得難以使用。
* 你的其它函式應該使用「underscore format(下劃線隔詞)」,所以你可以寫`my_awesome_hair`,而不是 `myawesomehair` 或者 `MyAwesomeHair`。
* 用一致的方式組織函式的參數。如果你的 class 需要處理 users、dogs、和cats,就保持這個次序(特別情況除外)。如果一個函式的參數是(dog, cat, user),另一個的是(user, cat, dog) ,這會讓函式使用起來很困難。
* 不要對全局變數或者來自模組的變數進行重定義或者賦值,讓這些東西自顧自就行了。
* 不要一根筋式地維持風格一致性,這是思維力底下的妖怪嘍囉做的事情。一致性是好事情,不過愚蠢地跟著別人遵從一些白痴口號是錯誤的行為——這本身就是一種壞的風格。好好為自己著想吧。
## 程式碼風格
* 為了以方便他人閱讀,在自己的程式碼之間留下一些空白。你將會看到一些很差的程式設計師,他們寫的程式碼還算通順,但程式碼之間沒有任何空間。這種風格在任何程式語言中都是壞習慣,人的眼睛和大腦會通過空白和垂直對齊的位置來掃描和區隔視覺元素,如果你的程式碼裡沒有任何空白,這相當於為你的程式碼上了迷彩裝。
* 如果一段程式碼你無法朗讀出來,那麼這段程式碼的可讀性可能就有問題。如你找不到讓某個東西易用的方法,試著也朗讀出來。這樣不僅會逼迫你慢速而且真正仔細閱讀過去,還會幫你找到難讀的段落,從而知道那些程式碼的易讀性需要作出改進。
* 學著模仿別人的風格寫程式,直到哪天你找到你自己的風格為止。
* 一旦你有了自己的風格,也別把它太當回事。程式設計師工作的一部分就是和別人的程式碼打交道,有的人審美觀就是很差。相信我,你的審美觀某一方面一定也很差,只是你從未意識到而已。
* 如果你發現有人寫的程式碼風格你很喜歡,那就模仿他們的風格。
## 好的註釋
* 有程序員會告訴你,說你的程式碼需要有足夠的可讀性,這樣你就無需寫註釋了。他們會以自己接近官腔的聲音說「所以你永遠都不應該寫程式碼註釋。」這些人要嘛是一些顧問型的人物,如果別人無法使用他們的程式碼,就會付更多錢給他們讓他們解決問題。要嘛他們能力不足,從來沒有跟別人合作過。別理會這些人,好好寫你的註解。
* 寫註解的時候,描述清楚為什麼你要這樣做。程式碼只會告訴你「這樣實現」,而不會告訴你「為什麼要這樣實現」,而後者比前者更重要。
* 當你為函式寫文件註解的時候,記得為別的程式碼使用者也寫些東西。你不需要狂寫一大堆,但一兩句話謝謝這個函式的用法還是很有用的。
* 最後要說的是,雖然註解是好東西,太多的註解就不見得是了。而且註解也是需要維護的,你要盡量讓註解短小精悍一語中的,如果你對程式碼做了更改,記得檢查並更新相關的註解,確認它們還是正確的。
## 評估你的遊戲
現在我要求你假裝成是我,板起臉來,把你的程式碼打印出來,然後拿一支紅筆,把程式碼中所有的錯誤都標出來。你要充分利用你在本章以及前面學到的知識。等你批改完了,我要求你把所有的錯誤改對。這個過程我需要你多重複幾次,爭取找到更多的可以改進的地方。使用我前面教過的方法,把程式碼分解成最細小的單元一一進行分析。
這節練習的目的是訓練你對於細節的關注程度。等你檢查完自己的程式碼,再找一段別人的程式碼用這種方法檢查一遍。把程式碼打印出來,檢查出所有程式碼和風格方面的錯誤,然後試著在不改壞別人程式碼的前提下把它們修改正確。
這週我要求你的事情就是批改和糾錯,包含你自己的程式碼和別人的程式碼,再沒有別的了。這節習題難度還是挺大,不過一旦你完成了任務,你學過的東西就會牢牢記在腦中。
习题 43: 你来制作一个游戏
最后更新于:2022-04-01 05:40:19
你要開始學會自食其力了。通過閱讀這本書你應該已經學到了一點,那就是你需要的所有的資訊網路上都有,你只要去搜尋就能找到。唯一困擾你的就是如何使用正確的詞彙進行搜尋。學到現在,你在挑選搜尋關鍵字方面應該已經有些感覺了。現在已經是時候了,你需要嘗試寫一個大的專案,並讓它運行起來。
以下是你的需求:
1. 製作一個截然不同的遊戲。
2. 使用多個檔案,並使用 `require`呼叫這些檔案。確認自己知道 `require`的用法。
3. 對於每個房間使用一個 `class`,`class` 的命名要能體現出它的用處。例如`GoldRoom`、`KoiPondRoom`。
4. 你的執行器程式碼應該了解這些房間,所以創建一個 class 來呼叫並且記錄這些房間。有很多種方法可以達到這個目的,不過你可以考慮讓每個房間傳回下一個房間,或者設置一個變數,讓它指定下一個房間是什麼。
其他的事情就全靠你了。花一個星期完成這件任務,做一個你能做出來的最好的遊戲。使用你學過的任何東西(類,函數,Hash,陣列……)來改進你的程式。這節課的目的是教你如何構建 class 出來,而這些 class 又能調用到其它 Ruby 檔案中的 class。
我不會詳細地告訴你告訴你怎樣做,你需要自己完成。試著下手吧,寫程式就是解決問題的過程,這就意味著你要嘗試各種可能性,進行實驗,經歷失敗,然後丟掉你做出來的東西重頭開始。當你被某個問題卡住的時候,你可以向別人尋求幫助,並把你的程式貼出來給他們看。如果有人刻薄你,別理他們,你只要集中精力在幫你的人身上就可以了。持續修改和清理你的程式碼,直到它完整可執行為止,然後再研究一下看它還能不能被改進。
祝你好運,下個星期你做出遊戲後我們再見。
习题 42: 物以类聚
最后更新于:2022-04-01 05:40:17
雖說將函式放到 Hash 裡是很有趣的一件事情,你應該也會想到「如果 Ruby 內建這件事情該多好」。事實上也的確有,那就是 `class` 這個關鍵字。你可以使用 `class` 創建更棒的 「函式 Hash」,比你在上節練習中做的強大多了。Class(類)有著各種各樣強大的功能和用法,但本書不會深入講這些內容,在這裡,你只要你學會把它們當作高級的「函式字典」使用就可以了。
用到「class」的程式語言被稱作「Object Oriented Programming(面向對象編程式語言」。這是一種傳統的寫程式的方式,你需要做出「東西」來,然後你「告訴」這些東西去完成它們的工作。類似的事情你其實已經做過不少了,只不過還沒有意識到而已。記得你做過的這個吧:
~~~
stuff = ['Test', 'This', 'Out']
puts stuff.join(' ')
~~~
其實你這裡已經使用了 `class`。 stuff 這個變數其實是一個 Array Class。而 `stuff.join()` 呼叫了 `Array` 函式中的 `join`,然後傳遞了字串 `' '`(就是一個空格),這也是一個 class —— 它是一個 `String` class (字符串類)。到處都是 class!
其實你這裡已經使用了 `class`。`stuff`這個變量其實是一個list `class`(列表類)。而’ ‘.join(stuff)裡調用函式join的字符串’ ‘(就是一個空格)也是一個`class` ——它是一個string `class` (字符串類)。到處都是`class`!
還有一個概念是 object(物件),不過我們暫且不提。當你建立過幾個`class` 後就會學到了。怎樣建立`class`呢?和你建立 `ROOMS` Hash 的方法差不多,但其實更簡單:
~~~
class TheThing
attr_reader :number
def initialize()
@number = 0
end
def some_function()
puts "I got called."
end
def add_me_up(more)
@number += more
return @number
end
end
# two different things
a = TheThing.new
b = TheThing.new
a.some_function()
b.some_function()
puts a.add_me_up(20)
puts a.add_me_up(20)
puts b.add_me_up(30)
puts b.add_me_up(30)
puts a.number
puts b.number
~~~
看到了在 `@number` 前面的 `@` 吧?這是一個實例變數 (instance variable)。每個在 `TheThing` 中你建立的實例都會擁有 `@number` 中自己的值。我們不能透過直接打 `a.number` 直接拿到 number。除非我們特別使用 `attr_reader :number`,宣告讓外界能存取資料。
若要讓 `@number` write-only,我們可以使用 `attr_writer :number`。為了讓它可以既可讀又可寫,我們可以使用 `attr_accessor :number`。Ruby 使用了這些優良的物件導向原則來封裝資料。
下來,看到 `initialize` 函式了嗎?這就是你為建立 `class` 設置內部變數的方式。你可以用以 `@`符號開頭的方式去設定它們。另外看到我們使用了`add_me_up()` 為你建立 `number`加值。後面你可以看到我們怎樣可以使用這種方法為數字加值,然後印出來。
Class 是很強大的東西,你應該好好讀讀相關的東西。盡可能多找些東西讀並且多多實驗。你其實知道它們該怎麼用,只要試試就知道了。其實我馬上就要去練吉他了,所以我不會讓你寫練習了。你將使用 `class` 寫一個練習。
接下來我們將把習題 41 的內容重寫一遍,不過這回我們將使用 `class`:
~~~
class Game
def initialize(start)
@quips = [
"You died. You kinda suck at this.",
"Nice job, you died ...jackass.",
"Such a luser.",
"I have a small puppy that's better at this."
]
@start = start
puts "in init @start = " + @start.inspect
end
def prompt()
print "> "
end
def play()
puts "@start => " + @start.inspect
next_room = @start
while true
puts "\n--------"
room = method(next_room)
next_room = room.call()
end
end
def death()
puts @quips[rand(@quips.length())]
Process.exit(1)
end
def central_corridor()
puts "The Gothons of Planet Percal #25 have invaded your ship and destroyed"
puts "your entire crew. You are the last surviving member and your last"
puts "mission is to get the neutron destruct bomb from the Weapons Armory,"
puts "put it in the bridge, and blow the ship up after getting into an "
puts "escape pod."
puts "\n"
puts "You're running down the central corridor to the Weapons Armory when"
puts "a Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown costume"
puts "flowing around his hate filled body. He's blocking the door to the"
puts "Armory and about to pull a weapon to blast you."
prompt()
action = gets.chomp()
if action == "shoot!"
puts "Quick on the draw you yank out your blaster and fire it at the Gothon."
puts "His clown costume is flowing and moving around his body, which throws"
puts "off your aim. Your laser hits his costume but misses him entirely. This"
puts "completely ruins his brand new costume his mother bought him, which"
puts "makes him fly into an insane rage and blast you repeatedly in the face until"
puts "you are dead. Then he eats you."
return :death
elsif action == "dodge!"
puts "Like a world class boxer you dodge, weave, slip and slide right"
puts "as the Gothon's blaster cranks a laser past your head."
puts "In the middle of your artful dodge your foot slips and you"
puts "bang your head on the metal wall and pass out."
puts "You wake up shortly after only to die as the Gothon stomps on"
puts "your head and eats you."
return :death
elsif action == "tell a joke"
puts "Lucky for you they made you learn Gothon insults in the academy."
puts "You tell the one Gothon joke you know:"
puts "Lbhe zbgure vf fb sng, jura fur fvgf nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr."
puts "The Gothon stops, tries not to laugh, then busts out laughing and can't move."
puts "While he's laughing you run up and shoot him square in the head"
puts "putting him down, then jump through the Weapon Armory door."
return :laser_weapon_armory
else
puts "DOES NOT COMPUTE!"
return :central_corridor
end
end
def laser_weapon_armory()
puts "You do a dive roll into the Weapon Armory, crouch and scan the room"
puts "for more Gothons that might be hiding. It's dead quiet, too quiet."
puts "You stand up and run to the far side of the room and find the"
puts "neutron bomb in its container. There's a keypad lock on the box"
puts "and you need the code to get the bomb out. If you get the code"
puts "wrong 10 times then the lock closes forever and you can't"
puts "get the bomb. The code is 3 digits."
code = "%s%s%s" % [rand(9)+1, rand(9)+1, rand(9)+1]
print "[keypad]> "
guess = gets.chomp()
guesses = 0
while guess != code and guesses < 10
puts "BZZZZEDDD!"
guesses += 1
print "[keypad]> "
guess = gets.chomp()
end
if guess == code
puts "The container clicks open and the seal breaks, letting gas out."
puts "You grab the neutron bomb and run as fast as you can to the"
puts "bridge where you must place it in the right spot."
return :the_bridge
else
puts "The lock buzzes one last time and then you hear a sickening"
puts "melting sound as the mechanism is fused together."
puts "You decide to sit there, and finally the Gothons blow up the"
puts "ship from their ship and you die."
return :death
end
end
def the_bridge()
puts "You burst onto the Bridge with the netron destruct bomb"
puts "under your arm and surprise 5 Gothons who are trying to"
puts "take control of the ship. Each of them has an even uglier"
puts "clown costume than the last. They haven't pulled their"
puts "weapons out yet, as they see the active bomb under your"
puts "arm and don't want to set it off."
prompt()
action = gets.chomp()
if action == "throw the bomb"
puts "In a panic you throw the bomb at the group of Gothons"
puts "and make a leap for the door. Right as you drop it a"
puts "Gothon shoots you right in the back killing you."
puts "As you die you see another Gothon frantically try to disarm"
puts "the bomb. You die knowing they will probably blow up when"
puts "it goes off."
return :death
elsif action == "slowly place the bomb"
puts "You point your blaster at the bomb under your arm"
puts "and the Gothons put their hands up and start to sweat."
puts "You inch backward to the door, open it, and then carefully"
puts "place the bomb on the floor, pointing your blaster at it."
puts "You then jump back through the door, punch the close button"
puts "and blast the lock so the Gothons can't get out."
puts "Now that the bomb is placed you run to the escape pod to"
puts "get off this tin can."
return :escape_pod
else
puts "DOES NOT COMPUTE!"
return :the_bridge
end
end
def escape_pod()
puts "You rush through the ship desperately trying to make it to"
puts "the escape pod before the whole ship explodes. It seems like"
puts "hardly any Gothons are on the ship, so your run is clear of"
puts "interference. You get to the chamber with the escape pods, and"
puts "now need to pick one to take. Some of them could be damaged"
puts "but you don't have time to look. There's 5 pods, which one"
puts "do you take?"
good_pod = rand(5)+1
print "[pod #]>"
guess = gets.chomp()
if guess.to_i != good_pod
puts "You jump into pod %s and hit the eject button." % guess
puts "The pod escapes out into the void of space, then"
puts "implodes as the hull ruptures, crushing your body"
puts "into jam jelly."
return :death
else
puts "You jump into pod %s and hit the eject button." % guess
puts "The pod easily slides out into space heading to"
puts "the planet below. As it flies to the planet, you look"
puts "back and see your ship implode then explode like a"
puts "bright star, taking out the Gothon ship at the same"
puts "time. You won!"
Process.exit(0)
end
end
end
a_game = Game.new(:central_corridor)
a_game.play()
~~~
## 你應該看到的結果
這個版本的遊戲和你的上一版效果應該是一樣的,其實有些代碼都幾乎一樣。比較一下兩版程式碼,弄懂其中不同的地方,重點在需要理解這些東西:
1. 怎樣建立一個 `class` Game 並且放函式到裡面去。
2. `initialize` 是一個特殊的初始方法,怎樣預設重要的變數在裡面。
3. 你如何透過將在 `class` 下這個關鍵字再巢狀排列這些定義的方式為`class` 添加函式。
4. 你如何透過在名稱底下加進巢狀內容來添加函式的。
5. `@` 的概念,還有它在 `initialize`、`play` 和 `death` 是怎樣被使用的。
6. 最後我們怎樣建立了一個 Game,然後透過 `play()`讓所有的東西運行起來。
加分習題 研究一下**dict**是什麼東西,應該怎樣使用。 再為遊戲添加一些房間,確認自己已經學會使用`class` 。 創建一個新版本,裡邊使用兩個`class`,其中一個是Map,另一個是Engine。提示:把play放到Engine裡面。
## 加分習題
1. 再為遊戲添加一些房間,確認自己已經學會使用 `class`。
2. 建一個新版本,裡邊使用兩個 `class`,其中一個是 `Map`,另一個是 `Engine`。提示:把 play 放到 `Engine` 裡面。
习题 41: 来自 Percal 25 号行星的哥顿人(Gothons)
最后更新于:2022-04-01 05:40:14
你在上一節中發現 Hash 的秘密功能了嗎?你可以解釋給自己嗎?讓我來給你解釋一下,順便和你自己的理解對比看有什麼不同。這裡是我們要討論的程式碼:
~~~
cities[:find] = method(:find_city)
puts cities[:find].call(cities, state)
~~~
你要記住一個函式也可以作為一個變數,為了要將一個程式碼區段儲存在一個變數裡,我們創造了一個東西叫「proc」,proc 是 procedure 縮寫。在這段程式碼中,首先我們呼叫了 Ruby 內建的函式`method`,它會回傳一個 proc 版的 `find_city` 函式。然後我們將之除存在一個 Hash 裡:key 是 `:find`,value 是 `cities`。。這和我們將州和市關聯起來的程式碼做的事情一樣,只不過在這個情況裡是個 proc。
好了,所以一旦我們知道 `find_city` 是在Hash中 `:find` 的位置,這就意味著我們可以去呼叫它。第二行程式碼可以分解成如下步驟:
1. Ruby 讀到了 `cities`,然後知道了它是一個 「Hash」。
2. 然後看到了`[:find]`,於是 Ruby 就從索引找到了 cities Hash 中對應的位置,並且獲取了該位置的內容。
3. `[:find]` 這個位置的內容是我們的函式 `find_city`,所以Ruby就知道了這裡表示一個函式,於是當它碰到`.call`就開始了 proc呼叫。
4. `cities`、`state` 這兩個參數將被傳遞到函式 `find_city` 中,然後這個函式就被運行了。
5. `find_city` 接著從 `cities` 中尋找 `states`,並且回傳它找到的內容,如果什麼都沒找到,就返回一個信息說它什麼都沒找到。
6. Ruby 接受 `find_city` 傳回的資訊,最後將該資訊賦值給一開始的 `city_found` 這個變數。
我再教你一個小技巧。如果你倒著閱讀的話,程式碼可能會變得更容易理解。讓我們來試一下,一樣是那行:
1. `state` 和 `city` 是…
2. 最為參數傳遞給…
3. 一個 proc 位於…
4. `:find` 然後尋找,目的地為…
5. `cities` 這個 Hash…
6. 最後印到螢幕上
還有一種方法讀它,這回是「由裡向外」。
1. 找到表示式的中心位置,此次為`[:find]`。
2. 逆時針追溯,首先看到的是一個叫 `cities`的 Hash,這樣就知道了 `cities` 中的 `:find` 元素。
3. 上一步得到一個函式。繼續逆時針尋找,看到的是參數。
4. 參數傳遞給函式後,函式會傳回一個值。然後再逆時針尋找。
5. 最後,我們到了`city_found` =的賦值位置,並且得到了最終結果。
數十年的程式經驗下來,我在讀程式碼的過程中已經用不到上面的三種方法了。我只要瞄一眼就能知道它的意思。甚至給我一整頁的程式碼,我也可以一眼瞄出裡邊的 bug 和錯誤。這樣的技能是花了超乎常人的時間和精力才鍛煉得來的。在磨練的過程中,我學會了下面三種讀程式碼的方法:
1. 從前向後。
2. 從後向前。
3. 逆時針方向。
現在我們來寫這次的練習,寫完後再過一遍,這節習題其實挺有趣的。
程式碼不少,不過還是從頭寫完吧。確認它能運行,然後玩一下看看。
~~~
def prompt()
print "> "
end
def death()
quips = ["You died. You kinda suck at this.",
"Nice job, you died ...jackass.",
"Such a luser.",
"I have a small puppy that's better at this."]
puts quips[rand(quips.length())]
Process.exit(1)
end
def central_corridor()
puts "The Gothons of Planet Percal #25 have invaded your ship and destroyed"
puts "your entire crew. You are the last surviving member and your last"
puts "mission is to get the neutron destruct bomb from the Weapons Armory,"
puts "put it in the bridge, and blow the ship up after getting into an "
puts "escape pod."
puts "\n"
puts "You're running down the central corridor to the Weapons Armory when"
puts "a Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown costume"
puts "flowing around his hate filled body. He's blocking the door to the"
puts "Armory and about to pull a weapon to blast you."
prompt()
action = gets.chomp()
if action == "shoot!"
puts "Quick on the draw you yank out your blaster and fire it at the Gothon."
puts "His clown costume is flowing and moving around his body, which throws"
puts "off your aim. Your laser hits his costume but misses him entirely. This"
puts "completely ruins his brand new costume his mother bought him, which"
puts "makes him fly into an insane rage and blast you repeatedly in the face until"
puts "you are dead. Then he eats you."
return :death
elsif action == "dodge!"
puts "Like a world class boxer you dodge, weave, slip and slide right"
puts "as the Gothon's blaster cranks a laser past your head."
puts "In the middle of your artful dodge your foot slips and you"
puts "bang your head on the metal wall and pass out."
puts "You wake up shortly after only to die as the Gothon stomps on"
puts "your head and eats you."
return :death
elsif action == "tell a joke"
puts "Lucky for you they made you learn Gothon insults in the academy."
puts "You tell the one Gothon joke you know:"
puts "Lbhe zbgure vf fb sng, jura fur fvgf nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr."
puts "The Gothon stops, tries not to laugh, then busts out laughing and can't move."
puts "While he's laughing you run up and shoot him square in the head"
puts "putting him down, then jump through the Weapon Armory door."
return :laser_weapon_armory
else
puts "DOES NOT COMPUTE!"
return :central_corridor
end
end
def laser_weapon_armory()
puts "You do a dive roll into the Weapon Armory, crouch and scan the room"
puts "for more Gothons that might be hiding. It's dead quiet, too quiet."
puts "You stand up and run to the far side of the room and find the"
puts "neutron bomb in its container. There's a keypad lock on the box"
puts "and you need the code to get the bomb out. If you get the code"
puts "wrong 10 times then the lock closes forever and you can't"
puts "get the bomb. The code is 3 digits."
code = "%s%s%s" % [rand(9)+1, rand(9)+1, rand(9)+1]
print "[keypad]> "
guess = gets.chomp()
guesses = 0
while guess != code and guesses < 10
puts "BZZZZEDDD!"
guesses += 1
print "[keypad]> "
guess = gets.chomp()
end
if guess == code
puts "The container clicks open and the seal breaks, letting gas out."
puts "You grab the neutron bomb and run as fast as you can to the"
puts "bridge where you must place it in the right spot."
return :the_bridge
else
puts "The lock buzzes one last time and then you hear a sickening"
puts "melting sound as the mechanism is fused together."
puts "You decide to sit there, and finally the Gothons blow up the"
puts "ship from their ship and you die."
return :death
end
end
def the_bridge()
puts "You burst onto the Bridge with the netron destruct bomb"
puts "under your arm and surprise 5 Gothons who are trying to"
puts "take control of the ship. Each of them has an even uglier"
puts "clown costume than the last. They haven't pulled their"
puts "weapons out yet, as they see the active bomb under your"
puts "arm and don't want to set it off."
prompt()
action = gets.chomp()
if action == "throw the bomb"
puts "In a panic you throw the bomb at the group of Gothons"
puts "and make a leap for the door. Right as you drop it a"
puts "Gothon shoots you right in the back killing you."
puts "As you die you see another Gothon frantically try to disarm"
puts "the bomb. You die knowing they will probably blow up when"
puts "it goes off."
return :death
elsif action == "slowly place the bomb"
puts "You point your blaster at the bomb under your arm"
puts "and the Gothons put their hands up and start to sweat."
puts "You inch backward to the door, open it, and then carefully"
puts "place the bomb on the floor, pointing your blaster at it."
puts "You then jump back through the door, punch the close button"
puts "and blast the lock so the Gothons can't get out."
puts "Now that the bomb is placed you run to the escape pod to"
puts "get off this tin can."
return :escape_pod
else
puts "DOES NOT COMPUTE!"
return :the_bridge
end
end
def escape_pod()
puts "You rush through the ship desperately trying to make it to"
puts "the escape pod before the whole ship explodes. It seems like"
puts "hardly any Gothons are on the ship, so your run is clear of"
puts "interference. You get to the chamber with the escape pods, and"
puts "now need to pick one to take. Some of them could be damaged"
puts "but you don't have time to look. There's 5 pods, which one"
puts "do you take?"
good_pod = rand(5)+1
print "[pod #]>"
guess = gets.chomp()
if guess.to_i != good_pod
puts "You jump into pod %s and hit the eject button." % guess
puts "The pod escapes out into the void of space, then"
puts "implodes as the hull ruptures, crushing your body"
puts "into jam jelly."
return :death
else
puts "You jump into pod %s and hit the eject button." % guess
puts "The pod easily slides out into space heading to"
puts "the planet below. As it flies to the planet, you look"
puts "back and see your ship implode then explode like a"
puts "bright star, taking out the Gothon ship at the same"
puts "time. You won!"
Process.exit(0)
end
end
ROOMS = {
:death => method(:death),
:central_corridor => method(:central_corridor),
:laser_weapon_armory => method(:laser_weapon_armory),
:the_bridge => method(:the_bridge),
:escape_pod => method(:escape_pod)
}
def runner(map, start)
next_one = start
while true
room = map[next_one]
puts "\n--------"
next_one = room.call()
end
end
runner(ROOMS, :central_corridor)
~~~
## 你應該看到的結果
~~~
$ ruby ex41.rb
--------
The Gothons of Planet Percal #25 have invaded your ship and destroyed
your entire crew. You are the last surviving member and your last
mission is to get the neutron destruct bomb from the Weapons Armory,
put it in the bridge, and blow the ship up after getting into an
escape pod.
You're running down the central corridor to the Weapons Armory when
a Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown costume
flowing around his hate filled body. He's blocking the door to the
Armory and about to pull a weapon to blast you.
> dodge!
Like a world class boxer you dodge, weave, slip and slide right
as the Gothon's blaster cranks a laser past your head.
In the middle of your artful dodge your foot slips and you
bang your head on the metal wall and pass out.
You wake up shortly after only to die as the Gothon stomps on
your head and eats you.
--------
Such a luser.
$ ruby ex41.rb
--------
The Gothons of Planet Percal #25 have invaded your ship and destroyed
your entire crew. You are the last surviving member and your last
mission is to get the neutron destruct bomb from the Weapons Armory,
put it in the bridge, and blow the ship up after getting into an
escape pod.
You're running down the central corridor to the Weapons Armory when
a Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown costume
flowing around his hate filled body. He's blocking the door to the
Armory and about to pull a weapon to blast you.
> tell a joke
Lucky for you they made you learn Gothon insults in the academy.
You tell the one Gothon joke you know:
Lbhe zbgure vf fb sng, jura fur fvgf nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr.
The Gothon stops, tries not to laugh, then busts out laughing and can't move.
While he's laughing you run up and shoot him square in the head
putting him down, then jump through the Weapon Armory door.
--------
You do a dive roll into the Weapon Armory, crouch and scan the room
for more Gothons that might be hiding. It's dead quiet, too quiet.
You stand up and run to the far side of the room and find the
neutron bomb in its container. There's a keypad lock on the box
and you need the code to get the bomb out. If you get the code
wrong 10 times then the lock closes forever and you can't
get the bomb. The code is 3 digits.
[keypad]> 123
BZZZZEDDD!
[keypad]> 234
BZZZZEDDD!
[keypad]> 345
BZZZZEDDD!
[keypad]> 456
BZZZZEDDD!
[keypad]> 567
BZZZZEDDD!
[keypad]> 678
BZZZZEDDD!
[keypad]> 789
BZZZZEDDD!
[keypad]> 384
BZZZZEDDD!
[keypad]> 764
BZZZZEDDD!
[keypad]> 354
BZZZZEDDD!
[keypad]> 263
The lock buzzes one last time and then you hear a sickening
melting sound as the mechanism is fused together.
You decide to sit there, and finally the Gothons blow up the
ship from their ship and you die.
--------
You died. You kinda suck at this.
~~~
## 加分習題
1. 解釋一下返回至下一個房間的運作原理。 2.建立更多的房間,讓遊戲規模變大。
2. 除了讓每個函式印出自己以外,試試學習一下「文件註解(doc comments)」。
3. 看看你能不能將房間描述寫成文件註解,然後修改運行它的程式碼,讓它把文檔註解打印出來。
4. 一旦你用了文件註解作為房間描述,你還需要讓這個函式打出用戶提示嗎?試著讓運行函數的代碼打出用戶提示來,然後將用戶輸入傳遞到各個函式。你的函式應該只是一些 `if` 語句組合,將結果印出來,並且返回下一個房間。
5. 這其實是一個小版本的「有限狀態機(finite state machine)」,找資料閱讀了解一下,雖然你可能看不懂,但還是找來看看吧
习题 40: Hash, 可爱的 Hash
最后更新于:2022-04-01 05:40:12
接下來我要教你另外一種讓你傷腦筋的容器型資料結構,因為一旦你學會這種資料結構,你將擁有超酷的能力。這是最有用的容器:Hash。
Ruby 將這種資料類型叫做「Hash」,有的語言裡它的名稱是「dictionaries」。這兩種名字我都會用到,不過這並不重要,重要的是它們和陣列的區別。你看,針對陣列你可以做這樣的事情:
~~~
ruby-1.9.2-p180 :015 > things = ['a','b','c','d']
=> ["a", "b", "c", "d"]
ruby-1.9.2-p180 :016 > print things[1]
b => nil
ruby-1.9.2-p180 :017 > things[1] = 'z'
=> "z"
ruby-1.9.2-p180 :018 > print things[1]
z => nil
ruby-1.9.2-p180 :019 > print things
["a", "z", "c", "d"] => nil
ruby-1.9.2-p180 :020 >
~~~
你可以使用數字作為陣列的「索引」,也就是你可以通過數字找到陣列中的元素。而 Hash 所作的,是讓你可以通過任何東西找到元素,不只是數字。是的,Hash 可以將一個物件和另外一個東西關聯,不管它們的類型是什麼,我們來看看:
~~~
ruby-1.9.2-p180 :001 > stuff = {:name => "Rob", :age => 30, :height => 5*12+10}
=> {:name=>"Rob", :age=>30, :height=>70}
ruby-1.9.2-p180 :002 > puts stuff[:name]
Rob
=> nil
ruby-1.9.2-p180 :003 > puts stuff[:age]
30
=> nil
ruby-1.9.2-p180 :004 > puts stuff[:height]
70
=> nil
ruby-1.9.2-p180 :005 > stuff[:city] = "New York"
=> "New York"
ruby-1.9.2-p180 :006 > puts stuff[:city]
New York
=> nil
ruby-1.9.2-p180 :007 >
~~~
你將看到除了通過數字以外,我們在 Ruby 還可以用字串來從 Hash 中獲取 `stuff`,我們還可以用字串來往 Hash 中添加元素。當然它支持的不只有字串,我們還可以做這樣的事情:
~~~
ruby-1.9.2-p180 :004 > stuff[1] = "Wow"
=> "Wow"
ruby-1.9.2-p180 :005 > stuff[2] = "Neato"
=> "Neato"
ruby-1.9.2-p180 :006 > puts stuff[1]
Wow
=> nil
ruby-1.9.2-p180 :007 > puts stuff[2]
Neato
=> nil
ruby-1.9.2-p180 :008 > puts stuff
{:name=>"Rob", :age=>30, :height=>70, :city=>"New York", 1=>"Wow", 2=>"Neato"}
=> nil
ruby-1.9.2-p180 :009 >
~~~
在這裡我使用了數字。其實我可以使用任何東西,不過這麼說並不准確,不過你先這麼理解就行了。
當然了,一個只能放東西進去的 Hash是沒啥意思的,所以我們還要有刪除物件的方法,也就是使用 `delete` 這個關鍵字:
~~~
ruby-1.9.2-p180 :009 > stuff.delete(:city)
=> "New York"
ruby-1.9.2-p180 :010 > stuff.delete(1)
=> "Wow"
ruby-1.9.2-p180 :011 > stuff.delete(2)
=> "Neato"
ruby-1.9.2-p180 :012 > stuff
=> {:name=>"Rob", :age=>30, :height=>70}
ruby-1.9.2-p180 :013 >
~~~
接下來我們要做一個練習,你必須「非常」仔細,我要求你將這個練習寫下來,然後試著弄懂它做了些什麼。這個練習很有趣,做完以後你可能會有豁然開朗的感覺。
~~~
cities = {'CA' => 'San Francisco',
'MI' => 'Detroit',
'FL' => 'Jacksonville'}
cities['NY'] = 'New York'
cities['OR'] = 'Portland'
def find_city(map, state)
if map.include? state
return map[state]
else
return "Not found."
end
end
# ok pay attention!
cities[:find] = method(:find_city)
while true
print "State? (ENTER to quit) "
state = gets.chomp
break if state.empty?
# this line is the most important ever! study!
puts cities[:find].call(cities, state)
end
~~~
## 你應該看到的結果
~~~
$ ruby ex40.rb
State? (ENTER to quit) > CA
San Francisco
State? (ENTER to quit) > FL
Jacksonville
State? (ENTER to quit) > O
Not found.
State? (ENTER to quit) > OR
Portland
State? (ENTER to quit) > VT
Not found.
State? (ENTER to quit) >
~~~
## 加分習題
1. 在 Ruby 文件中找到 Hash 相關的內容,學著對 Hash 做更多的操作。
2. 找出一些 Hash 無法做到的事情。例如比較重要的一個就是 Hash 的內容是無序的,你可以檢查一下看看是否真是這樣。
3. 試著把 `for` 迴圈執行到 Hash 上面,然後試著在 `for` 迴圈中使用 Hash 的 each 函式,看看會有什麼樣的結果。
习题 39: 阵列的操作
最后更新于:2022-04-01 05:40:10
你已經學過了陣列。在你學習“`while` 迴圈的時候,你對陣列進行過「pushed」動作,而且將陣列的內容印了出來。另外你應該還在加分習題裡研究過 Ruby 文件,看了陣列支援的其他操作。這已經是一段時間以前了,所以如果你不記得了的話,就回到本書的前面再複習一遍吧。
找到了嗎?還記得嗎?很好。那時候你對一個陣列執行了 `push` 函式。不過,你也許還沒有真正明白發生的事情,所以我們再來看看我們可以對陣列進行什麼樣的操作。
當你看到像 `mystuff.append('hello')`這樣的程式時,你事實上已經在 Ruby 內部激發了一個連鎖反應。以下是它的運作原理:
1. Ruby 看到你用到了 `mystuff`,於是就去找到這個變數。也許它需要倒著檢查看你有沒有在哪裡用 `=` 建立過這個變數,或者檢查它是不是一個函式參數,或者看它是不是一個全局變數。不管哪種方式,它得先找到 `mystuff` 這個變數才行。
2. 一旦它找到了 `mystuff`,就輪到處理句點 `.` (period)這個操作符號,而且開始查看 `mystuff` 內部的一些變數了。由於 `mystuff` 是一個陣列,Ruby 知道 `mystuff` 支援一些函式。
3. 接下來輪到了處理 `push`。Ruby會將 「push」和 `mystuff` 支援的所有函式的名稱一一對比,如果確實其中有一個叫 `push` 的函式,那麼Ruby就會去使用這個函式。
4. 接下來Ruby看到了括號(parenthesis)並且意識到, 「噢,原來這應該是一個函式」,到了這裡,它就正常會呼叫這個函式了,不過這裡的函式還要多一個參數才行。
一下子要消化這麼多可能有點難度,不過我們將做幾個練習,讓你頭腦中有一個深刻的印象。下面的練習將字符串和列表混在一起,看看你能不能在裡邊找出點樂子來:
~~~
ten_things = "Apples Oranges Crows Telephone Light Sugar"
puts "Wait there's not 10 things in that list, let's fix that."
stuff = ten_things.split(' ')
more_stuff = %w(Day Night Song Frisbee Corn Banana Girl Boy)
while stuff.length != 10
next_one = more_stuff.pop()
puts "Adding: #{next_one}"
stuff.push(next_one)
puts "There's #{stuff.length} items now."
end
puts "There we go: #{stuff}"
puts "Let's do some things with stuff."
puts stuff[1]
puts stuff[-1] # whoa! fancy
puts stuff.pop()
puts stuff.join(' ') # what? cool!
puts stuff.values_at(3,5).join('#') # super stellar!
~~~
## 你應該看到的結果
~~~
$ ruby ex39.rb
Wait there's not 10 things in that list, let's fix that.
Adding: Boy
There's 7 items now.
Adding: Girl
There's 8 items now.
Adding: Banana
There's 9 items now.
Adding: Corn
There's 10 items now.
There we go: ["Apples", "Oranges", "Crows", "Telephone", "Light", "Sugar", "Boy", "Girl", "Banana", "Corn"]
Let's do some things with stuff.
Oranges
Corn
Corn
Apples Oranges Crows Telephone Light Sugar Boy Girl Banana
Telephone#Sugar
$
~~~
## 加分習題
1. 上網閱讀一些關於「物件導向程式(Object Oriented Programming)」的資料。暈了吧?嗯,我以前也是。別擔心。你將從這本書學到足夠用的關於物件導向程式的基礎知識,而以後你還可以慢慢學到更多。
2. `something.methods` 和 something的 class 有什麼關係?
3. 如果你不知道我講的是些什麼東西,別擔心。程式設計師為了顯得自己聰明,於是就發明了Opject Oriented Programming,簡稱為OOP,然後他們就開始濫用這個東西了。如果你覺得這東西太難,你可以開始學一下「函式式程式(functional programming)」。
习题 38: 阅读程式码
最后更新于:2022-04-01 05:40:08
現在去找一些 Ruby 程式碼閱讀一下。你需要自己找程式碼,然後從中學習一些東西。你學到的東西已經足夠讓你看懂一些程式碼了,但你可能還無法理解這些程式碼的功能。這節課我要教給你的是:如何運用你學到的東西理解別人的程式碼。
首先把你想要理解的程式碼印到紙上。沒錯,你需要印出來,因為和螢幕輸出相比,你的眼睛和大腦更習慣於接受紙質列印的內容。一次最多列印幾頁就可以了。
然後通讀你列印出來的代碼並做好標記,標記的內容包括以下幾個方面:
1. 函數以及函數的功能。
2. 每個變數的初始賦值。
3. 每個在程式的各個部分中多次出現的變數。它們以後可能會給你帶來麻煩。
4. 任何不包含else的 `if` 語句。它們是正確的嗎?
5. 任何可能沒有結束點的while循環。
6. 最後一條,代碼中任何你看不懂的部分都記下來。
接下來你需要通過註解的方式向自己解釋程式碼的含義。解釋各個函式的使用方法,各個變數的用途,以及任何其它方面的內容,只要能幫助你理解程式碼即可。
最後,在程式碼中比較難的各個部分,逐行或者逐個函式跟踪變數值。你可以再打印一份出來,在空白處寫出你要「追踪」的每個變數的值。
一旦你基本理解了程式碼的功能,回到電腦面前,在程式碼上重讀一次,看看能不能找到新的問題點。然後繼續找新的程式碼,用上述的方法去閱讀理解,直到你不再需要紙質列印為止。
## 加分習題
1. 研究一下什麼是「流程圖(flow chart)」,並學著畫一下。
2. 如果你在讀程式碼的時候找出了錯誤,試著把它們改對,並把修改內容發給作者。
3. 不使用紙質打印時,你可以使用註解符號#在程序中加入筆記。有時這些筆記會對後來的讀程式碼的人有很大的幫助。
习题 37: 复习各种符号
最后更新于:2022-04-01 05:40:05
現在該複習你學過的符號和 Ruby 關鍵字了,而且你在本節還會學到一些新的東西。我在這裡所作的是將所有的 Ruby 符號和關鍵字列出來,這些都是值得掌握的重點。
在這節課中,你需要複習每一個關鍵字,從記憶中想起它的作用並且寫下來,接著上網搜索它真正的功能。有些內容可能是無法搜索的,所以這對你可能有些難度,不過你還是需要堅持嘗試。
如果你發現記憶中的內容有誤,就在索引卡片上寫下正確的定義,試著將自己的記憶糾正過來。如果你就是不知道它的定義,就把它也直接寫下來,以後再做研究。
最後,將每一種符號和關鍵字用在程式裡,你可以用一個小程式來做,也可以盡量多寫一些程式來鞏固記憶。這裡的關鍵點是明白各個符號的作用,確認自己沒搞錯,如果搞錯了就糾正過來,然後將其用在程序裡,並且通過這樣的方式鞏固自己的記憶。
## Keywords(關鍵字)
* `alias`
* `and`
* `BEGIN`
* `begin`
* `break`
* `case`
* `class`
* `def`
* `defined?`
* `do`
* `else`
* `elsif`
* `END`
* `end`
* `ensure`
* `false`
* `for`
* `if`
* `in`
* `module`
* `next`
* `nil`
* `not`
* `or`
* `redo`
* `rescue`
* `retry`
* `return`
* `self`
* `super`
* `then`
* `true`
* `undef`
* `unless`
* `until`
* `when`
* `while`
* `yield`
## 資料類型
針對每一種資料類型,都舉出一些例子來,例如針對 string,你可以舉出一些字。針對number,你可以舉出一些數字。
* **true**
* **false**
* **nil**
* `constants`
* `strings`
* `numbers`
* `ranges`
* `arrays`
* `hashes`
## 字串格式(String Formats)
一樣的,在字符串中使用它們,確認它們的功能。
* `\\`
* `\'`
* `\"`
* `\a`
* `\b`
* `\f`
* `\n`
* `\r`
* `\t`
* `\v`
## Operators
有些操作符號你可能還不熟悉,不過還是一一看過去,研究一下它們的功能,如果你研究不出來也沒關係,記錄下來日後解決。
* `::`
* `[]`
* `**`
* `-(unary)`
* `+(unary)`
* `!`
* `~`
* `*`
* `/`
* `%`
* `+`
* `-`
* `<<`
* `>>`
* `&`
* `|`
* `>`
* `>=`
* `<`
* `<=`
* `<=>`
* `==`
* `===`
* `!=`
* `=~`
* `!~`
* `&&`
* `||`
* `..`
* `...`
花一個星期學習這些東西,如果你能提前完成就更好了。我們的目的是覆蓋到所有的符號類型,確認你已經牢牢記住它們。另外很重要的一點是這樣你可以找出自己還不知道哪些東西,為自己日後學習找到一些方向。
习题 36: 设计和测试
最后更新于:2022-04-01 05:40:03
現在你已經學會了「`if` 語句」,我將給你一些使用 `for` 迴圈和 `while` 迴圈的規則,一面你日後碰到麻煩。我還會教你一些測試的小技巧,以便你能發現自己程式的問題。最後,你將需要設計一個和上節類似的小遊戲,不過內容略有更改。
## If 語句的規則
1.每一個「if語句」必伴隨須一個 `else`。 2\. 如果這個 `else` 因為沒有意義,而永遠都沒被執行到,那你必須在 `else` 語句後面使用一個叫 `die` 的函式,讓它印出錯誤並死給你看,這和上一節的習題類似,這樣你可以找到很多的錯誤。 3\. 千萬不要使用超過兩層的 `if` 語句,最好盡量保持只有 1 層。那你就需要把第二個 if 移到另一個函式裡面。 4\. 將`if`語句當做段落來對待,其中的每一個 `if`、`elsif`、`else`組合就跟 一個段落的句子組合一樣。在這種組合的最前面和最後面留一個空行以作區分。 5\. 你的布林測試應該很簡單,如果它們很複雜的話,你需要將它們的運算式先放到一個變數裡,並且為變數取一個好名字。
如果你遵循上面的規則,你就會寫出比大部分程式設計師都好的程式碼來。回到上一個練習中,看看我有沒有遵循這些規則,如果沒有的話,就將其改正過來。
> **Warning:** 在日常寫程式中不要成為這些規則的奴隸。在訓練中,你需要通過這些規則的應用來鞏固你學到的知識,而在實際寫程式中這些規則有時其實很蠢。如果你覺得哪個規則很蠢,就別使用它。
## Rules For Loops
1. 只有在迴圈循環永不停止時使用 `while` 迴圈,這意味著你可能永遠都用不到。這條只有 Ruby 中成立,其他的語言另當別論。
2. 其他類型的迴圈都使用 `for` 迴圈,尤其是在迴圈的對象數量固定或者有限的情況下。
## 除錯(Debug) 的小技巧
1. 不要使用「debugger」。Debugger 所作的相當於對病人的全身掃描。你並不會得到某方面的有用資訊,而且你會發現它輸出的資訊太多,而且大部分沒有用,或者只會讓你更困惑。
2. 最好的除錯技巧是使用 `puts` 或 `p` 在各個你想要檢查的關鍵環節將關鍵變數印出來,從而檢查哪裡是否有錯。
3. 讓程式一部分一部分地運行起來。不要等一個很長的腳本寫完後才去運行它。寫一點,運行一點,再修改一點。
## 家庭作業
寫一個和上節練習類似的遊戲。同類的任何題材的遊戲都可以,花一個星期讓它盡可能有趣一些。作為加分習題,你可以盡量多使用陣列、函式、以及模組(記得習題 13 嗎?),而且盡量多弄一些新的 Ruby 程式讓你的遊戲跑起來。
過有一點需要注意,你應該把遊戲的設計先寫出來。在你開始寫程式碼之前,你應該設計出遊戲的地圖,創建出玩家會碰到的房間、怪物、以及陷阱等環節。
一旦搞定了地圖,你就可以寫寫程式碼了。如果你發現地圖有問題,就調整一下地圖,讓寫程式碼和地圖互相符合。
最後一個建議:每一個程式設計師在開始一個新的大項目時,都會被非理性的恐懼影響到。為了避免這種恐懼,他們會拖延時間,到最後一事無成。我有時會這樣,每個人都會有這樣的經歷,避免這種情況的最好的方法是把自己要做的事情列出來,一次完成一樣。
開始做吧。先做一個小一點的版本,擴充它讓它變大,把自己要完成的事情一一列出來,然後逐個完成就可以了。
习题 35: 分支 (Branches) 和函式 (Functions)
最后更新于:2022-04-01 05:40:01
你已經學會了 `if` 語句、函式、還有陣列。現在你要練習扭轉一下思維了。把下面的代碼寫下來,看你是否能弄懂它實現的是什麼功能。
~~~
def prompt()
print "> "
end
def gold_room()
puts "This room is full of gold. How much do you take?"
prompt; next_move = gets.chomp
if next_move.include? "0" or next_move.include? "1"
how_much = next_move.to_i()
else
dead("Man, learn to type a number.")
end
if how_much < 50
puts "Nice, you're not greedy, you win!"
Process.exit(0)
else
dead("You greedy bastard!")
end
end
def bear_room()
puts "There is a bear here."
puts "The bear has a bunch of honey."
puts "The fat bear is in front of another door."
puts "How are you going to move the bear?"
bear_moved = false
while true
prompt; next_move = gets.chomp
if next_move == "take honey"
dead("The bear looks at you then slaps your face off.")
elsif next_move == "taunt bear" and not bear_moved
puts "The bear has moved from the door. You can go through it now."
bear_moved = true
elsif next_move == "taunt bear" and bear_moved
dead("The bear gets pissed off and chews your leg off.")
elsif next_move == "open door" and bear_moved
gold_room()
else
puts "I got no idea what that means."
end
end
end
def cthulu_room()
puts "Here you see the great evil Cthulu."
puts "He, it, whatever stares at you and you go insane."
puts "Do you flee for your life or eat your head?"
prompt; next_move = gets.chomp
if next_move.include? "flee"
start()
elsif next_move.include? "head"
dead("Well that was tasty!")
else
cthulu_room()
end
end
def dead(why)
puts "#{why} Good job!"
Process.exit(0)
end
def start()
puts "You are in a dark room."
puts "There is a door to your right and left."
puts "Which one do you take?"
prompt; next_move = gets.chomp
if next_move == "left"
bear_room()
elsif next_move == "right"
cthulu_room()
else
dead("You stumble around the room until you starve.")
end
end
start()
~~~
## 你應該看到的結果
你可以結果:
~~~
$ ruby ex35.rb
You are in a dark room.
There is a door to your right and left.
Which one do you take?
> left
There is a bear here.
The bear has a bunch of honey.
The fat bear is in front of another door.
How are you going to move the bear?
> taunt bear
The bear has moved from the door. You can go through it now.
> open door
This room is full of gold. How much do you take?
> asf
Man, learn to type a number. Good job!
$
~~~
## 加分習題
1. 把這個遊戲的地圖畫出來,把自己的路線也畫出來。
2. 改正你所有的錯誤,包括拼寫錯誤。
3. 為你不懂的函式寫註解。記得 **RDoc** 中的註釋嗎?
4. 為遊戲添加更多元素。通過怎樣的方式可以簡化並且擴充遊戲的功能呢?
5. 這個 gold_room 遊戲使用了奇怪的方式讓你鍵入一個數字。這種方式會導致什麼樣的bug?你可以用比檢查 0、1更好的方式判斷輸入是否是數字嗎? `to_i()` 這個函式可以給你一些頭緒。