10 年前,我剛剛開始山地自行車運動的時候,我更願意選用零件盡可能少盡可能簡單的自行車。稍後 ,我意識到一些零件(如後減震器)可以保護我的背部和我自行車的框架在德克薩斯州高低起伏的山區中 免受損害。我於是可以騎得更快,出問題的次數也漸少。雖然隨之帶來了操作上的復雜性和維護需求的增 加,但對於我來說這點代價還是值得的。
關於閉包這個問題,Java 愛好者們現在陷入了類似的爭論中。一些人認為閉包帶給編程語言的額外復 雜性並不劃算。他們的論點是:為了閉包帶來的一點點便利而打破原有語法糖的簡潔性非常不值得。其他 一些人則認為閉包將引發新一輪模式設計的潮流。要得到這個問題的最佳答案,您需要跨越邊界,去了解 程序員在其他語言中是如何使用閉包的。
Ruby 中的閉包
閉包是具有閉合作用域 的匿名函數。下面我會詳細解釋每個概念,但最好首先對這些概念進行一些簡 化。閉包可被視作一個遵循特別作用域規則且可以用作參數的代碼塊。我將使用 Ruby 來展示閉包的運行 原理。您如果想和我一起編碼,請下載樣例(參見 下載),下載並安裝 Ruby(參見 參考資料)。用 irb 命令啟動解釋程序,然後用 load filename 命令加載每個樣例。清單 1 是一個最簡單的閉包:
清單 1. 最簡單的閉包
3.times {puts "Inside the times method."}
Results:
Inside the times method.
Inside the times method.
Inside the times method.
times 是作用在對象 3 上的一個方法。它執行三次閉包中的代碼。{puts "Inside the times method."} 是閉包。它是一個匿名函數,times 方法被傳遞到該函數,函數的結果是打印出靜態語句。這 段代碼比實現相同功能的 for 循環(如清單 2 所示)更加緊湊也更加簡單:
清單 2: 不含閉包的循環
for i in 1..3
puts "Inside the times method."
end
Ruby 添加到這個簡單代碼塊的第一個擴展是一個參數列表。方法或函數可通過傳入參數與閉包通信。 在 Ruby 中,使用在 || 字符之間用逗號隔開的參數列表來表示參數,例如 |argument, list|。用這種 方法使用參數,可以很容易地在數據結構(如數組)中構建迭代。清單 3 顯示了在 Ruby 中對數組進行 迭代的一個例子:
清單 3. 使用了集合的閉包
['lions', 'tigers', 'bears'].each {|item| puts item}
Results:
lions
tigers
bears
each 方法用來迭代。您通常想要用執行結果生成一個新的集合。在 Ruby 中,這種方法被稱為 collect。您也許還想在數組的內容裡添加一些任意字符串。清單 4 顯示了這樣的一個例子。這些僅僅是 眾多使用閉包進行迭代的方法中的兩種。
清單 4. 將參數傳給閉包
animals = ['lions', 'tigers', 'bears'].collect {|item| item.upcase}
puts animals.join(" and ") + " oh, my."
LIONS and TIGERS and BEARS oh, my.
在清單 4 中,第一行代碼提取數組中的每個元素,並在此基礎上調用閉包,然後用結果構建一個集合 。第二行代碼將所有元素串接成一個句子,並用 " and " 加以分隔。到目前為止,介紹的還都是語法糖 而已。所有這些均適用於任何語言。
到目前為止看到的例子中,匿名函數都只不過是一個沒有名稱的函數,它被就地求值,基於定義它的 位置來決定它的上下文。但如果含閉包的語言和不含閉包的語言間惟一的區別僅僅是一點語法上的簡便 —— 即不需要聲明函數 —— 那就不會有如此多的爭論了。閉包的好處遠不止是節省幾行代碼,它的使 用模式也遠不止是簡單的迭代。
閉包的第二部分是閉合的作用域,我可以用另一個例子來很好地說明它。給定一組價格,我想要生成 一個含有價格和它相應的稅金的銷售-稅金表。我不想將稅率硬編碼到閉包裡。我寧願在別處設置稅率。 清單 5 是可能的一個實現:
清單 5. 使用閉包構建稅金表
tax = 0.08
prices = [4.45, 6.34, 3.78]
tax_table = prices.collect {|price| {:price => price, :tax => price * tax}}
tax_table.collect {|item| puts "Price: #{item[:price]} Tax: #{item[:tax]}"}
Results:
Price: 4.45 Tax: 0.356
Price: 6.34 Tax: 0.5072
Price: 3.78 Tax: 0.3024
在討論作用域前,我要介紹兩個 Ruby 術語。首先,symbol 是前置有冒號的一個標識符。可抽象地把 symbol 視為名稱。:price 和 :tax 就是兩個 symbol。其次,可以輕易地替換字符串中的變量值。第 6 行代碼的 puts "Price: #{item[:price]} Tax: #{item[:tax]}" 就利用了這項技術。現在,回到作用域 這個問題。
請看清單 5 中第 1 行和第 4 行代碼。第 1 行代碼為 tax 變量賦了一個值。第 4 行代碼使用該變 量來計算價格表的稅金一欄。但此項用法是在一個閉包裡進行的,所以這段代碼實際上是在 collect 方 法的上下文中執行的!現在您已經洞悉了閉包 這個術語。定義代碼塊的環境的名稱空間和使用它的函數 之間的作用域本質上是一個作用域:該作用域是閉合的。這是個基本特征。這個閉合的作用域是將閉包同 調用函數和定義它的代碼聯系起來的紐帶。
用閉包進行定制
您已經知道如何使用現成的閉包。Ruby 讓您也可以編寫使用自己的閉包的方法。這種自由的形式意味 著 Ruby API 的代碼會更加緊湊,因為 Ruby 不需要在代碼中定義每個使用模型。您可以根據需要通過閉 包構建自己的抽象概念。例如,Ruby 的迭代器數量有限,但該語言沒有迭代器也運行得很好,這是因為 可以通過閉包在代碼中構建您自己的迭代概念。
要構建一個使用閉包的函數,只需要使用 yield 關鍵字來調用該閉包。清單 6 是一個例子。 paragraph 函數提供第一句和最後一句輸出。用戶可以用閉包提供額外的輸出。
清單 6. 構建帶有閉包的方法
def paragraph
puts "A good paragraph should have a topic sentence."
yield
puts "This generic paragraph has a topic, body, and conclusion."
end
paragraph {puts "This is the body of the paragraph."}
Results:
A good paragraph should have a topic sentence.
This is the body of the paragraph.
This generic paragraph has a topic, body, and conclusion.
優點
通過將參數列表附加給 yield,很容易利用定制閉包中的參數,如清單 7 中所示。
清單 7. 附加參數列表
def paragraph
topic = "A good paragraph should have a topic sentence, a body, and a conclusion. "
conclusion = "This generic paragraph has all three parts."
puts topic
yield(topic, conclusion)
puts conclusion
end
t = ""
c = ""
paragraph do |topic, conclusion|
puts "This is the body of the paragraph. "
t = topic
c = conclusion
end
puts "The topic sentence was: '#{t}'"
puts "The conclusion was: '#{c}'"
不過,請認真操作以保證得到正確的作用域。在閉包裡聲明的參數的作用域是局部的。例如,清單 7 中的代碼可以運行,但清單 8 中的則不行,原因是 topic 和 conclusion 變量都是局部變量:
清單 8. 錯誤的作用域
def paragraph
topic = "A good paragraph should have a topic sentence."
conclusion = "This generic paragraph has a topic, body, and conclusion."
puts topic
yield(topic, conclusion)
puts conclusion
end
my_topic = ""
my_conclusion = ""
paragraph do |topic, conclusion| # these are local in scope
puts "This is the body of the paragraph. "
my_typic = topic
my_conclusion = conclusion
end
puts "The topic sentence was: '#{t}'"
puts "The conclusion was: '#{c}'"
閉包的應用
下面是一些常用的閉包應用:
重構
定制
遍歷集合
管理資源
實施策略
當您可以用一種簡單便利的方式構建自己的閉包時,您就找到了能帶來更多新可能性的技術。重構能 將可以運行的代碼變成運行得更好的代碼。大多數 Java 程序員都會從裡到外 進行重構。他們常在方法 或循環的上下文中尋找重復。有了閉包,您也可以從外到裡 進行重構。
用閉包進行定制會有一些驚人之處。清單 9 是 Ruby on Rails 中的一個簡短例子,清單中的閉包用 於為一個 HTTP 請求編寫響應代碼。Rails 把一個傳入請求傳遞給控制器,該控制器生成客戶機想要的數 據(從技術角度講,控制器基於客戶機在 HTTP accept 頭上設置的內容來呈現結果)。如果您使用閉包 的話,這個概念很好理解。
清單 9. 用閉包來呈現 HTTP 結果
@person = Person.find(id)
respond_to do |wants|
wants.html { render :action => @show }
wants.xml { render :xml => @person.to_xml }
end
清單 9 中的代碼很容易理解,您一眼就能看出這段代碼是用來做什麼的。如果發出請求的代碼塊是在 請求 HTML,這段代碼會執行第一個閉包;如果發出請求的代碼塊在請求 XML,這段代碼會執行第二個閉 包。您也能很容易地想象出實現的結果。wants 是一個 HTTP 請求包裝程序。該代碼有兩個方法,即 xml 和 html,每個都使用閉包。每個方法可以基於 accept 頭的內容選擇性地調用其閉包,如清單 10 所示 :
清單 10. 請求的實現
def xml
yield if self.accept_header == "text/xml"
end
def html
yield if self.accept_header == "text/html"
end
到目前為止,迭代是閉包在 Ruby 中最常見的用法,但閉包在這方面的用法遠不止使用集合內置的閉 包這一種。想想您每天使用的集合的類型。XML 文檔是元素集。Web 頁面是特殊的 XML 集。數據庫由表 組成,而表又由行組成。文件是字符集或字節集,通常也是多行文本或對象的集合。Ruby 在閉包中很好 地解決了這幾個問題。您已經見過了幾個對集合進行迭代的例子。清單 11 給出了一個對數據庫表進行遍 歷的示例閉包:
清單 11. 對數據庫的行進行遍歷
require 'mysql'
db=Mysql.new("localhost", "root", "password")
db.select_db("database")
result = db.query "select * from words"
result.each {|row| do_something_with_row}
db.close
清單 11 中的代碼也帶出了另一種可能的應用。MySQL API 迫使用戶建立數據庫並使用 close 方法關 閉數據庫。實際上可以使用閉包代替該方法來建立和清除資源。Ruby 開發人員常用這種模式來處理文件 等資源。使用這個 Ruby API,無需打開或關閉文件,也無需管理異常。File 類的方法會為您處理這一切 。您可以使用閉包來替換該方法,如清單 12 所示:
清單 12. 使用閉包操作 File
File.open(name) {|file| process_file(f)}
閉包還有一項重大的優勢:讓實施策略變得容易。例如,若要處理一項事務,采用閉包後,您就能確 保事務代碼總能由適當的函數調用界定。框架代碼能處理策略,而在閉包中提供的用戶代碼能定制此策略 。清單 13 是基本的使用模式:
清單 13. 實施策略
def do_transaction
begin
setup_transaction
yield
commit_transaction
rescue
roll_back_transaction
end
end
Java 語言中的閉包
Java 語言本身還沒有正式支持閉包,但它卻允許模擬閉包。可以使用匿名的內部類來實現閉包。和 Ruby 使用這項技術的原因差不多,Spring 框架也使用這項技術。為保持持久性,Spring 模板允許對結 果集進行迭代,而無需關注異常管理、資源分配或清理等細節,從而為用戶減輕了負擔。清單 14 的例子 取自於 Spring 框架的示例寵物診所應用程序:
清單 14. 使用內部類模擬閉包
JdbcTemplate template = new JdbcTemplate(dataSource);
final List names = new LinkedList();
template.query("SELECT id,name FROM types ORDER BY name",
new RowCallbackHandler() {
public void processRow(ResultSet rs)
throws SQLException
{
names.add(rs.getString(1));
}
});
編寫清單 14 中的代碼的程序員不再需要做如下這些事:
打開聯接
關閉聯接
處理迭代
處理異常
處理數據庫-依賴性問題
程序員們不用再為這些問題煩惱,因為該框架會處理它們。但匿名內部類只是寬泛地近似於閉包,它 們並沒有深入到您需要的程度。請看清單 14 中多余的句子結構。這個例子中的代碼至少一半是支持性代 碼。匿名類就像是滿滿一桶冰水,每次用的時候都會灑到您的腿上。多余句子結構所需的過多的額外處理 阻礙了對匿名類的使用。您遲早會放棄。當語言結構既麻煩又不好用時,人們自然不會用它。缺乏能夠有 效使用匿名內部類的 Java 庫使問題更為明顯。要想使閉包在 Java 語言中實踐並流行起來,它必須要敏 捷干淨。
過去,閉包絕不是 Java 開發人員優先考慮的事情。在早期,Java 設計人員並不支持閉包,因為 Java 用戶對無需顯式完成 new 操作就在堆上自動分配變量心存芥蒂(參見 參考資料)。 如今,圍繞是 否將閉包納入到基本語言中存在極大的爭議。最近幾年來,動態語言(如 Ruby、JavaScript,甚至於 Lisp )的流行讓將閉包納入 Java 語言的支持之聲日益高漲。從目前來看,Java 1.7 最終很可能會采納 閉包。只要不斷跨越邊界,總會好事連連。
本文配套源碼