- method — คำสั่งที่เราตั้งชื่อเองด้วย
def ชื่อ ... end· บรรทัดสุดท้ายคือค่าที่ส่งคืน (บท 93) - class และ
<— class คือแม่พิมพ์ของ object ·class A < Bแปลว่า A สืบทอดของทุกอย่างจาก B (บท 95) - nil — ค่าที่แปลว่า "ไม่มีค่า" เช่นหาของใน Hash ไม่เจอ (บท 94)
- symbol
:title— ชื่อสั้น ๆ ที่มีโคลอนนำหน้า ใช้เป็นป้ายชื่อ (บท 92) #{}— เสียบค่าลงไปใน string เช่น"คนละ #{x} บาท"(บท 91)
เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · Go 1.26.5
(สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — เป็นรุ่นแพตช์ใกล้กัน ไม่กระทบเรื่องในบทนี้) ·
โค้ด Ruby ทุกก้อนรันด้วย ruby ชื่อไฟล์.rb ใน terminal · ผลรันทุกกล่องก็อปจากเครื่องจริง
1 · ทวน Go: error คือ "ค่า" ที่ส่งคืนมา
เริ่มจากของที่คุณรู้แล้ว ตัวอย่างทั้งบทจะใช้เรื่องเดียวกัน: แบ่งบิลค่าอาหาร — ยอด 900 บาท หารกันกี่คน
ใน Go ฟังก์ชันที่อาจพังจะคืนค่าสองตัว: ผลลัพธ์ กับ error
// main.go (Go)
package main
import (
"errors"
"fmt"
)
func splitBill(total, people int) (int, error) {
if people == 0 {
return 0, errors.New("จำนวนคนเป็น 0 ไม่ได้")
}
return total / people, nil
}
func main() {
share, err := splitBill(900, 0)
if err != nil {
fmt.Println("แบ่งบิลไม่ได้:", err)
return
}
fmt.Println("คนละ", share)
}
$ go run .
แบ่งบิลไม่ได้: จำนวนคนเป็น 0 ไม่ได้
(int, error)— บอกว่าฟังก์ชันนี้คืนของสองอย่าง: ตัวเลข และ errorreturn 0, errors.New(...)— มีปัญหา: คืนเลข 0 เป็นค่าหลอก ๆ พร้อม error ที่บอกว่าเพราะอะไรreturn total / people, nil— ไม่มีปัญหา: error เป็นnil(ไม่มี error)if err != nil— คนเรียกต้องเช็คเองทุกครั้ง ว่าที่ได้มามี error ติดมาไหม
สรุปง่าย ๆ: ใน Go error เดินทางมากับค่าที่ return เหมือนของชิ้นหนึ่ง ·
ใครได้รับก็ต้องหยิบขึ้นมาดูเอง แล้วตัดสินใจว่าจะจัดการเลย หรือ return err ส่งต่อให้ชั้นบน
2 · Ruby: error ถูก "โยน" ขึ้นไปจนกว่าจะมีคนรับ
Ruby ไม่คืน error เป็นค่า · พอเกิดเรื่องผิดปกติ Ruby จะหยุดบรรทัดนั้นทันที
แล้วโยน error ออกไปหา "คนรับ" ที่อยู่ข้างบน
"ข้างบน" คือ method ที่เรียกเรามา แล้วก็ method ที่เรียก method นั้นมาอีกที ไล่ขึ้นไปทีละชั้น ·
ถ้าไม่มีใครรับเลยสักชั้น — โปรแกรมหยุดทั้งตัว
มาดูของจริง นี่คือการแบ่งบิลแบบ Ruby ที่ยังไม่มีใครรับ error:
# crash.rb — แบ่งบิลร้านอาหาร
def split_bill(total, people)
total / people
end
puts "เริ่มคิดเงิน"
puts split_bill(900, 3)
puts split_bill(900, 0)
puts "คิดเงินเสร็จ"
def split_bill(total, people)— method รับยอดเงินกับจำนวนคน · ไม่มีช่อง error เหมือน Gototal / people— หารกัน · บรรทัดเดียวของ method จึงเป็นค่าที่คืน- บรรทัดล่าง ๆ เรียกสามครั้ง: หาร 3 คน · หาร 0 คน (จะพัง) · แล้วพิมพ์ว่าเสร็จ
$ ruby crash.rb
เริ่มคิดเงิน
300
crash.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)
from crash.rb:3:in 'Object#split_bill'
from crash.rb:8:in '<main>'
$ echo $?
1
สังเกตว่า "คิดเงินเสร็จ" ไม่ถูกพิมพ์ · พอหารด้วย 0 Ruby โยน error ออกมา
ไม่มีใครรับ โปรแกรมจึงหยุดตรงนั้น · echo $? ได้ 1 แปลว่า "จบแบบพัง" (0 = จบปกติ)
Go เหมือนตึกที่ทุกห้องมีกล่องจดหมาย — ข่าวร้ายมาเป็นจดหมายใส่กล่อง
คนในห้องต้องเปิดกล่องดูเองทุกครั้ง (if err != nil)
ถ้าไม่เปิดดู งานก็เดินต่อไปเหมือนไม่มีอะไรเกิดขึ้น
Ruby เหมือนกริ่งไฟไหม้ — เกิดเรื่องที่ห้องไหน ทุกคนในห้องนั้นหยุดมือทันที
เสียงกริ่งดังขึ้นไปทีละชั้น จนกว่าจะเจอชั้นที่มีเจ้าหน้าที่ประจำ (rescue) มารับเรื่อง
· ถ้าดังไปถึงดาดฟ้าแล้วไม่มีใครรับ = อพยพทั้งตึก (โปรแกรมหยุด)
ที่ไม่เหมือนกัน: กริ่งจริงทุกคนได้ยินพร้อมกัน แต่ error ของ Ruby ขึ้นไปทีละชั้นตามลำดับที่ method เรียกกันมา · และเจ้าหน้าที่ใน Ruby รับเฉพาะเรื่องที่ตัวเองระบุไว้ (หัวข้อ 4) — ไม่ใช่รับได้ทุกเรื่อง
- exception
- ชื่อเรียก error แบบที่ "ถูกโยน" ใน Ruby · มันเป็น object ตัวหนึ่ง มีชื่อชนิด (เช่น
ZeroDivisionError) และข้อความ · ถาม senior ได้ว่า “method นี้ raise exception อะไรได้บ้าง” — เพราะ Ruby ไม่มีอะไรเขียนบอกไว้ที่หัว method เหมือน Go - raise (โยน) · propagate (ลอยขึ้น)
- raise คือการโยน exception ออกมา · propagate คือการที่มันลอยขึ้นไปหา method ที่เรียกมาทีละชั้นเอง
โดยเราไม่ต้องเขียน
return errส่งต่อ · พิมพ์บอก AI ได้ว่า “let this exception propagate up instead of handling it here” - unhandled exception
- exception ที่ลอยขึ้นไปจนสุดแล้วไม่มีใครรับ → โปรแกรมหยุด พิมพ์ข้อความ error และจบด้วย exit code 1 · ถาม senior ได้ว่า “ตรงนี้ควรปล่อยให้พัง หรือควรรับไว้”
3 · อ่านข้อความ error ให้ออก
ข้อความ error ดูน่ากลัว แต่จริง ๆ มันบอกทุกอย่างที่ต้องรู้ · ลองแยกของ crash.rb ทีละท่อน:
| ท่อน | แปลว่า |
|---|---|
crash.rb:3 | พังที่ไฟล์ crash.rb บรรทัด 3 (นับบรรทัด comment แรกด้วย) — เปิดไฟล์ไปดูบรรทัดนี้ก่อนเลย |
in 'Integer#/' | พังระหว่างทำ / (การหาร) ของตัวเลข · อ่านว่า "method / ของ class Integer" —
เครื่องหมาย # ในข้อความ error แค่คั่นชื่อ class กับชื่อ method · ใน Ruby แม้แต่ / ก็เป็น method ตัวหนึ่ง |
divided by 0 | ข้อความ — บอกเป็นภาษาคนว่าเกิดอะไร |
(ZeroDivisionError) | ชื่อชนิดของ error อยู่ในวงเล็บท้ายบรรทัดเสมอ · เอาไปใช้ตอน rescue ได้ (หัวข้อถัดไป) |
บรรทัด from ... | ทางที่ error ลอยขึ้นมา · อ่านบนลงล่าง = จากจุดพัง ย้อนไปหาคนที่เรียก ·
'Object#split_bill' คือ method ของเรา (method ที่เขียนนอก class Ruby นับเป็นของ Object — จำแค่นี้พอ) ·
'<main>' คือ "ตัวไฟล์เอง" ชั้นนอกสุด (บรรทัด 8 คือ split_bill(900, 0)) |
เคล็ดลับ: ดูวงเล็บท้ายบรรทัดแรกก่อน (พังเพราะอะไร) แล้วดูเลขบรรทัด (พังที่ไหน) · แค่สองอย่างนี้ก็แก้ได้เกินครึ่งแล้ว
4 · begin / rescue — ขึงคนรับ error
ถ้าไม่อยากให้โปรแกรมหยุด เราต้อง "ขึงคนรับ" ไว้ตรงที่คิดว่าจะพัง
ใส่โค้ดเสี่ยงไว้ระหว่าง begin กับ rescue แล้วเขียนว่าจะทำอะไรถ้าพัง:
# rescue1.rb
def split_bill(total, people)
total / people
end
begin
puts split_bill(900, 0)
rescue
puts "แบ่งบิลไม่ได้"
end
puts "คิดเงินเสร็จ"
$ ruby rescue1.rb
แบ่งบิลไม่ได้
คิดเงินเสร็จ
begin— "เริ่มเขตคุ้มครอง" · โค้ดข้างในถ้าพัง จะไม่หลุดออกไปไกลrescue— "ถ้าในเขตพัง ให้กระโดดมาทำตรงนี้แทน" · บรรทัดputs split_bill(...)พังกลางทาง เลยไม่ได้พิมพ์อะไรend— ปิดเขต · จากนั้นโปรแกรมเดินต่อปกติ "คิดเงินเสร็จ" จึงถูกพิมพ์
เทียบ Go: Go ไม่มีคู่คำนี้ — ทุกจุดที่อาจพังต้องเขียน if err != nil { ... } ต่อท้ายทีละจุด ·
ใน Ruby เขียน begin ... rescue ครอบทีเดียว คลุมได้หลายบรรทัด
ถ้าเคยเขียน TypeScript: begin ≈ try · rescue ≈ catch — คำต่างกันแต่หน้าที่เดียวกัน
รับเฉพาะชนิดที่อยากรับ
ในชีวิตจริง error มีหลายแบบ และแต่ละแบบควรจัดการต่างกัน ·
Ruby ให้เขียนชื่อชนิดไว้หลัง rescue ได้ แล้วเขียนกี่ช่องก็ได้
ตัวอย่างนี้จำนวนคนมาเป็นข้อความ (เหมือนที่ผู้ใช้พิมพ์มาในฟอร์ม) จึงพังได้สองแบบ:
# rescue2.rb — แยกรับทีละชนิด
def split_bill(total, people_text)
people = Integer(people_text) # แปลงข้อความเป็นตัวเลข
total / people
end
["3", "0", "สาม"].each do |text|
begin
puts "คนละ #{split_bill(900, text)} บาท"
rescue ZeroDivisionError
puts "จำนวนคนเป็น 0 ไม่ได้"
rescue ArgumentError => e
puts "อ่านจำนวนคนไม่ออก: #{e.message}"
ensure
puts " (ปิดบิล)"
end
end
Integer(people_text)— แปลงข้อความเป็นตัวเลข · หน้าตาเหมือนชื่อ class เพราะขึ้นต้นตัวใหญ่ แต่จริง ๆ เป็น method ธรรมดาที่ Ruby มีให้ ·"3"ได้ 3 · แต่"สาม"แปลงไม่ได้ จะโยนArgumentError("ค่าที่ส่งมาใช้ไม่ได้" — เคยเจอใน บท 93)[...].each do |text| ... end— วนทีละตัวในรายการ ตัวปัจจุบันชื่อtext(ทวน บท 93)rescue ZeroDivisionError— ช่องนี้รับเฉพาะเรื่องหารด้วยศูนย์rescue ArgumentError => e— รับเฉพาะ ArgumentError · ท่อน=> eแปลว่า "เอา error ก้อนนั้นมาใส่ตัวแปรชื่อe" จะได้เรียกe.message(ข้อความ) มาพิมพ์ได้ · ตั้งชื่ออื่นแทนeก็ได้ensure— ทำเสมอ ไม่ว่าจะพังหรือไม่ (ละเอียดในหัวข้อ 6)
$ ruby rescue2.rb
คนละ 300 บาท
(ปิดบิล)
จำนวนคนเป็น 0 ไม่ได้
(ปิดบิล)
อ่านจำนวนคนไม่ออก: invalid value for Integer(): "สาม"
(ปิดบิล)
แต่ละรอบไปตกคนละช่อง: "3" ไม่พัง · "0" ตกช่อง ZeroDivisionError · "สาม" ตกช่อง ArgumentError
Ruby ไล่ช่อง rescue จากบนลงล่าง แล้วเข้าช่องแรกที่ชนิดตรง — เข้าได้ช่องเดียว
แล้วถ้า error ที่เกิดไม่ตรงกับช่องไหนเลยล่ะ?
# only_one.rb — รับไว้แค่ชนิดเดียว
begin
puts 900 / 0
rescue ArgumentError
puts "รับ ArgumentError ไว้"
end
puts "ไปต่อ"
$ ruby only_one.rb
only_one.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)
from only_one.rb:3:in '<main>'
ช่อง rescue รอแค่ ArgumentError · ZeroDivisionError วิ่งผ่านหน้าไปเฉย ๆ แล้วไม่มีใครรับต่อ โปรแกรมเลยพัง · ไม่พิมพ์ทั้ง "รับ ArgumentError ไว้" และ "ไปต่อ"
ช่อง rescue แต่ละช่องเหมือนป้ายแผนรับมือที่ติดผนังครัวไว้:
"ถ้าแก๊สหมด → โทรร้านแก๊ส" · "ถ้าลูกค้าแพ้อาหาร → เรียกผู้จัดการ"
เกิดเรื่องขึ้น พนักงานอ่านป้ายจากบนลงล่าง เจอป้ายที่ตรงก็ทำตามป้ายนั้นแล้วจบ ·
เรื่องที่ไม่มีป้ายไหนเขียนไว้ ก็ต้องส่งต่อขึ้นไปให้ชั้นบน
ที่ไม่เหมือนกัน: พนักงานจริงใช้วิจารณญาณได้ เจอเรื่องคล้าย ๆ ก็ประยุกต์เอา ·
แต่ Ruby ดูชื่อชนิดอย่างเดียว ไม่ตรงคือไม่รับ ·
และ rescue เปล่า ๆ ที่ไม่เขียนชนิด ดูเหมือน "รับทุกเรื่อง" แต่จริง ๆ รับแค่ "เรื่องทั่วไป" (หัวข้อ 5 จะเล่า)
5 · raise — โยน error เอง และสร้างชนิดของตัวเอง
บางทีเราอยากเป็นคนโยนเอง เช่น ลูกค้าสั่งเมนูที่หมดแล้ว — โค้ดไม่ได้พังทางเทคนิค แต่ทำต่อไม่ได้ทางธุรกิจ
นึกถึงกริ่งไฟไหม้ในหัวข้อ 2: raise คือเราเดินไปกดกริ่งเองเมื่อเห็นปัญหา ·
ใช้ raise และสร้างชนิด error ของเราเองได้ในสองบรรทัด:
# raise.rb — โยน error เอง
class OutOfStockError < StandardError
end
def order(menu, stock)
raise OutOfStockError, "#{menu} หมดแล้ว" if stock == 0
"ได้ #{menu} 1 จาน"
end
puts order("ข้าวผัด", 5)
begin
puts order("ต้มยำ", 0)
rescue OutOfStockError => e
puts "ขอโทษครับ: #{e.message}"
end
$ ruby raise.rb
ได้ ข้าวผัด 1 จาน
ขอโทษครับ: ต้มยำ หมดแล้ว
class OutOfStockError < StandardError+end— สร้างชนิด error ใหม่ ชื่อ OutOfStockError โดยสืบทอดจากStandardError(แม่ของ error "ทั่วไป" ใน Ruby) · ข้างในว่างได้ เพราะได้ของทุกอย่างจากแม่มาแล้วraise ชนิด, "ข้อความ"— โยน error ชนิดนั้นออกไป พร้อมข้อความ... if stock == 0— Ruby ให้เขียนifไว้ท้ายบรรทัดได้ อ่านว่า "ทำบรรทัดนี้ ถ้า stock เป็น 0"- พอ
raiseแล้ว method หยุดทันที — บรรทัด"ได้ ... 1 จาน"ไม่ถูกรันในรอบต้มยำ rescue OutOfStockError => e— รับชนิดที่เราเพิ่งสร้าง แบบเดียวกับหัวข้อ 4
เพราะ error ทุกชนิดคือ class ที่สืบทอดกันเป็นตระกูล (ทวน < จาก บท 95) ·
rescue ชนิดไหน ก็รับลูกหลานของชนิดนั้นด้วย — เช่น rescue StandardError รับ OutOfStockError ได้ เพราะมันเป็นลูกของ StandardError
เทียบ Go: เหมือน errors.New("...") หรือสร้าง struct ที่มี method Error() ·
ต่างกันที่ Go คืน error ออกไป ส่วน Ruby โยน มันออกไป
< StandardError เสมอ
rescue แบบไม่เขียนชนิด (และ rescue => e) รับเฉพาะตระกูล StandardError ·
ส่วน Exception คือแม่ใหญ่สุดที่ครอบเรื่องระดับระบบด้วย (เช่นการกด Ctrl+C สั่งหยุดโปรแกรม) จึงตั้งใจไม่ให้ rescue ธรรมดารับ ·
ถ้าเผลอให้ error ของเราสืบทอดจาก Exception:
# wrong_parent.rb — สืบทอดผิดแม่
class PaymentError < Exception
end
begin
raise PaymentError, "บัตรถูกปฏิเสธ"
rescue => e
puts "รับไว้แล้ว"
end
$ ruby wrong_parent.rb
wrong_parent.rb:6:in '<main>': บัตรถูกปฏิเสธ (PaymentError)
ไม่ได้พิมพ์ "รับไว้แล้ว" — rescue => e ปล่อยผ่าน โปรแกรมพัง · แก้แค่เปลี่ยน Exception เป็น StandardError
ท่าย่อ: rescue ห้อยท้าย method (ไม่ต้องมี begin)
ถ้าอยากคุ้มครองทั้ง method ไม่ต้องเขียน begin — เอา rescue ไปห้อยก่อน end ของ method ได้เลย ·
โค้ด Rails ใช้ท่านี้บ่อยมาก จำหน้าตาไว้:
# method_rescue.rb — rescue ห้อยท้าย method (ไม่ต้องมี begin)
def parse_people(text)
Integer(text)
rescue ArgumentError
puts "อ่านจำนวนคนไม่ออก ขอนับเป็น 1 คน"
1
end
p parse_people("4")
p parse_people("four")
$ ruby method_rescue.rb
4
อ่านจำนวนคนไม่ออก ขอนับเป็น 1 คน
1
เขตคุ้มครองคือ "ทั้งตัว method" · ถ้าพัง ค่าบรรทัดสุดท้ายของช่อง rescue (1) จะกลายเป็นค่าที่ method คืน ·
(p คือพิมพ์ค่าออกมาแบบเห็นชนิดชัด ๆ — ทวน บท 93)
- begin / rescue / end
- คู่คำที่ขึงเขตคุ้มครอง · ≈
try/catchของ TypeScript · พิมพ์บอก AI ได้ว่า “wrap only the risky line in begin/rescue and rescue ZeroDivisionError only” rescue ชนิด => e- รับเฉพาะ error ชนิดนั้น แล้วเก็บตัว error ไว้ในตัวแปร
e· ใช้e.messageดูข้อความ ·e.classดูชนิด · ถาม senior ได้ว่า “ตรงนี้ควร rescue ชนิดไหน ให้แคบที่สุดเท่าที่ได้” - StandardError · custom error
- StandardError คือแม่ของ error ทั่วไป · custom error คือชนิดที่เราสร้างเองด้วย
class XxxError < StandardError· พิมพ์บอก AI ได้ว่า “define a custom error class that inherits from StandardError” - method-level rescue
- เขียน
rescueห้อยท้าย method โดยไม่มีbegin(คู่มือ Ruby เรียกว่า begin-less exception handler) · ถาม senior ได้ว่า “ทีมนี้ชอบ rescue ท้าย method หรือ begin ครอบเฉพาะบรรทัด”
6 · ensure — ทำเสมอ ไม่ว่าจะพังหรือไม่
บางงานต้องทำทุกครั้ง ไม่ว่าทางจะราบรื่นหรือพังกลางทาง เช่น ปิดเตา ปิดไฟล์ ปิดการเชื่อมต่อ ·
ช่อง ensure มีไว้สำหรับงานแบบนี้ — ถ้าเคยใช้ defer ของ Go
(บทเรียน 08) มันคือความคิดเดียวกัน: "ออกทางไหนก็ต้องผ่านตรงนี้"
ประตูครัวมีป้ายติดไว้: "ใครออกจากครัว ต้องปิดแก๊สก่อนเสมอ" ·
ทำอาหารเสร็จเรียบร้อยก็ต้องปิด · ทำพังกลางทางแล้ววิ่งไปตามผู้จัดการก็ต้องปิด — ออกทางไหนก็ต้องผ่านป้ายนี้
ensure คือป้ายนั้น · rescue คือ "ถ้ามีปัญหาให้ทำอะไร" ส่วน ensure คือ "ไม่ว่ายังไงก็ทำ"
ที่ไม่เหมือนกัน: คนจริงอาจลืมอ่านป้าย แต่ Ruby ไม่เคยลืม รันช่อง ensure ให้ทุกครั้ง · และ ensure ไม่ได้ "แก้" ปัญหา — ถ้าไม่มี rescue รับ error ก็ยังลอยต่อขึ้นไปหลังปิดแก๊สเสร็จ
- ensure · cleanup
- ensure คือช่องที่รันเสมอ · งานในช่องนี้เรียกรวม ๆ ว่า cleanup (เก็บกวาด เช่นปิดไฟล์ ปิดการเชื่อมต่อ) ≈
deferของ Go · ≈finallyของ TypeScript · พิมพ์บอก AI ได้ว่า “put the cleanup in an ensure block so it runs even if this raises”
# predict.rb
def cook
puts "1. ตั้งกระทะ"
raise "แก๊สหมด"
puts "2. ผัดข้าว"
end
begin
cook
puts "3. เสิร์ฟ"
rescue => e
puts "4. แจ้งลูกค้า: #{e.message}"
ensure
puts "5. ปิดเตา"
end
ข้อ 1 ถึง 5 — ข้อไหนถูกพิมพ์ออกมาบ้าง? (raise "ข้อความ" แบบไม่บอกชนิด = โยน error ชนิดทั่วไปชื่อ RuntimeError ·
rescue => e = รับ error ทั่วไปทุกชนิด แล้วเก็บไว้ใน e)
$ ruby predict.rb
1. ตั้งกระทะ
4. แจ้งลูกค้า: แก๊สหมด
5. ปิดเตา
- ข้อ 2 หายไป — พอ
raiseแล้ว methodcookหยุดทันที บรรทัดที่เหลือไม่ได้รัน - ข้อ 3 หายไป — error ลอยออกจาก
cookมาถึงเขตbeginแล้วกระโดดไปที่rescueเลย ข้าม "เสิร์ฟ" - ข้อ 5 มาเสมอ — นี่คืองานของ
ensure· ถ้าไม่พัง (ลองลบบรรทัดraise) ก็ยังได้ "5. ปิดเตา" อยู่ดี
7 · ข้อดีข้อเสีย: Go กับ Ruby
ไม่มีแบบไหนดีกว่ากันทุกเรื่อง · ลองดูว่าแต่ละแบบแลกอะไรกับอะไร
ก่อนอื่น ลองทำแบบ "ขี้เกียจเช็ค" ใน Go — ทิ้ง error ด้วย _:
// Go: ทิ้ง error ด้วย _ แล้วไม่เช็ค
share, _ := splitBill(900, 0)
fmt.Println("คนละ", share)
$ go build ./... # ผ่าน ไม่มีคำเตือน
$ go run .
คนละ 0
โปรแกรม Go ไม่พัง แต่บอกว่าคนละ 0 บาท — ค่าผิดที่เดินต่อไปเงียบ ๆ · ถ้าเป็น Ruby เราได้เห็นแล้วว่ามันพังดัง ๆ แทน
| เรื่อง | Go (error เป็นค่า) | Ruby (โยน exception) |
|---|---|---|
| รู้ไหมว่าบรรทัดไหนพังได้ | ✅ รู้ — ดูจาก error ที่หัวฟังก์ชัน | ❌ ไม่รู้ — ต้องอ่านเอกสารหรือลองรัน |
| ลืมจัดการแล้วเกิดอะไร | เดินต่อด้วยค่าเพี้ยน ๆ (เช่น คนละ 0) | โปรแกรมหยุด พร้อมบอกว่าพังที่ไหน |
| ความยาวโค้ด | ยาว — if err != nil ทุกชั้น | สั้น — รับครั้งเดียวที่ชั้นที่จัดการได้จริง |
| ใครเตือนก่อนรัน | compiler เตือนบางกรณี | ไม่มีใครเตือน — ต้องพึ่ง test |
8 · ใน Rails: error ที่ไม่มีใครรับ = หน้า 500 (แต่ร้านไม่ปิด)
แอป Rails ก็คือโค้ด Ruby · ถ้าโค้ดใน controller (หัวหน้าเชฟที่รับใบสั่ง — เจอละเอียดใน บท 102) โยน error แล้วไม่มีใครรับ Rails จะเป็นคนรับชั้นบนสุดให้เอง แล้วตอบกลับไปเป็นหน้า error · server ไม่ดับ
ลองในแอป wongnok_blog (แอปบล็อกในสไลด์ — บทความหนึ่งชิ้นเรียกว่า Post) ·
ตอนนี้ยังไม่มี database (รอ Day 2) จึงจำลองบทความด้วย Hash แทน
การทำให้โค้ดนี้รันได้ต้องรู้เรื่อง route (พนักงานต้อนรับที่ชี้ว่า URL ไหนไปหาใคร — บท 101) และ controller (บท 102) ก่อน · ตอนนี้ให้ดูแค่ว่า error ใน Rails หน้าตาเป็นยังไง
ไฟล์ app/controllers/posts_controller.rb:
class PostsController < ApplicationController
# จำลองบทความด้วย Hash (ของจริงจะอยู่ใน database — Day 2)
POSTS = { 1 => "Rails วันแรก" }
def show
title = POSTS[params[:id].to_i] # หาไม่เจอ → nil
render plain: title.upcase
end
end
class PostsController < ApplicationController— controller ของบทความ สืบทอดจาก controller แม่ที่rails newสร้างไว้ให้ (บท 102)POSTS = { 1 => "Rails วันแรก" }— Hash ที่มีบทความเลข 1 อยู่ชิ้นเดียว · ชื่อตัวใหญ่ล้วน = ค่าคงที่ (บท 91)params[:id]— เลขที่อยู่ใน URL เช่น/posts/2ได้"2"(เหมือน "จานที่ 2" ในใบสั่ง — บท 102) ·.to_iแปลงเป็นตัวเลขrender plain: ...— ส่งข้อความกลับไปให้ browser (บท 102)title.upcase— ทำเป็นตัวพิมพ์ใหญ่ · ถ้า title เป็น nil จะพัง (nil ไม่มีupcase)
เปิด server (bin/rails server) แล้วยิง request สามครั้ง: บทความที่มี → บทความที่ไม่มี → บทความที่มีอีกที
$ curl http://localhost:3000/posts/1
RAILS วันแรก
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/posts/2
500
$ curl http://localhost:3000/posts/1
RAILS วันแรก
# log ของ server ตอน /posts/2 (บางส่วน)
Completed 500 Internal Server Error in 0ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
NoMethodError (undefined method 'upcase' for nil):
app/controllers/posts_controller.rb:7:in 'PostsController#show'
curl— โปรแกรมส่ง request จาก terminal (เหมือนเปิด browser แต่ได้ผลเป็นข้อความ) ·-s -o /dev/null= ไม่ต้องโชว์เนื้อหาหน้าเว็บ ·-w "%{http_code}\n"= พิมพ์แค่รหัสสถานะ · 500 = "server พังเอง" (Internal Server Error) · ข้อความที่ส่งกลับไม่มีการขึ้นบรรทัดใหม่ท้าย ถ้าลองเองใน zsh อาจเห็น%ต่อท้ายRAILS วันแรก— ไม่ใช่ error- log บอกชนิด error · ข้อความ · และไฟล์กับบรรทัดที่พัง — อ่านแบบเดียวกับหัวข้อ 3 · ตัวเลข
msในบรรทัด Completed เป็นเวลาที่ใช้ (ของคุณจะต่างไปทุกครั้ง) ข้ามไปได้ - request ที่สามยังได้คำตอบปกติ — พังแค่ request เดียว ร้านยังเปิดอยู่
- ถ้าเปิดด้วย browser จะได้หน้า debug ของ Rails ชื่อ Action Controller: Exception caught (เฉพาะตอนพัฒนาในเครื่อง)
ลูกค้าสั่ง "บทความจานที่ 2" (request) · หัวหน้าเชฟ (controller) ไปเปิดตู้แล้วไม่เจอวัตถุดิบ
แต่ดันลงมือทำต่อ — ทำไม่ได้ ครัวโวยวายขึ้นมา (error)
ผู้จัดการร้าน (Rails) เดินมารับเรื่อง ยกป้าย "ขออภัย ครัวมีปัญหา" ไปให้โต๊ะนั้น (หน้า 500) ·
โต๊ะอื่นที่สั่งจานที่ 1 ยังได้อาหารตามปกติ ร้านไม่ต้องปิด
ที่ไม่เหมือนกัน: ผู้จัดการจริงอาจเสนอเมนูอื่นแทนหรือขอโทษอย่างสุภาพ · แต่ Rails ทำแค่ส่งหน้า error มาตรฐาน — ถ้าอยากได้คำตอบที่ดีกว่า เช่น "ไม่พบบทความนี้" (404) เราต้องเขียนเอง · และ 500 ที่ได้ก็ไม่ได้บอกลูกค้าว่าพลาดตรงไหน (รายละเอียดอยู่ใน log ฝั่งร้านเท่านั้น)
ในโค้ด Rails จริง คุณจะเจอวิธี "ขึงคนรับให้ทุก action ทีเดียว" หน้าตาแบบนี้:
rescue_from ActiveRecord::RecordNotFound, with: :render_404
— อ่านว่า "ถ้า action ไหนโยน error ชนิด หาข้อมูลไม่เจอ ให้ไปเรียก method render_404 แทน" ·
มันคือ rescue ที่ครอบทั้ง controller · ตอนนี้แค่จำหน้าตาไว้ ใช้จริงตอน Day 2
- 500 Internal Server Error
- รหัสคำตอบที่แปลว่า "ฝั่ง server พังเอง ไม่ใช่ความผิดของคนขอ" · ใน Rails มักมาจาก exception ที่ไม่มีใครรับ · ถาม senior ได้ว่า “หน้า 500 นี้ ดู log ตรงไหนถึงจะเจอบรรทัดที่พัง”
- server log
- ข้อความที่
bin/rails serverพิมพ์ออกมาทุก request — บอกว่าใครขออะไร ได้รหัสอะไร และถ้าพังก็บอกชนิดกับบรรทัด · เวลาถาม AI ให้แปะ log ส่วนนี้ไปด้วยเสมอ ไม่ใช่แค่บอกว่า "ได้ 500" - rescue_from
- คำสั่งของ controller ที่บอกว่า "error ชนิดนี้ ไม่ว่าเกิดใน action ไหน ให้ method นี้รับ" · พิมพ์บอก AI ได้ว่า “use rescue_from in ApplicationController to render a custom 404 page” (Day 2)
9 · Duck typing — ไม่ถามว่าเป็นใคร ถามแค่ว่า "ทำได้ไหม"
เปลี่ยนเรื่องนิดหนึ่ง แต่เกี่ยวกันมาก · ใน Go ถ้าอยากให้ฟังก์ชันรับ "ของอะไรก็ได้ที่มี Title()" คุณต้องประกาศ interface ไว้ก่อน แล้ว compiler จะตรวจให้ตอน build (ทวน บทเรียน 10):
// Go (บางส่วน — ไฟล์เต็มมี package, import และ type Menu ที่มี method Title())
type HasTitle interface {
Title() string
}
func show(x HasTitle) {
fmt.Println(x.Title())
}
// ... ใน main
show(Menu{}) // Menu มี Title() → ผ่าน
show(42) // int ไม่มี Title() → build ไม่ผ่าน
$ go build ./...
# goduck
./main.go:19:7: cannot use 42 (constant of type int) as HasTitle value in argument to show: int does not implement HasTitle (missing method Title)
(# goduck คือชื่อ package ของตัวอย่าง) · Go จับได้ก่อนรัน · โปรแกรมไม่มีโอกาสได้รันสักบรรทัด
Ruby ไม่มี interface ให้ประกาศเลย · method แค่เรียก x.title ไปเฉย ๆ —
ใครส่งอะไรมา ถ้ามี title ก็ผ่าน ไม่สนว่าเป็น class อะไร:
# duck.rb — ใครมี .title ก็ใช้ได้
class Post
def title
"Rails วันแรก"
end
end
class Menu
def title
"ข้าวผัดกระเพรา"
end
end
def show(x)
puts x.title
end
show(Post.new)
show(Menu.new)
show(42)
class Postกับclass Menu— สอง class ไม่เกี่ยวกันเลย ไม่มีแม่ร่วม ไม่มี interface · แค่บังเอิญมี method ชื่อtitleเหมือนกัน (Post ตรงนี้เป็น class จำลองธรรมดา ไม่ใช่ Post ของ Rails)def show(x)— ไม่บอกเลยว่าxต้องเป็นอะไรPost.new— สร้าง object จากแม่พิมพ์ Post (ทวน บท 95)
$ ruby duck.rb
Rails วันแรก
ข้าวผัดกระเพรา
duck.rb:15:in 'Object#show': undefined method 'title' for an instance of Integer (NoMethodError)
puts x.title
^^^^^^
from duck.rb:20:in '<main>'
สองบรรทัดแรกรันผ่านไปก่อน แล้วเพิ่งมาพังตอนถึง show(42) จริง ๆ ·
นี่คือความต่างที่สำคัญ: Go ตรวจตอน build · Ruby รู้ตัวตอนรันถึงบรรทัดนั้น
for an instance of Integer— "กับ object ที่เป็นตัวเลข (Integer)" · instance = object ที่สร้างจาก class (บท 95)- บรรทัด
puts x.titleกับ^^^^^^ข้างใต้ — Ruby ยกบรรทัดที่พังมาให้ดู แล้วขีดเส้นใต้จุดที่พัง (ตรง.title)
ชื่อ duck typing มาจากสำนวน "ถ้ามันเดินเหมือนเป็ด ร้องก๊าบ ๆ เหมือนเป็ด ก็ถือว่ามันเป็นเป็ด"
Ruby ไม่ขอดูใบรับรองว่าคุณ "เป็นเป็ดจริงไหม" (เป็น class อะไร) · ขอแค่ตอนที่ต้องร้อง คุณร้องได้ (มี method title) ก็พอ ·
Go ต่างออกไป: ต้องมีรายชื่อ "สิ่งที่เป็ดต้องทำได้" (interface) แล้ว compiler ตรวจทุกตัวก่อนปล่อยลงบ่อ
ที่ไม่เหมือนกัน: คนดูเป็ดจริงตัดสินได้ตั้งแต่แรกเห็น ·
แต่ Ruby ไม่ดูก่อนเลย — ปล่อยทุกตัวลงบ่อ แล้วรู้ว่าตัวไหนไม่ใช่เป็ด
ก็ตอนสั่งให้ร้องแล้วมันร้องไม่ได้ (พังตอนรัน) · ถ้าอยากรู้ก่อน ต้องถามเอง (respond_to? ข้างล่าง) หรือเขียน test
respond_to? — ถามก่อนว่า "ทำได้ไหม"
ทุก object ใน Ruby มี method respond_to? ไว้ถามล่วงหน้าว่า "ตอบ method ชื่อนี้ได้ไหม":
# respond.rb — ถามก่อนว่า "ทำได้ไหม"
class Menu
def title
"ข้าวผัดกระเพรา"
end
end
p Menu.new.respond_to?(:title)
p 42.respond_to?(:title)
def safe_show(x)
if x.respond_to?(:title)
puts "หัวข้อ: #{x.title}"
else
puts "(#{x.class} ไม่มี title)"
end
end
safe_show(Menu.new)
safe_show(42)
$ ruby respond.rb
true
false
หัวข้อ: ข้าวผัดกระเพรา
(Integer ไม่มี title)
respond_to?(:title)— ส่งชื่อ method เป็น symbol (:title) · ได้trueหรือfalse· ชื่อลงท้าย?ตามธรรมเนียม "method ที่ตอบใช่/ไม่ใช่" (บท 93)x.class— ถามว่า object นี้เป็น class อะไร · 42 ได้Integer- ใช้แบบนี้ได้ แต่ไม่ต้องเขียนทุกที่ — ส่วนใหญ่ปล่อยให้พังแล้วแก้ที่ต้นเหตุดีกว่า
- duck typing
- การตัดสินว่า object ใช้ได้ไหมจากสิ่งที่มันทำได้ (method ที่มี) ไม่ใช่จากชนิดของมัน · ถาม AI ได้ว่า “this method relies on duck typing — what objects can I pass to it?”
- runtime · compile time
- compile time = ตอน build ก่อนรัน (Go ตรวจ interface ตรงนี้) · runtime = ตอนโปรแกรมกำลังรันอยู่ (Ruby รู้ตรงนี้) · ถาม senior ได้ว่า “error นี้จับได้ตั้งแต่ compile time ไหม หรือต้องรอ runtime”
- respond_to?
- method ที่ทุก object มี ถามว่า "ตอบ method ชื่อนี้ได้ไหม" · พิมพ์บอก AI ได้ว่า “guard this call with respond_to?(:title) instead of checking the class”
- NoMethodError
- error ที่ได้เมื่อเรียก method ที่ object นั้นไม่มี — ตัวที่คุณจะเจอบ่อยที่สุดใน Ruby (หัวข้อถัดไป) · ถาม senior ได้ว่า “NoMethodError ตัวนี้ ควรกันที่ต้นทางหรือเช็คที่ปลายทาง”
10 · NoMethodError — error ที่จะเจอบ่อยที่สุด
error ที่มือใหม่ Ruby เจอบ่อยมาก คือเรียก method บน nil —
แปลว่า "ตัวแปรนี้ไม่มีค่า ทั้งที่คุณคิดว่ามี" · มักเกิดจากหาของไม่เจอมาตั้งแต่ก่อนหน้า:
# nil_title.rb — ของที่หาไม่เจอ = nil
posts = { 1 => "Rails วันแรก" } # จำลองบทความด้วย Hash
title = posts[2] # ไม่มีบทความเลข 2 → ได้ nil
puts title.upcase
$ ruby nil_title.rb
nil_title.rb:4:in '<main>': undefined method 'upcase' for nil (NoMethodError)
puts title.upcase
^^^^^^^
| ท่อน | แปลว่า |
|---|---|
undefined method 'upcase' | "ไม่รู้จัก method ชื่อ upcase" · ชื่อในเครื่องหมาย '...' คือ method ที่เราพยายามเรียก |
for nil | ของที่อยู่หน้าจุดเป็น nil · ถ้าเป็นของชนิดอื่นจะเขียนว่า for an instance of Integer (เป็นตัวเลข) แบบใน duck.rb |
^^^^^^^ | Ruby ขีดเส้นใต้ให้เลยว่าพังตรง .upcase |
วิธีแก้: อย่าแก้ที่บรรทัดที่พัง · ถามว่า "ทำไม title ถึงเป็น nil" แล้วย้อนไปบรรทัดก่อนหน้า —
ที่นี่คือ posts[2] หาไม่เจอ · วิธีรับมือ nil ให้สวย ๆ อยู่ใน บท 94
11 · กับดักของคนที่มาจาก Go
มือ Go ที่เพิ่งมา Ruby มักสะดุดเรื่องเดิม ๆ · เรื่อง "ไม่มีใครบังคับให้รับ error" คุณเห็นกับตาแล้วในหัวข้อ 2 และ 8 · เรื่อง "เขียน for loop แทน map/select" ทวนได้ใน บท 92 · ที่เหลือ 4 ข้อ รันให้ดูสั้น ๆ:
① เลข 0 นับว่า "จริง"
# zero.rb
count = 0
if count
puts "0 ถือว่าจริง!"
end
p count.zero?
p count.positive?
$ ruby zero.rb
0 ถือว่าจริง!
true
false
ใน Ruby มีแค่ false กับ nil ที่นับว่าเท็จ · อยากเช็คว่าเป็นศูนย์ให้ถามตรง ๆ ด้วย .zero? หรือ .positive? (มากกว่า 0) ·
(ใน Go เขียน if count กับตัวเลขไม่ได้อยู่แล้ว · ข้อนี้คนจาก TypeScript โดนบ่อยกว่า)
② บรรทัดสุดท้ายคือค่าที่คืน — เผลอเติมบรรทัดแล้วค่าเปลี่ยน
# last_line.rb
def full_name(first, last)
name = "#{first} #{last}"
puts "สร้างชื่อเสร็จแล้ว" # เผลอเติมบรรทัดนี้ต่อท้าย
end
p full_name("สมชาย", "ใจดี")
$ ruby last_line.rb
สร้างชื่อเสร็จแล้ว
nil
คนจาก Go รอ return name แต่ Ruby คืนค่าของบรรทัดสุดท้าย ซึ่งคือ puts → nil เสมอ ·
ไม่มี error ไม่มีคำเตือน — แล้ว nil ตัวนี้จะไปพังเป็น NoMethodError ที่อื่นทีหลัง
③ ไม่มี type บอก — ส่งผิดชนิดแล้วไม่พัง แต่ได้ผลแปลก
# no_types.rb
def total(price, qty)
price * qty
end
p total(5, 3)
p total("5", 3)
$ ruby no_types.rb
15
"555"
ส่ง "5" (ข้อความ) แทน 5 (ตัวเลข) — ไม่พังด้วยซ้ำ เพราะ string ก็มี * (แปลว่า "ทำซ้ำ 3 ครั้ง") ·
duck typing ทำงาน "ถูก" แต่ผลผิด · เมื่อไม่มี type ให้อ่าน วิธีรู้ว่า method รับอะไรคืออ่าน test (โค้ดที่ทดสอบ method นั้น — Day 4)
และลองของจริงใน console (ครัวทดลองหลังร้าน — บท 104)
④ ชื่อไฟล์ใน Rails — ใช้ snake_case
ใน Go เราตั้งชื่อไฟล์อะไรก็ได้ แล้ว import เอาเอง · แต่ Rails หาไฟล์จากชื่อ โดยไม่ต้อง import
(เหมือนร้านแฟรนไชส์ที่ของทุกชิ้นต้องอยู่ช่องที่ตั้งชื่อไว้ — ละเอียดใน บท 99)
กฎคือ class BlogPost ต้องอยู่ในไฟล์ blog_post.rb — ตัวเล็กทั้งหมด คั่นคำด้วย _ เรียกว่า snake_case ·
ถ้าตั้งชื่อ blogpost.rb (ติดกันหมด) Rails จะฟ้อง uninitialized constant BlogPost = "หา BlogPost ไม่เจอ"
ภารกิจ
🎯 ภารกิจ: เครื่องแบ่งบิลที่ไม่ล้ม (~10 นาที)
เริ่มจากโค้ดนี้ ซึ่งพังทันทีที่เจอ "0":
# bill.rb — ขั้นที่ 1: ยังไม่มีใครรับ error
def split_bill(total, people_text)
people = Integer(people_text)
total / people
end
puts split_bill(900, "3")
puts split_bill(900, "0")
puts split_bill(900, "สาม")
puts "ร้านปิด"
- สร้างไฟล์
bill.rbวางโค้ดข้างบน แล้วรันruby bill.rb— ควรเห็น300แล้วพังด้วย(ZeroDivisionError) - แก้
split_billให้คืนข้อความแทนตัวเลข: ถ้าสำเร็จคืน"คนละ 300 บาท"(คำใบ้: เปลี่ยนบรรทัดสุดท้ายเป็น string ที่มี#{total / people}อยู่ข้างใน) - เพิ่ม
rescueห้อยท้าย method (หัวข้อ 5) สองช่อง: หารด้วยศูนย์ → คืน"จำนวนคนต้องมากกว่า 0"· แปลงตัวเลขไม่ได้ → คืน"อ่านจำนวนคนไม่ออก (ArgumentError)"โดยดึงชื่อชนิดมาจากe.class - ต่อท้ายไฟล์ด้วยสามบรรทัดนี้ เพื่อเช็คตัวเอง — ต้องได้
trueทั้งสามบรรทัด:p split_bill(900, "3") == "คนละ 300 บาท" p split_bill(900, "0") == "จำนวนคนต้องมากกว่า 0" p split_bill(900, "สาม") == "อ่านจำนวนคนไม่ออก (ArgumentError)"
ถ้าเจอ error นี้ แปลว่า…
undefined local variable or method 'e' for main (NameError)→ คุณใช้eแต่ลืมเขียน=> eหลังrescue ArgumentError- ยังพังด้วย
invalid value for Integer(): "สาม" (ArgumentError)→ มีแค่ช่อง ZeroDivisionError ยังไม่มีช่องรับ ArgumentError syntax errors found (SyntaxError)พร้อมข้อความexpected an `end` to close the `def` statement→ ลบendของ method ไปเผลอ ๆ · rescue ห้อยท้ายต้องอยู่ก่อนendของdef
# bill.rb — ขั้นที่ 2: มีคนรับแล้ว
def split_bill(total, people_text)
people = Integer(people_text)
"คนละ #{total / people} บาท"
rescue ZeroDivisionError
"จำนวนคนต้องมากกว่า 0"
rescue ArgumentError => e
"อ่านจำนวนคนไม่ออก (#{e.class})"
end
puts split_bill(900, "3")
puts split_bill(900, "0")
puts split_bill(900, "สาม")
puts "ร้านปิด"
# เช็คตัวเอง: ต้องได้ true ทั้งสามบรรทัด
p split_bill(900, "3") == "คนละ 300 บาท"
p split_bill(900, "0") == "จำนวนคนต้องมากกว่า 0"
p split_bill(900, "สาม") == "อ่านจำนวนคนไม่ออก (ArgumentError)"
$ ruby bill.rb
คนละ 300 บาท
จำนวนคนต้องมากกว่า 0
อ่านจำนวนคนไม่ออก (ArgumentError)
ร้านปิด
true
true
true
- ไม่ต้องมี
beginเพราะ rescue ห้อยท้าย method คุ้มครองทั้งตัว method อยู่แล้ว - ค่าที่ method คืน = บรรทัดสุดท้ายของช่องที่ได้ทำ (ช่องปกติ หรือช่อง rescue ช่องใดช่องหนึ่ง)
- "ร้านปิด" ถูกพิมพ์แล้ว — ไม่มีอะไรหลุดออกมาจน program หยุด
- ลองต่อ: ลบช่อง
rescue ZeroDivisionErrorออก แล้วดูว่า"0"ไปตกที่ไหน (คำใบ้: หัวข้อ 4)
แบบทดสอบ
โค้ดนี้พิมพ์อะไรออกมา? (; ใช้แทนการขึ้นบรรทัดใหม่)begin; puts 900 / 0; rescue ArgumentError; puts "รับไว้"; end; puts "ไปต่อ"
ตัวเลือกที่ 3 ถูก — ตรงกับผลรันของ only_one.rb ในหัวข้อ 4 ·
ช่อง rescue รอแค่ ArgumentError แต่สิ่งที่เกิดคือ ZeroDivisionError จึงวิ่งผ่านไป แล้วไม่มีใครรับต่อ
ตัวเลือกที่ 1 ผิดเพราะ rescue ดูชื่อชนิด ไม่ใช่รับทุกเรื่อง · ตัวเลือกที่ 2 ผิดเพราะ Ruby ไม่เคยข้าม error เงียบ ๆ — ไม่มีคนรับคือพัง · ตัวเลือกที่ 4 ผิดเพราะ Ruby ไม่ตรวจล่วงหน้าว่าชนิดตรงไหม รันได้ปกติจนถึงตอนพัง
จุดที่ต้องจำ: rescue ระบุชนิดไหน ก็รับแค่ชนิดนั้น (และลูกหลานของมัน) — นอกนั้นลอยผ่านไป
โค้ดในช่อง ensure ถูกรันเมื่อไหร่?
ตัวเลือกที่ 1 ถูก — ใน predict.rb โค้ดพังกลางทาง ก็ยังได้ "5. ปิดเตา" ·
และใน rescue2.rb รอบ "3" ที่ไม่พังก็ได้ "(ปิดบิล)" เหมือนกัน
ตัวเลือกที่ 2 กับ 3 แต่ละข้อถูกแค่ครึ่งเดียว · ตัวเลือกที่ 4 ผิดเพราะ ensure ไม่สนว่ามีคนรับหรือไม่ — มันทำเสมอ (แม้ error จะลอยต่อขึ้นไป ensure ก็ทำก่อนออก)
จุดที่ต้องจำ: ensure ≈ defer ของ Go — "ออกทางไหนก็ต้องผ่านตรงนี้" · ใช้กับงานเก็บกวาด
มี def show(x); puts x.title; end · ส่งอะไรเข้าไปแล้วไม่พัง?
ตัวเลือกที่ 4 ถูก — ใน duck.rb ทั้ง Post และ Menu ใช้ได้ ทั้งที่ไม่เกี่ยวกันเลย · นี่คือ duck typing
ตัวเลือกที่ 1 ผิดเพราะ Ruby ไม่ดูชื่อ class · ตัวเลือกที่ 2 ผิดเพราะ Ruby ไม่มี interface ให้ประกาศ · ตัวเลือกที่ 3 ผิดครึ่งหนึ่ง — ไม่ตรวจก่อนรันก็จริง แต่พังตอนรัน ถ้าส่ง 42 เข้าไป (NoMethodError)
จุดที่ต้องจำ: Ruby ถามแค่ "ทำได้ไหม" และถามตอนรันถึงบรรทัดนั้นเท่านั้น
เห็น undefined method 'title' for nil (NoMethodError) — ควรคิดอะไรเป็นอย่างแรก?
ตัวเลือกที่ 2 ถูก — for nil บอกว่าของที่อยู่หน้าจุดคือ nil · ต้นเหตุมักอยู่บรรทัดก่อนหน้า
เช่น posts[2] ที่หาไม่เจอใน nil_title.rb
ตัวเลือกที่ 1 ผิดเพราะปัญหาไม่ได้อยู่ที่ class — ถ้าตัวแปรมีของจริง title อาจทำงานได้ปกติ · ตัวเลือกที่ 3 ผิดเพราะไม่เกี่ยวกับรุ่น · ตัวเลือกที่ 4 แค่ซ่อนอาการ — nil ยังอยู่ และจะไปโผล่ที่อื่น
จุดที่ต้องจำ: อ่าน error ส่วน for ... ให้ออก แล้วแก้ที่ต้นเหตุ ไม่ใช่ที่บรรทัดที่พัง
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง (ลองแล้ว) | ต้องสนใจไหม |
|---|---|---|---|
| 30 | ใต้โค้ด Go: "ลืมไม่ได้ compiler เตือน" | compiler เตือนเฉพาะตอนรับ err มาแล้วไม่ใช้ (declared and not used: err) · ถ้าทิ้งด้วย _ build ผ่าน และได้ คนละ 0 (หัวข้อ 7) |
นิดหน่อย — Go ก็ลืมได้ถ้าตั้งใจทิ้ง |
| 30 | do_thing · handle(e) · cleanup |
เป็นชื่อสมมุติ รันตรง ๆ ไม่ได้ · บทนี้ใช้ตัวอย่างแบ่งบิลแทน | ไม่ต้อง |
| 30 | rescue_from ActiveRecord::RecordNotFound, with: :render_404 |
ถึงไม่เขียน rescue_from Rails ก็ตอบ 404 ให้ error ชนิดนี้อยู่แล้ว (ลองแล้ว) · rescue_from ใช้เมื่ออยากเลือกหน้าตาคำตอบเอง | ยังไม่ต้อง — Day 2 |
| 31 | render partial: "post", object: anything_that_has_title |
partial = ชิ้นส่วนของหน้าเว็บ (Day 3) · ใจความคือ view ใช้ได้กับทุก object ที่มี .title = duck typing |
ยังไม่ต้อง |
| 32 | "blogPost.rb จะทำให้ Rails หาไฟล์ไม่เจอ" |
Rails 8.1.4 โหลด blogPost.rb ได้ · ที่หาไม่เจอคือ blogpost.rb · ข้อสรุป "ใช้ blog_post.rb" ยังถูก |
ใช้ snake_case ไว้ แล้วไม่ต้องจำข้อยกเว้น |
ติดตรงไหน หรือรันแล้วได้ผลไม่เหมือนในบท — ถามกลับมาได้เลย พร้อมแปะข้อความ error ทั้งก้อนที่ได้
อ้างอิง
- Ruby 4.0 — Exceptions — อ่านหัวข้อ Unrescued Exceptions (ตรงกับหัวข้อ 2) แล้วต่อด้วย Rescued Exceptions ซึ่งมีหัวข้อย่อย Rescue Clauses · Ensure Clause · Begin-Less Exception Handlers (= rescue ห้อยท้าย method) · ปิดด้วย Custom Exceptions ที่แนะนำให้สืบทอดจาก StandardError (หรือ RuntimeError ซึ่งเป็นลูกของมัน)
- Ruby 4.0 — Exception Handling (syntax) — หน้าสั้น · หาประโยค “By default, StandardError and its subclasses are rescued” ซึ่งเป็นหลักฐานของกล่องเตือนในหัวข้อ 5
- Ruby 4.0 — class NoMethodError — ประโยคแรกของหน้าบอกว่ามันเกิดเมื่อ "เรียก method ที่ object นั้นไม่มี" พร้อมตัวอย่างสั้น ๆ — ชนิดเดียวกับหัวข้อ 9–10
- Ruby 4.0 — Object#respond_to? —
อ่านคำอธิบายของ method เดียว · สังเกตว่ารับชื่อ method เป็น symbol (
:title) - Go Blog — Error handling and Go —
อ่านช่วงต้นจนถึงตัวอย่าง
if err != nilเพื่อทวนฝั่ง Go ก่อนเทียบกับ Ruby - Rails Guides — rescue_from —
ตัวอย่างแรกคือ
rescue_from ActiveRecord::RecordNotFound, with: :record_not_foundโครงเดียวกับสไลด์หน้า 30 · ตอนนี้อ่านให้เห็นภาพพอ ใช้จริง Day 2