JDK新提議:新增Record記錄的"with"表示式

banq發表於2022-06-14

甲骨文java語言架構師Brian Goetz提議JDK增加with功能,用來增強Record功能。

記錄Record和內聯類是 Java 中兩種新形式的淺不可變類:
如果我們的Point記錄想要公開一種“set”x和y元件的方法,它必須編寫withX和 withY方法:

record Point(int x, int y) {
    Point withX(int newX) { return new Point(newX, y); }
    Point withY(int newY) { return new Point(x, newY); }
}

這是可行的,並且具有使用我們今天擁有的語言的優勢,但有兩個明顯的缺點。開發人員顯然很高興能夠擺脫通常與狀態相關的樣板,這將形成一個不會自動化的新樣板類別(前進兩步,後退一步)。

當記錄有許多狀態元件時,編寫這些“withers”變得更加乏味且容易出錯。至少對於記錄Record而言,如果語言有足夠的資訊來自動完成這一工作酒更好,要求開發人員手動完成這項工作尤其令人遺憾。

事實上,"withers "會比getters和setters更糟糕,因為雖然一個類可能有O(n)個getters,但可以想象它可能有O(2^n)個withers。更糟的是,隨著元件數量的增加,這些 "withers "訪問器的主體變得更加容易出錯。

這個問題不是記錄或內聯類所獨有的;現有的基於值的類(例如LocalDateTime)也必須公開 wither 方法。但是,如果語言要鼓勵我們編寫不可變類,它也應該可以幫助我們解決這個問題。

向 C# 學習
我們在C#世界裡的朋友已經對這個問題進行了兩次衝擊。他們的第一個解決方案是建立在他們已經允許引數有預設值的基礎上,後來又增加了預設值的引用能力。這意味著你可以寫一個普通的庫方法,用以下方式呼叫。

class Point {
    int x;
    int y;

    Point with(int x = this.x, int y = this.y) {
       return new Point(x, y)。
    }
}

這是一個改進,因為它允許你寫一個方法來處理2^n個可能的組合,作為一個純粹的API考慮,客戶端可以只指定他們想要改變的引數。

p = p.with(x : 3)。


但是,對於C開發者來說,這顯然是不夠的,因為最近(C# 9),他們還在語言中引入了一個with表示式。

p with { x = 3; }

右邊的塊是極其有限的;它是一組屬性賦值。(C最近還引入了 "init-only "屬性(實際上是命名的建構函式引數),所以上述內容會導致一個新的點被例項化,其中寫在塊中的屬性賦值覆蓋了左邊運算元的屬性賦值)。

Java中的with表示式
對他們來說,C的方法是明智的,因為他們可以建立在他們已經擁有的特性上(預設引數、屬性),但僅僅複製這種方法會有很多包袱。在Java中,我們已經有了我們需要(幾乎)以不同的方式,而且可能是更豐富的方式來做的構件:建構函式和解構函式。

重構表示式接收一個靜態型別為T的運算元和一個塊,其中塊表達了對運算元狀態的函式轉換,併產生一個型別為T的新例項。

Point p;
Point pp = p with { x = 3; }


這裡,pp將擁有p的任何y值,而x=3。
這裡程式碼塊可以是一個任意的Java語句序列(賦值、,迴圈、方法呼叫等),但有一些限制。
理想情況下,我們可以為任意的類定義重構,而不僅僅是記錄record,但我們將從記錄開始。一個記錄總是有一個規範的建構函式和解構模式。這意味著我們可以把上面的與表示式解釋為。
  • 宣告一個新的程式碼塊,有新的可變的區域性,其名稱和型別是記錄的組成部分。
  • 用典型的解構器解構目標,並將結果分配給上面描述的變數。
  • 在該範圍內執行與的塊,如果使用了正確的名稱,就可以改變這些locals。
  • 讀取locals的最終值,然後用它們來呼叫規範建構函式。
  • with表示式的值就是結果的例項。

因此,一個表示式,如:

p with { x = 3; }


可以被解釋為這樣的內容:

{ // 新的範圍
    Point(var x, var y) = p; // 用規範的ctor解構LHS
    { x = 3; }                // 在該作用域中執行RHS的
    yield new Point(x, y); // 用新的值來重構
}


我們可以把with表示式的RHS上的塊看作是對記錄狀態的功能轉換。因此,對它施加一些限制是合理的。出於稍後會清楚的原因,我們將禁止向任何變數寫入,除了對應於被提取的元件的locals和在塊內宣告的locals。該塊可以自由地使用語言的任何特性(如迴圈和條件),而不僅僅是賦值--它不是一個 "DSL",它是一個Java程式碼塊,表達了對記錄狀態的轉換。

客戶端可以與表示式一起使用,但類也可能想在其實現中使用它們。比如說:

record Complex(double real, double im) {
    Complex conjugate() { return this with { im = -im; } }
    Complex realOnly() { return this with { im = 0; } }
    Complex imOnly() { return this with { re = 0; } }
}



好處:構建器
考慮到一條記錄有許多 元件的情況,所有這些元件都是可選的。

     record Config(int a,
                   int b,
                   int c,
                   ...
                   int z) {
     }


顯然,沒有人願意用26個值來呼叫規範的建構函式。
標準的解決方法是使用一個構建器,但這是一個很大的 儀式。

 record Config(int a,
                   int b,
                   int c,
                   ...
                   int z) {

         private Config() {
             this(0, 0, 0, ... 0);
         }

         public static Config BUILDER = new Config();
     }




`with`機制給了我們一條出路:

Config c = Config.BUILDER with { c = 3; q = 45; }


 

相關文章