Jumping with Option

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

后需文章將逐一解開它們的面紗禁荸,敬請期待右蒲!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赶熟,隨后出現(xiàn)的幾起案子瑰妄,更是在濱河造成了極大的恐慌,老刑警劉巖映砖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翰撑,死亡現(xiàn)場離奇詭異,居然都是意外死亡啊央,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門涨醋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瓜饥,“玉大人,你說我怎么就攤上這事浴骂∨彝粒” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵溯警,是天一觀的道長趣苏。 經(jīng)常有香客問我,道長梯轻,這世上最難降的妖魔是什么食磕? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮喳挑,結(jié)果婚禮上彬伦,老公的妹妹穿的比我還像新娘滔悉。我一直安慰自己,他們只是感情好单绑,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布回官。 她就那樣靜靜地躺著,像睡著了一般搂橙。 火紅的嫁衣襯著肌膚如雪歉提。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天区转,我揣著相機(jī)與錄音苔巨,去河邊找鬼。 笑死蜗帜,一個胖子當(dāng)著我的面吹牛恋拷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播厅缺,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼蔬顾,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了湘捎?” 一聲冷哼從身側(cè)響起诀豁,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窥妇,沒想到半個月后舷胜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡活翩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年烹骨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片材泄。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡沮焕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拉宗,到底是詐尸還是另有隱情峦树,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布旦事,位于F島的核電站魁巩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏姐浮。R本人自食惡果不足惜谷遂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望单料。 院中可真熱鬧埋凯,春花似錦点楼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至甩恼,卻和暖如春蟀瞧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背条摸。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工悦污, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人钉蒲。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓切端,卻偏偏與公主長得像,于是被迫代替她去往敵國和親顷啼。 傳聞我的和親對象是個殘疾皇子踏枣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 在大多數(shù)程序語言中,我們都需要與Null打交道钙蒙,并且糾纏于對它的檢查中茵瀑。一不小心讓它給溜出來,就可能像打開潘多拉的...
    _張逸_閱讀 982評論 2 1
  • 這篇講義只講scala的簡單使用躬厌,目的是使各位新來的同事能夠首先看懂程序马昨,因?yàn)?scala 有的語法對于之前使用習(xí)...
    MrRobot閱讀 2,906評論 0 10
  • BAPE從何而來 BAPE,全名“A Bathing Ape in Lukewater”扛施,意思是安逸生活的猿人鸿捧。據(jù)...
    NICER_LAB閱讀 2,763評論 0 3
  • “創(chuàng)業(yè)雖然苦笛谦,但是生活還是要有保障的”, “4000塊的房租而已昌阿,一個月的工資很輕松負(fù)擔(dān)起來的”。 我信了恳邀。然后沒...
    Tt小教父閱讀 131評論 0 0
  • 這節(jié)課是對整期課程的小回顧懦冰,這期課程的大綱也有講到,我們學(xué)到的會是一套理財?shù)幕究蚣芎头椒ā?整理谣沸、規(guī)劃刷钢、預(yù)算、行...
    浩子古小浩閱讀 258評論 1 0