閉包是可以用作函數(shù)參數(shù)和方法參數(shù)的代碼塊。一直以來,這種編程結(jié)構(gòu)都是一些語言(如 Lisp、Smalltalk 和 Haskell)的重要組成部分。盡管一些頗具競爭力的語言(如 C#)采納了閉包,但 Java 社區(qū)至今仍抵制對它的使用。本文探討閉包在為編程語言帶來一點點便利的同時是否也帶來不必要的復雜性、閉包還有無更多的益處。 10 年前,我剛剛開始山地自行車運動的時候,我更愿意選用零件盡可能少盡可能簡單的自行車。稍后,我意識到一些零件(如后減震器)可以保護我的背部和我自行車的框架在德克薩斯州高低起伏的山區(qū)中免受損害。我于是可以騎得更快,出問題的次數(shù)也漸少。雖然隨之帶來了操作上的復雜性和維護需求的增加,但對于我來說這點代價還是值得的。 關(guān)于閉包這個問題,Java 愛好者們現(xiàn)在陷入了類似的爭論中。一些人認為閉包帶給編程語言的額外復雜性并不劃算。他們的論點是:為了閉包帶來的一點點便利而打破原有語法糖的簡潔性非常不值得。其他一些人則認為閉包將引發(fā)新一輪模式設計的潮流。要得到這個問題的最佳答案,您需要跨越邊界,去了解程序員在其他語言中是如何使用閉包的。 Ruby 中的閉包 閉包是具有閉合作用域 的匿名函數(shù)。下面我會詳細解釋每個概念,但最好首先對這些概念進行一些簡化。閉包可被視作一個遵循特別作用域規(guī)則且可以用作參數(shù)的代碼塊。我將使用 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 上的一個方法。它執(zhí)行三次閉包中的代碼。{puts "Inside the times method."} 是閉包。它是一個匿名函數(shù),times 方法被傳遞到該函數(shù),函數(shù)的結(jié)果是打印出靜態(tài)語句。這段代碼比實現(xiàn)相同功能的 for 循環(huán)(如清單 2 所示)更加緊湊也更加簡單:
清單 2: 不含閉包的循環(huán) for i in 1..3 puts "Inside the times method." end | Ruby 添加到這個簡單代碼塊的第一個擴展是一個參數(shù)列表。方法或函數(shù)可通過傳入?yún)?shù)與閉包通信。在 Ruby 中,使用在 || 字符之間用逗號隔開的參數(shù)列表來表示參數(shù),例如 |argument, list| 。用這種方法使用參數(shù),可以很容易地在數(shù)據(jù)結(jié)構(gòu)(如數(shù)組)中構(gòu)建迭代。清單 3 顯示了在 Ruby 中對數(shù)組進行迭代的一個例子:
清單 3. 使用了集合的閉包 [‘lions‘, ‘tigers‘, ‘bears‘].each {|item| puts item} Results: lions tigers bears | each 方法用來迭代。您通常想要用執(zhí)行結(jié)果生成一個新的集合。在 Ruby 中,這種方法被稱為 collect 。您也許還想在數(shù)組的內(nèi)容里添加一些任意字符串。清單 4 顯示了這樣的一個例子。這些僅僅是眾多使用閉包進行迭代的方法中的兩種。
清單 4. 將參數(shù)傳給閉包 animals = [‘lions‘, ‘tigers‘, ‘bears‘].collect {|item| item.upcase} puts animals.join(" and ") + " oh, my." LIONS and TIGERS and BEARS oh, my. | 在清單 4 中,第一行代碼提取數(shù)組中的每個元素,并在此基礎上調(diào)用閉包,然后用結(jié)果構(gòu)建一個集合。第二行代碼將所有元素串接成一個句子,并用 " and " 加以分隔。到目前為止,介紹的還都是語法糖而已。所有這些均適用于任何語言。 到目前為止看到的例子中,匿名函數(shù)都只不過是一個沒有名稱的函數(shù),它被就地求值,基于定義它的位置來決定它的上下文。但如果含閉包的語言和不含閉包的語言間惟一的區(qū)別僅僅是一點語法上的簡便 —— 即不需要聲明函數(shù) —— 那就不會有如此多的爭論了。閉包的好處遠不止是節(jié)省幾行代碼,它的使用模式也遠不止是簡單的迭代。 閉包的第二部分是閉合的作用域,我可以用另一個例子來很好地說明它。給定一組價格,我想要生成一個含有價格和它相應的稅金的銷售-稅金表。我不想將稅率硬編碼到閉包里。我寧愿在別處設置稅率。清單 5 是可能的一個實現(xiàn):
清單 5. 使用閉包構(gòu)建稅金表 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 術(shù)語。首先,symbol 是前置有冒號的一個標識符??沙橄蟮匕?symbol 視為名稱。:price 和 :tax 就是兩個 symbol。其次,可以輕易地替換字符串中的變量值。第 6 行代碼的 puts "Price: #{item[:price]} Tax: #{item[:tax]}" 就利用了這項技術(shù)?,F(xiàn)在,回到作用域這個問題。 請看清單 5 中第 1 行和第 4 行代碼。第 1 行代碼為 tax 變量賦了一個值。第 4 行代碼使用該變量來計算價格表的稅金一欄。但此項用法是在一個閉包里進行的,所以這段代碼實際上是在 collect 方法的上下文中執(zhí)行的!現(xiàn)在您已經(jīng)洞悉了閉包 這個術(shù)語。定義代碼塊的環(huán)境的名稱空間和使用它的函數(shù)之間的作用域本質(zhì)上是一個作用域:該作用域是閉合的。這是個基本特征。這個閉合的作用域是將閉包同調(diào)用函數(shù)和定義它的代碼聯(lián)系起來的紐帶。
用閉包進行定制 您已經(jīng)知道如何使用現(xiàn)成的閉包。Ruby 讓您也可以編寫使用自己的閉包的方法。這種自由的形式意味著 Ruby API 的代碼會更加緊湊,因為 Ruby 不需要在代碼中定義每個使用模型。您可以根據(jù)需要通過閉包構(gòu)建自己的抽象概念。例如,Ruby 的迭代器數(shù)量有限,但該語言沒有迭代器也運行得很好,這是因為可以通過閉包在代碼中構(gòu)建您自己的迭代概念。 要構(gòu)建一個使用閉包的函數(shù),只需要使用 yield 關(guān)鍵字來調(diào)用該閉包。清單 6 是一個例子。paragraph 函數(shù)提供第一句和最后一句輸出。用戶可以用閉包提供額外的輸出。
清單 6. 構(gòu)建帶有閉包的方法 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. | 優(yōu)點 通過將參數(shù)列表附加給 yield ,很容易利用定制閉包中的參數(shù),如清單 7 中所示。
清單 7. 附加參數(shù)列表 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}‘" | 不過,請認真操作以保證得到正確的作用域。在閉包里聲明的參數(shù)的作用域是局部的。例如,清單 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}‘" |
閉包的應用 下面是一些常用的閉包應用: - 重構(gòu)
- 定制
- 遍歷集合
- 管理資源
- 實施策略
當您可以用一種簡單便利的方式構(gòu)建自己的閉包時,您就找到了能帶來更多新可能性的技術(shù)。重構(gòu)能將可以運行的代碼變成運行得更好的代碼。大多數(shù) Java 程序員都會從里到外 進行重構(gòu)。他們常在方法或循環(huán)的上下文中尋找重復。有了閉包,您也可以從外到里 進行重構(gòu)。 用閉包進行定制會有一些驚人之處。清單 9 是 Ruby on Rails 中的一個簡短例子,清單中的閉包用于為一個 HTTP 請求編寫響應代碼。Rails 把一個傳入請求傳遞給控制器,該控制器生成客戶機想要的數(shù)據(jù)(從技術(shù)角度講,控制器基于客戶機在 HTTP accept 頭上設置的內(nèi)容來呈現(xiàn)結(jié)果)。如果您使用閉包的話,這個概念很好理解。
清單 9. 用閉包來呈現(xiàn) HTTP 結(jié)果 @person = Person.find(id) respond_to do |wants| wants.html { render :action => @show } wants.xml { render :xml => @person.to_xml } end | 清單 9 中的代碼很容易理解,您一眼就能看出這段代碼是用來做什么的。如果發(fā)出請求的代碼塊是在請求 HTML,這段代碼會執(zhí)行第一個閉包;如果發(fā)出請求的代碼塊在請求 XML,這段代碼會執(zhí)行第二個閉包。您也能很容易地想象出實現(xiàn)的結(jié)果。wants 是一個 HTTP 請求包裝程序。該代碼有兩個方法,即 xml 和 html ,每個都使用閉包。每個方法可以基于 accept 頭的內(nèi)容選擇性地調(diào)用其閉包,如清單 10 所示:
清單 10. 請求的實現(xiàn) def xml yield if self.accept_header == "text/xml" end def html yield if self.accept_header == "text/html" end | 到目前為止,迭代是閉包在 Ruby 中最常見的用法,但閉包在這方面的用法遠不止使用集合內(nèi)置的閉包這一種。想想您每天使用的集合的類型。XML 文檔是元素集。Web 頁面是特殊的 XML 集。數(shù)據(jù)庫由表組成,而表又由行組成。文件是字符集或字節(jié)集,通常也是多行文本或?qū)ο蟮募稀uby 在閉包中很好地解決了這幾個問題。您已經(jīng)見過了幾個對集合進行迭代的例子。清單 11 給出了一個對數(shù)據(jù)庫表進行遍歷的示例閉包:
清單 11. 對數(shù)據(jù)庫的行進行遍歷 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 迫使用戶建立數(shù)據(jù)庫并使用 close 方法關(guān)閉數(shù)據(jù)庫。實際上可以使用閉包代替該方法來建立和清除資源。Ruby 開發(fā)人員常用這種模式來處理文件等資源。使用這個 Ruby API,無需打開或關(guān)閉文件,也無需管理異常。File 類的方法會為您處理這一切。您可以使用閉包來替換該方法,如清單 12 所示:
清單 12. 使用閉包操作 File File.open(name) {|file| process_file(f)} | 閉包還有一項重大的優(yōu)勢:讓實施策略變得容易。例如,若要處理一項事務,采用閉包后,您就能確保事務代碼總能由適當?shù)暮瘮?shù)調(diào)用界定??蚣艽a能處理策略,而在閉包中提供的用戶代碼能定制此策略。清單 13 是基本的使用模式:
清單 13. 實施策略 def do_transaction begin setup_transaction yield commit_transaction rescue roll_back_transaction end end |
Java 語言中的閉包 Java 語言本身還沒有正式支持閉包,但它卻允許模擬閉包。可以使用匿名的內(nèi)部類來實現(xiàn)閉包。和 Ruby 使用這項技術(shù)的原因差不多,Spring 框架也使用這項技術(shù)。為保持持久性,Spring 模板允許對結(jié)果集進行迭代,而無需關(guān)注異常管理、資源分配或清理等細節(jié),從而為用戶減輕了負擔。清單 14 的例子取自于 Spring 框架的示例寵物診所應用程序:
清單 14. 使用內(nèi)部類模擬閉包 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 中的代碼的程序員不再需要做如下這些事: - 打開聯(lián)接
- 關(guān)閉聯(lián)接
- 處理迭代
- 處理異常
- 處理數(shù)據(jù)庫-依賴性問題
程序員們不用再為這些問題煩惱,因為該框架會處理它們。但匿名內(nèi)部類只是寬泛地近似于閉包,它們并沒有深入到您需要的程度。請看清單 14 中多余的句子結(jié)構(gòu)。這個例子中的代碼至少一半是支持性代碼。匿名類就像是滿滿一桶冰水,每次用的時候都會灑到您的腿上。多余句子結(jié)構(gòu)所需的過多的額外處理阻礙了對匿名類的使用。您遲早會放棄。當語言結(jié)構(gòu)既麻煩又不好用時,人們自然不會用它。缺乏能夠有效使用匿名內(nèi)部類的 Java 庫使問題更為明顯。要想使閉包在 Java 語言中實踐并流行起來,它必須要敏捷干凈。 過去,閉包絕不是 Java 開發(fā)人員優(yōu)先考慮的事情。在早期,Java 設計人員并不支持閉包,因為 Java 用戶對無需顯式完成 new 操作就在堆上自動分配變量心存芥蒂(參見 參考資料)。 如今,圍繞是否將閉包納入到基本語言中存在極大的爭議。最近幾年來,動態(tài)語言(如 Ruby、JavaScript,甚至于 Lisp )的流行讓將閉包納入 Java 語言的支持之聲日益高漲。從目前來看,Java 1.7 最終很可能會采納閉包。只要不斷跨越邊界,總會好事連連。
|