天天看點

《Java8實戰》-第十章筆記(用Optional取代null)用Optional取代null

用Optional取代null

如果你作為Java程式員曾經遭遇過NullPointerException,請舉起手。如果這是你最常遭遇的異常,請繼續舉手。非常可惜,這個時刻,我們無法看到對方,但是我相信很多人的手這個時刻是舉着的。我們還猜想你可能也有這樣的想法:“毫無疑問,我承認,對任何一位Java程式員來說,無論是初出茅廬的新人,還是久經江湖的專家,NullPointerException都是他心中的痛,可是我們又無能為力,因為這就是我們為了使用友善甚至不可避免的像null引用這樣的構造所付出的代價。”這就是程式設計世界裡大家都持有的觀點,然而,這可能并非事實的全部真相,隻是我們根深蒂固的一種偏見。

1965年,英國一位名為Tony Hoare的計算機科學家在設計ALGOL W語言時提出了null引用的想法。ALGOL W是第一批在堆上配置設定記錄的類型語言之一。Hoare選擇null引用這種方式,“隻是因為這種方法實作起來非常容易”。雖然他的設計初衷就是要“通過編譯器的自動檢測機制,確定所有使用引用的地方都是絕對安全的”,他還是決定為null引用開個綠燈,因為他認為這是為“不存在的值”模組化最容易的方式。很多年後,他開始為自己曾經做過這樣的決定而後悔不疊,把它稱為“我價值百萬的重大失誤”。我們已經看到它帶來的後果——程式員對對象的字段進行檢查,判斷它的值是否為期望的格式,最終卻發現我們檢視的并不是一個對象,而是一個空指針,它會立即抛出一個讓人厭煩的NullPointerException異常。

實際上,Hoare的這段話低估了過去五十年來數百萬程式員為修複空引用所耗費的代價。近十年出現的大多數現代程式設計語言,包括Java,都采用了同樣的設計方式,其原因是為了與更老的語言保持相容,或者就像Hoare曾經陳述的那樣,“僅僅是因為這樣實作起來更加容易”。讓我們從一個簡單的例子入手,看看使用null都有什麼樣的問題。

如何為缺失的值模組化

假設你需要處理下面這樣的嵌套對象,這是一個擁有汽車及汽車保險的客戶。

public class Person {
    private Car car;
    public Car getCar() { return car; }
}
public class Car {
    private Insurance insurance;
    public Insurance getInsurance() { return insurance; }
}
public class Insurance {
    private String name;
    public String getName() { return name; }
}           

那麼,下面這段代碼存在怎樣的問題呢?

public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}           

這段代碼看起來相當正常,但是現實生活中很多人沒有車。是以調用getCar方法的結果會怎樣呢?在實踐中,一種比較常見的做法是傳回一個null引用,表示該值的缺失,即使用者沒有車。而接下來,對getInsurance的調用會傳回null引用的insurance,這會導緻運作時出現一個NullPointerException,終止程式的運作。但這還不是全部。如果傳回的person值為null會怎樣?如果getInsurance的傳回值也是null,結果又會怎樣?

采用防禦式檢查減少NullPointerException

怎樣做才能避免這種不期而至的NullPointerException呢?通常,你可以在需要的地方添加null的檢查(過于激進的防禦式檢查甚至會在不太需要的地方添加檢測代碼),并且添加的方式往往各有不同。下面這個例子是我們試圖在方法中避免NullPointerException的第一次嘗試。

public String getCarInsuranceName(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}           

這個方法每次引用一個變量都會做一次null檢查,如果引用鍊上的任何一個周遊的解變量值為null,它就傳回一個值為“Unknown”的字元串。唯一的例外是保險公司的名字,你不需要對它進行檢查,原因很簡單,因為任何一家公司必定有個名字。注意到了嗎,由于你掌握業務領域的知識,避免了最後這個檢查,但這并不會直接反映在你模組化資料的Java類之中。

我們将上面的代碼标記為“深層質疑”,原因是它不斷重複着一種模式:每次你不确定一個變量是否為null時,都需要添加一個進一步嵌套的if塊,也增加了代碼縮進的層數。很明顯,這種方式不具備擴充性,同時還犧牲了代碼的可讀性。面對這種窘境,你也許願意嘗試另一種方案。下面的代碼清單中,我們試圖通過一種不同的方式避免這種問題。

public String getCarInsuranceName(Person person) {
    if (person == null) {
        return "Unknown";
    }
    Car car = person.getCar();
    if (car == null) {
        return "Unknown";
    }
    Insurance insurance = car.getInsurance();
    if (insurance == null) {
        return "Unknown";
    }
    return insurance.getName();
}           

第二種嘗試中,你試圖避免深層遞歸的if語句塊,采用了一種不同的政策:每次你遭遇null變量,都傳回一個字元串常量“Unknown”。然而,這種方案遠非理想,現在這個方法有了四個截然不同的退出點,使得代碼的維護異常艱難。更糟的是,發生null時傳回的預設值,即字元串“Unknown”在三個不同的地方重複出現——出現拼寫錯誤的機率不小!當然,你可能會說,我們可以用把它們抽取到一個常量中的方式避免這種問題。

進一步而言,這種流程是極易出錯的;如果你忘記檢查了那個可能為null的屬性會怎樣?通過這一章的學習,你會了解使用null來表示變量值的缺失是大錯特錯的。你需要更優雅的方式來對缺失的變量值模組化。

null 帶來的種種問題

讓我們一起回顧一下到目前為止進行的讨論,在Java程式開發中使用null會帶來理論和實際操作上的種種問題。

  1. 它是錯誤之源。NullPointerException是目前Java程式開發中最典型的異常。
  2. 它會使你的代碼膨脹。它讓你的代碼充斥着深度嵌套的null檢查,代碼的可讀性糟糕透頂。
  3. 它自身是毫無意義的。null自身沒有任何的語義,尤其是,它代表的是在靜态類型語言中以一種錯誤的方式對缺失變量值的模組化。
  4. 它破壞了Java的哲學。Java一直試圖避免讓程式員意識到指針的存在,唯一的例外是:null指針。
  5. 它在Java的類型系統上開了個口子。null并不屬于任何類型,這意味着它可以被指派給任意引用類型的變量。這會導緻問題,原因是當這個變量被傳遞到系統中的另一個部分後,你将無法獲知這個null變量最初的指派到底是什麼類型。

Optional 類入門

為了更好的解決和避免NPE異常,Java 8中引入了一個新的類java.util.Optional。這是一個封裝Optional值的類。舉例來說,使用新的類意味着,如果你知道一個人可能有也可能沒有車,那麼Person類内部的car變量就不應該聲明為Car,遭遇某人沒有車時把null引用指派給它,而是将其聲明為Optional類型。

變量存在時,Optional類隻是對類簡單封裝。變量不存在時,缺失的值會被模組化成一個“空”的Optional對象,由方法Optional.empty()傳回。Optional.empty()方法是一個靜态工廠方法,它傳回Optional類的特定單一執行個體。你可能還有疑惑,null引用和Optional.empty()有什麼本質的差別嗎?從語義上,你可以把它們當作一回事兒,但是實際中它們之間的差别非常大: 如果你嘗試解引用一個null , 一定會觸發NullPointerException , 不過使用Optional.empty()就完全沒事兒,它是Optional類的一個有效對象,多種場景都能調用,非常有用。關于這一點,接下來的部分會詳細介紹。

使用Optional而不是null的一個非常重要而又實際的語義差別是,第一個例子中,我們在聲明變量時使用的是Optional類型,而不是Car類型,這句聲明非常清楚地表明了這裡發生變量缺失是允許的。與此相反,使用Car這樣的類型,可能将變量指派為null,這意味着你需要獨立面對這些,你隻能依賴你對業務模型的了解,判斷一個null是否屬于該變量的有效範疇。

牢記上面這些原則,你現在可以使用Optional類對最初的代碼進行重構,結果如下。

public class Person {
    private Optional<Car> car;

    public Optional<Car> getCar() {
        return car;
    }
}
public class Insurance {
    private String name;

    public String getName() {
        return name;
    }
}
public class Car {
    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}           

發現Optional是如何豐富你模型的語義了吧。代碼中person引用的是Optional,而car引用的是Optional,這種方式非常清晰地表達了你的模型中一個person可能擁有也可能沒有car的情形,同樣,car可能進行了保險,也可能沒有保險。

與此同時,我們看到insurance公司的名稱被聲明成String類型,而不是Optional,這非常清楚地表明聲明為insurance公司的類型必須提供公司名稱。使用這種方式,一旦解引用insurance公司名稱時發生NullPointerException,你就能非常确定地知道出錯的原因,不再需要為其添加null的檢查,因為null的檢查隻會掩蓋問題,并未真正地修複問題。insurance公司必須有個名字,是以,如果你遇到一個公司沒有名稱,你需要調查你的資料出了什麼問題,而不應該再添加一段代碼,将這個問題隐藏。

在你的代碼中始終如一地使用Optional,能非常清晰地界定出變量值的缺失是結構上的問題,還是你算法上的缺陷,抑或是你資料中的問題。另外,我們還想特别強調,引入Optional類的意圖并非要消除每一個null引用。與此相反,它的目标是幫助你更好地設計出普适的API,讓程式員看到方法簽名,就能了解它是否接受一個Optional的值。這種強制會讓你更積極地将變量從Optional中解包出來,直面缺失的變量值。

應用Optional 的幾種模式

到目前為止,一切都很順利;你已經知道了如何使用Optional類型來聲明你的域模型,也了解了這種方式與直接使用null引用表示變量值的缺失的優劣。但是,我們該如何使用呢?用這種方式能做什麼,或者怎樣使用Optional封裝的值呢?

建立Optional 對象

使用Optional之前,你首先需要學習的是如何建立Optional對象。完成這一任務有多種方法。

  1. 聲明一個空的Optional

正如前文已經提到,你可以通過靜态工廠方法Optional.empty,建立一個空的Optional對象:

Optional<Car> optCar = Optional.empty();           
  1. 依據一個非空值建立Optional

你還可以使用靜态工廠方法Optional.of,依據一個非空值建立一個Optional對象:

Optional<Car> optCar = Optional.of(car);           

如果car是一個null,這段代碼會立即抛出一個NullPointerException,而不是等到你試圖通路car的屬性值時才傳回一個錯誤。

  1. 可接受null的Optional

最後,使用靜态工廠方法Optional.ofNullable,你可以建立一個允許null值的Optional對象:

Optional<Car> optCar = Optional.ofNullable(car);           

如果car是null,那麼得到的Optional對象就是個空對象。

你可能已經猜到,我們還需要繼續研究“如何擷取Optional變量中的值”。尤其是,Optional提供了一個get方法,它能非常精準地完成這項工作,我們在後面會詳細介紹這部分内容。不過get方法在遭遇到空的Optional對象時也會抛出異常,是以不按照約定的方式使用它,又會讓我們再度陷入由null引起的代碼維護的夢魇。是以,我們首先從無需顯式檢查的Optional值的使用入手,這些方法與Stream中的某些操作極其相似。

使用map 從Optional 對象中提取和轉換值

從對象中提取資訊是一種比較常見的模式。比如,你可能想要從insurance公司對象中提取公司的名稱。提取名稱之前,你需要檢查insurance對象是否為null,代碼如下所示:

String name = null;
if(insurance != null){
    name = insurance.getName();
}           

為了支援這種模式,Optional提供了一個map方法。它的工作方式如下:

Optional<Insurance> optionalInsurance = Optional.ofNullable(insurance);
Optional<String> name = optionalInsurance.map(Insurance::getName);           

從概念上,這與我們在第4章和第5章中看到的流的map方法相差無幾。map操作會将提供的函數應用于流的每個元素。你可以把Optional對象看成一種特殊的集合資料,它至多包含一個元素。如果Optional包含一個值,那函數就将該值作為參數傳遞給map,對該值進行轉換。如果Optional為空,就什麼也不做。

這看起來挺有用,但是你怎樣才能應用起來,重構之前的代碼呢?前文的代碼裡用安全的方式連結了多個方法。

public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}           

為了達到這個目的,我們需要求助Optional提供的另一個方法flatMap。

使用flatMap 連結Optional 對象

由于我們剛剛學習了如何使用map,你的第一反應可能是我們可以利用map重寫之前的代碼,如下所示:

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
                    .map(Car::getInsurance)
                    .map(Insurance::getName);           

不幸的是,這段代碼無法通過編譯。為什麼呢?optPerson是Optional類型的變量, 調用map方法應該沒有問題。但getCar傳回的是一個Optional類型的對象,這意味着map操作的結果是一個Optional>類型的對象。是以,它對getInsurance的調用是非法的,因為最外層的optional對象包含了另一個optional對象的值,而它當然不會支援getInsurance方法。

是以,我們該如何解決這個問題呢?讓我們再回顧一下你剛剛在流上使用過的模式:flatMap方法。使用流時,flatMap方法接受一個函數作為參數,這個函數的傳回值是另一個流。這個方法會應用到流中的每一個元素,最終形成一個新的流的流。但是flagMap會用流的内容替換每個新生成的流。換句話說,由方法生成的各個流會被合并或者扁平化為一個單一的流。這裡你希望的結果其實也是類似的,但是你想要的是将兩層的optional合并為一個。

《Java8實戰》-第十章筆記(用Optional取代null)用Optional取代null

這個例子中,傳遞給流的flatMap方法會将每個正方形轉換為另一個流中的兩個三角形。那麼,map操作的結果就包含有三個新的流,每一個流包含兩個三角形,但flatMap方法會将這種兩層的流合并為一個包含六個三角形的單一流。類似地,傳遞給optional的flatMap方法的函數會将原始包含正方形的optional對象轉換為包含三角形的optional對象。如果将該方法傳遞給map方法,結果會是一個Optional對象,而這個Optional對象中包含了三角形;但flatMap方法會将這種兩層的Optional對象轉換為包含三角形的單一Optional對象。

  1. 使用Optional擷取car的保險公司名稱

相信現在你已經對Optional的map和flatMap方法有了一定的了解,讓我們看看如何應用。

public String getCarInsuranceName(Optional<Person> person) {
        return person.flatMap(Person::getCar)
                .flatMap(Car::getInsurance)
                .map(Insurance::getName)
                // 如果Optional的j結果為空值,設定預設值
                .orElse("Unknown");
}           

我們可以看到,處理潛在可能缺失的值時,使用Optional具有明顯的優勢。這一次,你可以用非常容易卻又普适的方法實作之前你期望的效果——不再需要使用那麼多的條件分支,也不會增加代碼的複雜性。

  1. 使用Optional解引用串接的Person/Car/Insurance對象

由Optional對象,我們可以結合使用之前介紹的map和flatMap方法,從Person中解引用出Car,從Car中解引用出Insurance,從Insurance對象中解引用出包含insurance公司名稱的字元串。

《Java8實戰》-第十章筆記(用Optional取代null)用Optional取代null

這裡,我們從以Optional封裝的Person入手,對其調用flatMap(Person::getCar)。如前所述,這種調用邏輯上可以劃分為兩步。第一步,某個Function作為參數,被傳遞給由Optional封裝的Person對象,對其進行轉換。這個場景中,Function的具體表現是一個方法引用,即對Person對象的getCar方法進行調用。由于該方法傳回一個Optional類型的對象,Optional内的Person也被轉換成了這種對象的執行個體,結果就是一個兩層的Optional對象,最終它們會被flagMap操作合并。從純理論的角度而言,你可以将這種合并操作簡單地看成把兩個Optional對象結合在一起,如果其中有一個對象為空,就構成一個空的Optional對象。如果你對一個空的Optional對象調用flatMap,實際情況又會如何呢?結果不會發生任何改變,傳回值也是個空的Optional對象。與此相反,如果Optional封裝了一個Person對象,傳遞給flapMap的Function,就會應用到Person上對其進行處理。這個例子中,由于Function的傳回值已經是一個Optional對象,flapMap方法就直接将其傳回。

第二步與第一步大同小異,它會将Optional轉換為Optional。第三步則會将Optional轉化為Optional對象,由于Insurance.getName()方法的傳回類型為String,這裡就不再需要進行flapMap操作了。

截至目前為止,傳回的Optional可能是兩種情況:如果調用鍊上的任何一個方法傳回一個空的Optional,那麼結果就為空,否則傳回的值就是你期望的保險公司的名稱。那麼,你如何讀出這個值呢?畢竟你最後得到的這個對象還是個Optional,它可能包含保險公司的名稱,也可能為空。我們使用了一個名為orElse的方法,當Optional的值為空時,它會為其設定一個預設值。除此之外,還有很多其他的方法可以為Optional設定預設值,或者解析出Optional代表的值。接下來我們會對此做進一步的探讨。

預設行為及解引用Optional 對象

我們決定采用orElse方法讀取這個變量的值,使用這種方式你還可以定義一個預設值,遭遇空的Optional變量時,預設值會作為該方法的調用傳回值。Optional類提供了多種方法讀取Optional執行個體中的變量值。

  1. get()是這些方法中最簡單但又最不安全的方法。如果變量存在,它直接傳回封裝的變量值,否則就抛出一個NoSuchElementException異常。是以,除非你非常确定Optional變量一定包含值,否則使用這個方法是個相當糟糕的主意。此外,這種方式即便相對于嵌套式的null檢查,也并未展現出多大的改進。
  2. orElse(T other)是我們在代碼使用的方法,正如之前提到的,它允許你在Optional對象不包含值時提供一個預設值。
  3. orElseGet(Supplier<? extends T> other)是orElse方法的延遲調用版,Supplier方法隻有在Optional對象不含值時才執行調用。如果建立預設值是件耗時費力的工作,你應該考慮采用這種方式(借此提升程式的性能),或者你需要非常确定某個方法僅在Optional為空時才進行調用,也可以考慮該方式(這種情況有嚴格的限制條件)。
  4. orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常類似,它們遭遇Optional對象為空時都會抛出一個異常,但是使用orElseThrow你可以定制希望抛出的異常類型。
  5. ifPresent(Consumer<? super T>)讓你能在變量值存在時執行一個作為參數傳入的方法,否則就不進行任何操作。

Optional類和Stream接口的相似之處,遠不止map和flatMap這兩個方法。還有第三個方法filter,它的行為在兩種類型之間也極其相似。

兩個Optional 對象的組合

現在,我們假設你有這樣一個方法,它接受一個Person和一個Car對象,并以此為條件對外部提供的服務進行查詢,通過一些複雜的業務邏輯,試圖找到滿足該組合的最便宜的保險公司:

public Insurance findCheapestInsurance(Person person, Car car) {
    // 不同的保險公司提供的查詢服務
    // 對比所有資料
    return cheapestCompany;
}           

我們還假設你想要該方法的一個null-安全的版本,它接受兩個Optional對象作為參數,傳回值是一個Optional對象,如果傳入的任何一個參數值為空,它的傳回值亦為空。Optional類還提供了一個isPresent方法,如果Optional對象包含值,該方法就傳回true,是以你的第一想法可能是通過下面這種方式實作該方法:

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    if (person.isPresent() && car.isPresent()) {
        return Optional.of(findCheapestInsurance(person.get(), car.get()));
    } else {
        return Optional.empty();
    }
}           

這個方法具有明顯的優勢,我們從它的簽名就能非常清楚地知道無論是person還是car,它的值都有可能為空,出現這種情況時,方法的傳回值也不會包含任何值。不幸的是,該方法的具體實作和你之前曾經實作的null檢查太相似了:方法接受一個Person和一個Car對象作為參數,而二者都有可能為null。利用Optional類提供的特性,有沒有更好或更道地的方式來實作這個方法呢?

Optional類和Stream接口的相似之處遠不止map和flatMap這兩個方法。還有第三個方法filter,它的行為在兩種類型之間也極其相似,我們在接下來的一節會進行介紹。

使用filter 剔除特定的值

你經常需要調用某個對象的方法,檢視它的某些屬性。比如,你可能需要檢查保險公司的名稱是否為“Cambridge-Insurance”。為了以一種安全的方式進行這些操作,你首先需要确定引用指向的Insurance對象是否為null,之後再調用它的getName方法,如下所示:

Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
    System.out.println("ok");
}           

使用Optional對象的filter方法,這段代碼可以重構如下:

Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -> System.out.println("ok"));           

filter方法接受一個謂詞作為參數。如果Optional對象的值存在,并且它符合謂詞的條件,filter方法就傳回其值;否則它就傳回一個空的Optional對象。如果你還記得我們可以将Optional看成最多包含一個元素的Stream對象,這個方法的行為就非常清晰了。如果Optional對象為空,它不做任何操作,反之,它就對Optional對象中包含的值施加謂詞操作。如果該操作的結果為true,它不做任何改變,直接傳回該Optional對象,否則就将該值過濾掉,将Optional的值置空。

下一節中,我們會探讨Optional類剩下的一些特性,并提供更實際的例子,展示多種你能夠應用于代碼中更好地管理缺失值的技巧。

使用Optional 的實戰示例

相信你已經了解,有效地使用Optional類意味着你需要對如何處理潛在缺失值進行全面的反思。這種反思不僅僅限于你曾經寫過的代碼,更重要的可能是,你如何與原生Java API實作共存共赢。

實際上,我們相信如果Optional類能夠在這些API建立之初就存在的話,很多API的設計編寫可能會大有不同。為了保持後向相容性,我們很難對老的Java API進行改動,讓它們也使用Optional,但這并不表示我們什麼也做不了。你可以在自己的代碼中添加一些工具方法,修複或者繞過這些問題,讓你的代碼能享受Optional帶來的威力。我們會通過幾個實際的例子講解如何達到這樣的目的。

用Optional 封裝可能為null 的值

現存Java API幾乎都是通過傳回一個null的方式來表示需要值的缺失,或者由于某些原因計算無法得到該值。比如,如果Map中不含指定的鍵對應的值,它的get方法會傳回一個null。但是,正如我們之前介紹的,大多數情況下,你可能希望這些方法能傳回一個Optional對象。你無法修改這些方法的簽名,但是你很容易用Optional對這些方法的傳回值進行封裝。我們接着用Map做例子,假設你有一個Map方法,通路由key索引的值時,如果map中沒有與key關聯的值,該次調用就會傳回一個null。

Object value = map.get("key");           

使用Optional封裝map的傳回值,你可以對這段代碼進行優化。要達到這個目的有兩種方式:你可以使用笨拙的if-then-else判斷語句,毫無疑問這種方式會增加代碼的複雜度;或者你可以采用我們前文介紹的Optional.ofNullable方法:

Optional<Object> value = Optional.ofNullable(map.get("key"));           

每次你希望安全地對潛在為null的對象進行轉換,将其替換為Optional對象時,都可以考慮使用這種方法。

異常與Optional 的對比

由于某種原因,函數無法傳回某個值,這時除了傳回null,Java API比較常見的替代做法是抛出一個異常。這種情況比較典型的例子是使用靜态方法Integer.parseInt(String),将String轉換為int。在這個例子中,如果String無法解析到對應的整型,該方法就抛出一個NumberFormatException。最後的效果是,發生String無法轉換為int時,代碼發出一個遭遇非法參數的信号,唯一的不同是,這次你需要使用try/catch 語句,而不是使用if條件判斷來控制一個變量的值是否非空。

你也可以用空的Optional對象,對遭遇無法轉換的String時傳回的非法值進行模組化,這時你期望parseInt的傳回值是一個optional。我們無法修改最初的Java方法,但是這無礙我們進行需要的改進,你可以實作一個工具方法,将這部分邏輯封裝于其中,最終傳回一個我們希望的Optional對象,代碼如下所示。

public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}           

我們的建議是,你可以将多個類似的方法封裝到一個工具類中,讓我們稱之為OptionalUtility。通過這種OptionalUtility.stringToInt方法,将String轉換為一個Optional對象,而不再需要記得你在其中封裝了笨拙的try/catch的邏輯了。

代碼

Github:

chap10

Gitee: