快速排序發明者、圖靈獎得主(Turing Award Winner)Tony Hoare,在QCon London 2009的《Null References: The Billion Dollar Mistake》演講摘要指出,null的使用已經造成無數的錯誤、弱點與系統當機,在過去四十年來,或許造就了價值數十億美元的苦難與損失。
長久以來,不少語言亦採用了null的觀念與類似特性,多數程式庫亦常見null的蹤跡,開發者也常不假思索地加以使用。然而在檢視過去犯下的錯誤同時,著實也累積了不少補救措施與設計,可用以避免null的繼續使用,或者是銜接既存的程式。
含糊的null與引發的問題
不少程式語言中都有null觀念的存在,像是C++的NULL、Java的null、Python的None或Ruby的nil等,JavaScript甚至有兩種令人混淆的undefined與null。
null的根本問題在於語意含糊不清,就字面來說,null可以是「不存在」、「沒有」、「無」或「空」的概念,在應用時,總是令人感到模稜兩可,也就讓開發者有了各自解釋的空間。當開發者想到「嘿!這邊可以沒有東西……」就直接放個null,或者是想到「嗯!沒什麼東西可以傳回……」,就不假思索地傳回個null,然後使用者就總是忘了檢查null,引發各種可能的錯誤。
舉例來說,Java的Map在鍵/值部份都允許是null,而get()方法在指定的鍵不存在時,也是傳回null,這就形成一個很詭異的情況:如果使用Map的get()指定鍵為"SomeKey"時傳回null,那會是指"SomeKey"對應值為null呢?還是指Map中不存在一個鍵為"SomeKey"呢?
同樣是不存在,類似結構也常會有不同表現,例如ServletRequest在取得請求參數值時,會使用getParameter()方法並傳入請求參數名稱,這感覺像是Map結構;如果請求參數「不存在」呢?傳回null還是空字串?請求參數不存在到底是指沒有指定的請求參數名稱,或使用者沒有填值?
檢查值是否為null也是個問題。
例如,當某資料庫查詢方法會傳回字串,如果指定的查詢對象不存在時會傳回null,方法呼叫者必須記得檢查傳回值是否為null,並提供null的替代方案,如果忘了,那麼Java開發者最熟悉的NullPointException就會出現, null的觀念應該是不能作任何操作嗎?Ruby中的nil可以作一些操作,nil.to_a會傳回[],nil.to_s會傳回空字串,如果程式剛好是呼叫這類操作,而又忘了檢查傳回值為nil的情況,程式會很高興地執行下去。
避免使用null或實現速錯概念
想要避免null的問題,基本上就是避免使用null,或是實現速錯(Fail fast)概念。若既有程式庫允許null,也要避免在具有集合概念的資料使用null,像是Set或Map的鍵,如果用來查找的鍵可能出現null,可使用明確的判斷式來處理。如果索引結構的數列會有null元素的可能性,可考慮以Map來取代,將數字索引當作鍵,而非null元素當作值。
速錯就是在問題發生時,快速呈現錯誤,而不是讓程式繼續執行下去。此概念運用在null的情況下,就是方法參數或傳回值為null直接拋出錯誤。舉例而言,有些開發者會檢查方法參數是否為null,若不是null就執行成立區塊,否則就靜悄悄地結束。以速錯概念來實現的話,以Java為例,方法中若有執行param.doSome(),不檢查param是否為null,在null時拋出NullPointerException,方法呼叫者就會知道不該傳入null;在對null操作不引發錯誤的語言中,可以於檢查到參數是null時,主動拋出錯誤。
類似地,檢查null並提供預設值,可考慮是否改以速錯概念實作。例如若securityLevel是從System.getProperty()取得的某屬性字串,為了提供預設值撰寫為securityLevel = (securityLevel == null ? "medium" : securityLevel);假使原先程式設定cc.opehhome.securityLevel為high,並正常運行,日後因人員疏失而誤砍設定,結果系統使用了預設的"medium",錯誤可能在運行若干時日後才會發現。如果改為if(securityLevel == null) throw new NullPointerException("property not found: " + prop),誤砍設定時程式會立即接收到例外,相關人員也就能夠立即處理。guava-libraries中不少API,在參數為null時會拋出例外,群集相關物件查找不到相關元素時也會拋出例外,而不是傳回null。
建立明確的null語意
有些情況下,方法要傳回的值可能存在或不存在,但也不適宜以速錯概念實作,且不想傳回null時怎麼辦?有些程式語言不提供null的概念,可以從中借鏡,例如Haskell在傳回值可能存在或不存在時,提供了Mabye型態,分別以Just t與Nothing來代表值存在與不存在。在具有null的語言中,可以自行實作此類型態,像是Scala的Option也分別具有Some與None兩個實例,guava-libraries或是JDK8也有各自提供的Optional。當方法傳回此類型態時,呼叫者(被迫地)要判斷是否有值,並必須明確從中取出真正的值。
舉例來說,如果某方法原先傳回字串,使用guava-libraries的話,可將傳回型態改為Optional,並在方法中原先傳回null的地方,改為Optional.absent()傳回沒有包裏任何值的Optional,對於確實傳回字串實例的地方,改用Optional.of("...")來包裏該實例。
由於傳回的是Optional型態,等於明確告知方法呼叫者要用isPresent()確認包裏值存在再以get()取出,否則的話,get()會拋出IllegalStateException,這避免了將傳回值直接傳遞給另一個方法,而傳回值可能是null的可能性。如果真的需要預設值,Optional提供了or方法讓語意更為明確,例如firstName + " " + maybeMiddleName.or("") + " " + lastName就讓人一目瞭然,結果也許會是有maybeMiddleName內含的字串或者是空字串。
即使在真正必須使用null的情況下,也可以讓程式擁有明確語意。
例如Optional確實也提供了orNull(),如果在沒有包裹任何值時,確實需要一個null,就可以使用orNull()。Optional也有個靜態方法fromNullable(),用來銜接會傳回null的既有API,如果fromNullable()傳入值不是null,它會將值傳給Optional.of()建立一個Optional實例來包裏它,如果傳入null,則會使用Optional.absent()傳回沒有包裹任何值的Optional實例。
字串型態是個很適合用來說明null缺點的對象,原因在於:所謂沒有字串,到底應該是空字串或是null呢?無論選用何者,開發程式時必須統一,銜接相關程式庫時,也可使用明確的API來彰顯語意;例如guava-libraries提供了emptyToNull()與nullToEmpty()方法,可分別將空字串轉為null,或是將null轉為空字串,它也提供了isNullOrEmpty()方法,在接收到的字串參考為null或空字串實例時傳回true。
對不存在多一份思考
null的意義是含糊不明確的,開發者在將來程式中應該避免使用,或在被使用時拋出錯誤。然而不少語言與程式庫都允許null的存在,對於一些具有null類似語意的情況,必須讓它更明確,這代表著設計者與呼叫者都要多一點思考與更明確的程式碼。像是使用Optional這類型態,強制呼叫者要思考值不存在的情況;既有程式若運用到null,也可運用一些銜接程式庫讓語意清楚,即便允許null也要明確標識,像是guava-libraries在參數確實可接收null,或傳回值可以是null的情況下,必須明確標註@Nullable,而不是留下讓使用者猜測的空間。
專欄作者
熱門新聞
2024-11-25
2024-11-25
2024-11-15
2024-11-15
2024-11-28