Ruby 中 **Proc 對象** 是將代碼塊對象化、賦值給變量、傳遞給方法、存儲在數據結構中的核心機制。理解 `Proc`、`lambda`、`proc`、`->`、`&` 操作符的區別與聯繫,是掌握 Ruby 元編程、DSL 設計、回調模式的關鍵。本文系統梳理創建方式、語義差異、參數綁定規則、常見陷阱與最佳實踐。
# 方式 1: Proc.new { |x| x + 1 } (經典)
inc1 = Proc.new { |x| x + 1 }
# 方式 2: lambda { |x| x + 1 } (別名: ->(x) { x + 1 })
inc2 = lambda { |x| x + 1 }
inc2_arrow = ->(x) { x + 1 } # 刺頭語法 (Ruby 1.9+)
# 方式 3: proc { |x| x + 1 } (Kernel#proc, Ruby 1.9+ 等同 lambda! 關鍵差異見下)
inc3 = proc { |x| x + 1 }
# 方式 4: 方法參數 &block 隱式轉換 (最常用於方法定義)
def my_method(&block)
block # 這裡的 block 已經是 Proc 對象
end
my_proc = my_method { |x| x + 1 } # 傳入塊,返回 Proc
| 特性 | Proc.new / proc (Ruby 1.9+) | lambda / -> | 說明 |
|---|---|---|---|
| 參數檢查 | 寬鬆:參數過多忽略,過少賦 nil | 嚴格:參數數量必須完全匹配,否則 ArgumentError | lambda 更接近方法語義 |
| return 語義 | **從定義 Proc 的外層方法返回** (非局部返回) | **從 lambda 自身返回** (局部返回) | **最大陷阱來源** |
| break 語義 | 嘗試跳出定義上下文,若上下文已返回則 LocalJumpError | 僅跳出 lambda 體 | |
| self 綁定 | 定義時的 self (動態作用域) | 調用時的 self (詞法作用域) | |
| arity (參數數) | 負數表示可變參數 (如 -1 表 0 或更多) | 非負整數 (固定) 或負數 (可變,但更精確) | `proc.arity` |
def proc_return_trap
p = Proc.new { return "從 Proc 返回" } # 定義在方法內
p.call # 執行這行時,直接從 proc_return_trap 方法返回!
"這行永遠不會執行"
end
def lambda_return_safe
l = -> { return "從 Lambda 返回" }
l.call # 僅從 lambda 返回
"這行會正常執行"
end
puts proc_return_trap # 輸出: "從 Proc 返回"
puts lambda_return_safe # 輸出: "這行會正常執行" (然後隱式返回 nil)
鐵律:**除非明確需要非局部返回 (極少見),請始終優先使用 `lambda` / `->`**。它們的行為符合直覺:`return` 只跳出塊本身,不影響外層方法流程。
`&` 符號在**方法定義參數列表**和**方法調用參數列表**中含義相反:
def my_method(&block) # & 將隱式塊轉為顯式 Proc
puts block.class # Proc
block.call(10)
end
my_method { |x| puts x * 2 } # 傳入塊
# 輸出: Proc \n 20
my_proc = Proc.new { |x| puts x * 3 }
# 這些等價:
my_method(&my_proc) # & 將 Proc 轉為塊傳給 my_method
my_method { |x| puts x * 3 } # 直接寫塊
def identity(&block)
block # 返回 Proc
end
p = identity { "hello" } # 塊 -> Proc
identity(&p) { "world" } # Proc -> 塊 -> Proc (但塊被忽略)
# 關鍵:&p 只能出現在參數列表**最後** (Ruby 2.7+ 允許前面有關鍵字參數)
def takes_block
yield(1) # 期望塊
end
p = Proc.new { |x| puts x }
# takes_block(p) # 錯誤: wrong number of arguments (given 1, expected 0)
takes_block(&p) # 正確: & 將 Proc 轉為塊
def process(items)
items.each do |item| # each 內部 yield 塊
return if item.nil? # 危險!直接從 process 返回,而非跳過本次迭代
puts item
end
puts "處理完成"
end
process([1, nil, 2]) # 輸出: 1 (然後方法結束,"處理完成" 不打印)
修正:使用 `next` 跳過本次迭代,`break` 跳出整個迭代 (如 `each` 循環)。
建議:代碼中**永遠不要使用 `proc` 關鍵字**。明確使用 `Proc.new` (需要寬鬆/非局部) 或 `lambda` / `->` (標準行為)。
# 錯誤: & 必須在參數列表最後 (Ruby < 2.7)
def bad(a, &block, b) end
# 正確: 最後一個參數
def good(a, b, &block) end
# Ruby 2.7+: 允許關鍵字參數後
def good_kw(a, b, **kw, &block) end
class EventEmitter
def initialize
@callbacks = Hash.new { |h,k| h[k] = [] }
end
def on(event, &callback)
@callbacks[event] << callback # 存儲 Proc
end
def emit(event, *args)
@callbacks[event].each { |cb| cb.call(*args) } # 調用 Proc
end
end
emitter = EventEmitter.new
emitter.on(:click) { |x,y| puts "Clicked at #{x},#{y}" }
emitter.emit(:click, 100, 200)
class Sorter
def initialize(&strategy)
@strategy = strategy # Proc 接收兩個參數返回 -1/0/1
end
def sort(array)
array.sort(&@strategy) # & 將 Proc 轉為塊傳給 sort
end
end
# 使用
asc = Sorter.new { |a,b| a <=> b }
desc = Sorter.new { |a,b| b <=> a }
puts asc.sort([3,1,2]) # [1,2,3]
puts desc.sort([3,1,2]) # [3,2,1]
class QueryBuilder
def initialize
@conditions = []
end
def where(&condition)
@conditions << condition # 存儲 Proc,不立即執行
self
end
def execute(data)
data.select { |row| @conditions.all? { |cond| cond.call(row) } }
end
end
users = [{name: "Alice", age: 25}, {name: "Bob", age: 17}]
qb = QueryBuilder.new.where { |u| u[:age] >= 18 }.where { |u| u[:name].start_with?("A") }
puts qb.execute(users) # [{:name=>"Alice", :age=>25}]
Ruby `Proc` 對象是代碼塊的物質化載體。核心掌握點: