Billion-Dollar Mistake
Tony Hoare, null
的發(fā)明者在2009
年公開道歉,并將此錯誤稱為Billion-Dollar Mistake
飞蚓。
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Idioms and Patterns
Preconditions
絕大多數(shù)public
的函數(shù)對于傳遞給它們的參數(shù)都需要進(jìn)行限制摇予。例如叁熔,索引值不能為負(fù)數(shù)耕魄,對象引用不能為空等等铐姚。良好的設(shè)計應(yīng)該保證“發(fā)生錯誤應(yīng)盡快檢測出來”返十。為此妥泉,常常會在函數(shù)入口處進(jìn)行參數(shù)的合法性校驗(yàn)。
為了消除大量參數(shù)前置校驗(yàn)的重復(fù)代碼洞坑,可以提取公共的工具類庫盲链,例如:
public final class Precoditions {
private Precoditions() {
}
public static void checkArgument(boolean exp, String msg = "") {
if (!exp) {
throw new IllegalArgumentException(msg);
}
}
public static <T> T requireNonNull(T obj, String msg = "") {
if (obj == null)
throw new NullPointerException(msg);
return obj;
}
public static boolean isNull(Object obj) {
return obj == null;
}
public static boolean nonNull(Object obj) {
return obj != null;
}
}
使用requireNonNull
等工具函數(shù)時,常常import static
迟杂,使其更具表達(dá)力刽沾。
import static Precoditions.*;
系統(tǒng)中大量存在前置校驗(yàn)的代碼,例如:
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new IllegalArgumentException("must be positive: " + m);
...
}
可以被重構(gòu)得更加整潔排拷、緊湊侧漓,且富有表現(xiàn)力。
public BigInteger mod(BigInteger m) {
checkArgument(m.signum() > 0 , "must be positive: " + m);
...
}
一個常見的誤區(qū)就是:對所有參數(shù)都進(jìn)行限制监氢、約束和檢查布蔗。我將其稱為“缺乏自信”的表現(xiàn),因?yàn)樵谝恍﹫鼍跋旅Σぃ@樣的限制和檢查純屬多余何鸡。
以C++
為例,如果public
接口傳遞了指針牛欢,對該指針做前置校驗(yàn)無可厚非骡男,但僅僅在此做一次校驗(yàn),其在內(nèi)部調(diào)用鏈上的所有private
子函數(shù)傍睹,如果要傳遞此指針隔盛,應(yīng)該將其變更為pass by reference
;特殊地拾稳,如果是只讀吮炕,為了做到編譯時的安全,pass by const-reference
更是明智之舉访得。
可以得到一個推論龙亲,對于private
的函數(shù)陕凹,你對其調(diào)用具有完全的控制,自然保證了其傳遞參數(shù)的有效性鳄炉;如果非得對其private
的參數(shù)進(jìn)行前置校驗(yàn)杜耙,應(yīng)該使用assert
。例如:
private static void <T> sort(T a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
...
}
Avoid Pass/Return Null
private final List<Product> stock = new ArrayList<>();
public Product[] filter(Predicate<Product> pred) {
if (stock.isEmpty()) return null;
...
}
客戶端不得不為此校驗(yàn)返回值拂盯,否則將在運(yùn)行時拋出NullPointerException
異常佑女。
Product[] fakes = repo.filter(Product::isFake);
if (fakes != null && Arrays.asList(fakes).contains(Product.STILTON)) {
...
}
經(jīng)過社區(qū)的實(shí)踐總結(jié)出,返回null
的數(shù)組或列表是不明智的谈竿,而應(yīng)該返回零長度的數(shù)組或列表团驱。
private final List<Product> stock = new ArrayList<>();
private static final Product[] EMPTY = new Product[0];
public Product[] filter(Predicate<Product> pred) {
if (stock.isEmpty()) return EMPTY;
...
}
對于返回值是List
的,則應(yīng)該使用Collections.emptyXXX
的靜態(tài)工廠方法空凸,返回零長度的列表嚎花。
private final List<Product> stock = new ArrayList<>();
public Product[] filter(Predicate<Product> pred) {
if (stock.isEmpty()) return Collections.emptyList();
...
}
Null Object
private final List<Product> stock = new ArrayList<>();
public Product[] filter(Predicate<Product> pred) {
if (stock.isEmpty()) return Collections.emptyList();
...
}
Collections.emptyList()
工廠方法返回的就是一個Null Object
,它的實(shí)現(xiàn)大致是這樣的劫恒。
public final class Collections {
private Collections() {
}
private static class EmptyList<E>
extends AbstractList<E>
implements RandomAccess, Serializable {
private static final long serialVersionUID = 8842843931221139166L;
public Iterator<E> iterator() {
return emptyIterator();
}
public ListIterator<E> listIterator() {
return emptyListIterator();
}
public int size() {return 0;}
public boolean isEmpty() {return true;}
public boolean contains(Object obj) {return false;}
public boolean containsAll(Collection<?> c) { return c.isEmpty(); }
public Object[] toArray() { return new Object[0]; }
public <T> T[] toArray(T[] a) {
if (a.length > 0)
a[0] = null;
return a;
}
public E get(int index) {
throw new IndexOutOfBoundsException("Index: "+index);
}
public boolean equals(Object o) {
return (o instanceof List) && ((List<?>)o).isEmpty();
}
public int hashCode() { return 1; }
private Object readResolve() {
return EMPTY_LIST;
}
}
@SuppressWarnings("rawtypes")
public static final List EMPTY_LIST = new EmptyList<>();
@SuppressWarnings("unchecked")
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
}
Null Object
代表了一種例外贩幻,并且這樣的例外具有特殊性,它是一個有效的對象两嘴,對于用戶來說是透明的,是感覺不出來的族壳。使用Null Object
憔辫,遵循了"按照接口編程"的良好設(shè)計原則,并且讓用戶處理空和非空的情況得到了統(tǒng)一仿荆,使得因缺失null
檢查的錯誤拒之門外贰您。
Monadic Option
Null Object
雖然很優(yōu)雅地使得空與非空得到和諧,但也存在一些難以忍受的情況拢操。
- 接口發(fā)生變化(例如新增加一個方法)锦亦,代表
Null Object
的類也需要跟著變化; -
Null Object
在不同的場景下重復(fù)這一實(shí)現(xiàn)方式令境,其本質(zhì)是一種模式的重復(fù)杠园; - 有時候,引入
Null Object
使得設(shè)計變得更加復(fù)雜舔庶,往往得不償失抛蚁;
Option的引入
問題的本質(zhì)在哪里?null
代表的是一種空惕橙,與其對立的一面便是非空瞧甩。如果將其放置在一個容器中,問題便得到了很完美的解決弥鹦。也就是說肚逸,如果為空,則該容器為空容器;如果不為空朦促,則該值包含在容器之中膝晾。
用Scala
語言表示,可以建立一個Option
的容器思灰。如果存在玷犹,則用Some
表示;否則用None
表示洒疚。
sealed abstract class Option[+A] {
def isEmpty: Boolean
def get: A
}
case class Some[+A](x: A) extends Option[A] {
def isEmpty = false
def get = x
}
case object None extends Option[Nothing] {
def isEmpty = true
def get = throw new NoSuchElementException("None.get")
}
這樣的表示有如下幾個方面的好處:
- 對于存在與不存在的值在類型系統(tǒng)中得以表示歹颓;
- 顯式地表達(dá)了不存在的語義;
- 編譯時保證錯誤的發(fā)生油湖;
問題并沒有那么簡單巍扛,如果如下使用,并沒有發(fā)揮出Option
的威力乏德。
def double(num: Option[Int]) = {
num match {
Some(n) => Some(n*2)
None => None
}
}
將Option
視為容器撤奸,讓其處理Some/None
得到統(tǒng)一性和一致性。
def double(num: Option[Int]) = num.map(_*2)
也可以使用for Comprehension
喊括,在某些場景下將更加簡潔胧瓜、漂亮。
def double(num: Option[Int]) = for (n <- num) yield(n*2)
Option的本質(zhì)
通過上例的可以看出來郑什,Option
本質(zhì)上是一個Monad
府喳,它是一種函數(shù)式的設(shè)計模式。用Java8
簡單地形式化一下蘑拯,可以如下形式化地描述一個Monad
钝满。
interface M<A> {
M<B> flatMap(Function<A, M<B>> f);
default M<B> map(Function<A, B> f) {
return flatMap(a -> unit(f(a)));
}
static M<A> unit(A a) {
...
}
}
同時滿足以下三條規(guī)則:
- 右單位元(identity),既對于任意的
Monad m
申窘,則m.flatMap(unit) <=> m
弯蚜; - 左單位元(unit),既對于任意的
Monad m
剃法,則unit(v).flatMap(f) <=> f(v)
碎捺; - 結(jié)合律,既對于任意的
Monad m
, 則m.flatMap(g).flatMap(h) <=> m.flatMap(x => g(x).flatMap(h))
在這里玄窝,我們將Monad
的數(shù)學(xué)語義簡化牵寺,為了更深刻的了解Monad
的本質(zhì),必須深入理解Cathegory Theory
恩脂,這好比你要吃披薩的烹飪精髓帽氓,得學(xué)習(xí)意大利的文化。但這對于大部分的程序員要求優(yōu)點(diǎn)過高俩块,但不排除部分程序員追求極致黎休。
Option的實(shí)現(xiàn)
Option
的設(shè)計與List
相似浓领,有如下幾個方面需要注意:
-
Option
是一個Immutablity Container
,或者是一個函數(shù)式的數(shù)據(jù)結(jié)構(gòu)势腮; -
sealed
保證其類型系統(tǒng)的封閉性联贩; -
Option[+A]
類型參數(shù)是協(xié)變的,使得None
可以成為任意Option[+A]
的子對象捎拯; - 可以被
for Comprehension
調(diào)用泪幌;
sealed abstract class Option[+A] { self =>
def isEmpty: Boolean
def get: A
final def map[B](f: A => B): Option[B] =
if (isEmpty) None else Some(f(this.get))
final def flatMap[B](f: A => Option[B]): Option[B] =
if (isEmpty) None else f(this.get)
......
}
case class Some[+A](x: A) extends Option[A] {
def isEmpty = false
def get = x
}
case object None extends Option[Nothing] {
def isEmpty = true
def get = throw new NoSuchElementException("None.get")
}
for Comprehension
的本質(zhì)
for Comprehension
其實(shí)是對具有foreach, map, flatMap, withFilter
訪問方法的容器的一個語法糖。
首先署照,pat <- expr
的生成器被解釋為:
// pat <- expr
pat <- expr.withFilter { case pat => true; case _ => false }
如果存在一個生成器和yield
語句祸泪,則解釋為:
// for (pat <- expr1) yield expr2
expr1.map{ case pat => expr2 }
如果存在多個生成器,則解釋為:
// for (pat1 <- expr1; pat2 <- expr2) yield exprN
expr.flatMap { case pat1 => for (pat2 <- expr2) yield exprN }
expr.flatMap { case pat1 => expr2.map { case pat2 => exprN }}
對于for loop
建芙,可解釋為:
// for (pat1 <- expr1; pat2 <- expr2没隘;...) exprN
expr.foreach { case pat1 => for (pat2 <- expr2; ...) yield exprN }
對于包含guard
的生成器,可解釋為:
// pat1 <- expr1 if guard
pat1 <- expr1.withFilter((arg1, arg2, ...) => guard)
Others
- Stream
- Promise
- Either
- Try
- Validation
- Transaction
后需文章將逐一解開它們的面紗禁荸,敬請期待右蒲!