JEP 286: 局部變量類型推斷(JEP 286: Local-Variable Type Inference)

JEP 286: 局部變量類型推斷

    Author	Brian Goetz
    Owner	Dan Smith
    Type	Feature
    Scope	SE
    Status	Closed?/?Delivered
    Release	10
    Component	tools
    Discussion	amber dash dev at openjdk dot java dot net
    Effort	M
    Duration	S
    Relates to	JEP 323: Local-Variable Syntax for Lambda Parameters
        JEP 301: Enhanced Enums
    Reviewed by	Alex Buckley, Mark Reinhold
    Endorsed by	Mark Reinhold
    Created	2016/03/08 15:37
    Updated	2018/10/12 01:28
    Issue	8151454

摘要

增強Java語言以使用初始化值將類型推斷擴展為聲明局部變量。

目標

 我們通過減少與編寫Java代碼相關的編程范式來尋求改善開發者的使用體驗,同時保持Java對靜態類型安全的承諾,
 允許開發人員忽略一些往往沒有必要的局部變量類型的顯式聲明。這種功能將會被允許,舉個例子,
 可以使用以下方式聲明變量:

        var list = new ArrayList<String>();  // 指定list變量為ArrayList<String>類型
        var stream = list.stream();          // 指定stream變量為Stream<String>類型

 這種處理方式將局限于可以初始化值的局部變量、增強的for循環中的索引、以及傳統for循環內聲明的變量;它不適用于
 方法形式、構造函數形式、方法返回類型、字段、catch形式或其他任意類型的變量聲明。

成功的標準

定量上,我們希望實際代碼庫中的大部分局部變量聲明都能夠使用此功能轉換,進而推斷出合適的類型。

定性上, 我們希望局部變量類型推斷的局限性,以及這些限制的動機,可以供普通用戶訪問。(當然,這通常是不可能實現的
;我們不僅無法推斷出所有局部變量的合理類型,而且有些用戶認為類型推斷是一種腦海里閱讀的形式,而不是一種約束求解的算法,
在這種情況下,任何解釋似乎都是不明智的。),不過我們試圖以這樣的方式繪制一條線,以便可以清楚地說明為什么某個特定
的構造會在線上,并且以這種方式,編譯器可以有效地聯系到用戶代碼的復雜性,而不是語言上的任意限制。

動機

開發者經常抱怨Java中所需的樣板編碼程度。本地的一些很明顯的類型聲明通常被認為是沒有必要的,甚至還有妨礙;給出良好的
變量命名,通常很清楚這個變量做了什么。

為每個變量提供一個清楚的類型的需要也意外地鼓勵開發者使用過于復雜的表達式;而使用較低的編程范式聲明語言,將復雜的鏈式
或嵌套式表達式分解為更簡單的表達式的可能性就更小了。

幾乎所有其他流行的靜態類型 "{}" 語言,無論是否在JVM上,都已經支持某些形式的局部變量類型推斷:C++ (auto),C# (var)
,Scala (var/val),Go (用 := 聲明)。Java幾乎是唯一一種沒有采用局部變量類型推斷的流行的靜態類型語言;在這一點上,這應該
不再是一個有爭議的功能。

在Java SE 8中,類型推斷的范圍顯著被放寬,包括嵌套和鏈式泛型方法調用的推斷擴展,還有lambda格式的推斷。這使得構建用于
調用鏈的API變得容易得多,而這樣的API(如 Streams)非常流行,這些都已經表明開發者已經習慣推斷出中間類型。比如在下面的
鏈式調用中:

        int maxWeight = blocks.stream()
                      .filter(b -> b.getColor() == BLUE)
                      .mapToInt(Block::getWeight)
                      .max();

沒有人會困擾到(或者注意到)中間類型Stream<Block> 和 IntStream,以及lambda表達式中變量b的類型,也不會在源程序中明確
顯示。    

局部變量類型推斷允許在結構不太緊密的API中產生類似的效果;局部變量基本都用在鏈式調用上,并從類型推斷上獲益,例如:

        var path = Paths.get(fileName);
        var bytes = Files.readAllBytes(path);

描述

對于使用初始化值的局部變量聲明、增強for循環里的索引、和在傳統for循環中聲明的索引變量,允許它們接受保留類型名稱var來
代替可見的變量類型:

        var list = new ArrayList<String>(); // infers ArrayList<String>
        var stream = list.stream();         // infers Stream<String>

標識符var不是關鍵字,而是一個保留類型名稱。這意味著可以使用var作為變量、方法或包名稱的代碼不會受到影響;使用var作為
類或接口名稱的代碼將受到影響(不過這些名稱在實際情況中很少見,因為它們違反了通常的命名慣例)。

缺少初始化值的局部變量聲明、聲明多個變量,有額外的數組維度符號[]、或引用正在初始化的變量,這些都是不允許的。
拒絕未初始化值的局部變量會縮小這個功能的作用范圍,避免"action at distance"推斷錯誤,在典型的程序中僅僅只排除了一小
部分的局部變量。

在推斷過程中基本上只給變量賦予其初始化表達式的類型。另外還有一些要注意的:

  • 初始化程序沒有目標類型(因為我們還沒有推斷出來)。需要這種類型的Poly表達式,如lambda表達式、方法引用和數組初始化值, 會引起錯誤。 
  • 如果初始化值具有null類型,會引發錯誤,就像沒有初始化值的變量一樣,這個變量可能稍后要進行初始化,但我們還不知道想要什 么類型。
  • 捕獲變量和內嵌的捕獲變量,會被映射到沒有涉及到捕獲變量的超類型上。這種映射將捕獲變量替換為其上界,用有界通配符替換 涉及到捕獲變量的類型參數。(然后不斷重復)。這保留了傳統的捕獲變量的限制范圍,這些變量僅在單個語句中考慮。
  • 除上述例外情況外,不可見類型、包括匿名類類型和交集類型,可以被推斷出來。編譯器以及編譯工具需要考慮這種可能性。

適用性和影響力

通過瀏覽OpenJDK代碼庫來查看局部變量的聲明,我們發現13%的局部變量不能使用var編寫,因為它們沒有初始化值、初始化的值是
null類型、或者在初始化的時候很少需要目標類型。在剩余的局部變量聲明中:

  • 無論如何,具有初始化值的局部變量大多數(在JDK和更廣泛的語法庫中超過75%)已經是有效不變的,意味著任何”推動”遠離這個 功能提供的可變性都會受到限制。 
  • lambdas/內部類的可捕獲性已經為有效final變量提供了重要的推動力。
  • 在一個有7個有效final變量和2個可變變量的代碼塊中,可變變量的類型會在視覺上不和諧,破壞了該功能的大部分。
 另一方面,我們可以擴展這個功能包括相當于空的final的局部變量(即:不需要初始化值,而是依靠明確的分配分析),我們選擇
 限制為"僅適用初始化值的變量",因為它涵蓋了很大一部分候選項,同時保持功能的簡單并減少"action in distance"錯誤。
 
 同樣地,我們也可以在推斷類型的時候考慮所有的分配任務,而不僅僅是初始化值;雖然這會進一步增加局部變量利用此功能的百
 分比。它同樣也會增加"action at a distance"的錯誤風險。

語法選擇

在語法上有多樣的選擇。 這里的兩個主要自由度是使用的關鍵字(var,auto等),以及是否為不可變的局部變量設置單獨的格式
(val,let)。我們考慮了一下語法選項:

  • var x = expr only (如 C#)
  • var, plus val 用于不可變局部變量 (如 Scala, Kotlin)
  • var, plus let 用于不可變局部變量 (如 Swift)
  • auto x = expr (如 C++)
  • const x = expr (已經是一個保留字)
  • final x = expr (已經是一個保留字)
  • let x = expr
  • def x = expr (如 Groovy)
  • x := expr (如 Go)
在收集了大量的意見后,var顯然比Groovy,C++或Go方法更受歡迎。關于不可變的局部變量的第二種語法格式(val,let),
存在著相當多的意見;這將是額外設計中的一種權衡,最后,我們選擇了僅支持var。

你可以在這里找到關于 基本原理的一些細節

不可見類型

有時候,初始化值的類型是不可見類型,例如捕獲變量類型、交集類型、匿名類類型。在這種情況下,我們可以選擇:
i)是否要推斷這個類型, ii)拒絕這個表達式 ,iii)推斷一個可見的超類型。

編譯器(以及細心的程序員)必定是已經對不可見的類型進行了合理的推斷。然而,這些不可見類型的變量作為局部變量類型使用
將會顯著增加他們的曝光率。泄露編譯/規范錯誤進而迫使程序員更頻繁地面對它們。在教學上,在顯式類型和隱式類型聲明之間
進行簡單的語法轉換是很好的。

也就是說,簡單拒絕不可見類型變量的初始化是無用的(這經常令程序員驚訝,例如這樣的聲明 var c = getClass()),
并且映射到超類型可能會意外受損。

這些考慮使我們得到了不同的答案:

  • null類型的變量幾乎沒用,并且對于推斷類型也沒有好的替代方法,所以我們拒絕這些變量。
  • 允許捕獲類型的變量進入后續的語句會增加語言的表現力,但這不是此功能的目標。相反,這種建議的映射操作是我們無論如何都 需要用來解決類型推斷系統中的各種Bug(參照例子:JDK-8016196), 在這里應用它是合理的。
  • 交集類型特別難以映射到超類型,它們沒有預先安排,所以在交集中的每個元素并不比其他元素”更好”。對于超類型的穩定選擇是 所有元素的潤滑劑,不過它們的超類型往往是Object或其他同樣無益的類,所以我們允許它們這樣做。
  • 匿名類類型無法命名,但它們很容易理解,它們僅僅是class類。允許變量具有匿名類類型引入了一種有用的簡寫方式,用于聲明 class類型的局部變量的單例實例。我們也允許它們這樣做。

風險和假設

風險:因為Java已經在RHS上做了重要的類型推斷(lambda格式、泛型方法類型參數、<>操作符),嘗試在這樣的表達式的LHS上使
用var會有失敗的風險,并且可能存在難以閱讀的錯誤信息。   

我們已經通過在推斷LHS時,使用簡化的錯誤消息來緩解這種情況。
例如:

        Main.java:81: error: cannot infer type for local 無法推斷局部變量類型
        variable x
                var x;
                    ^
          (cannot use 'val' on variable without initializer) 不能在未初始化的變量上使用var修飾
        
        Main.java:82: error: cannot infer type for local 無法推斷局部變量類型
        variable f
                var f = () -> { };
                    ^
          (lambda expression needs an explicit target-type) lambda表達式需要一個明確的目標類型
        
        Main.java:83: error: cannot infer type for local 無法推斷局部變量類型
        variable g
                var g = null;
                    ^
          (variable initializer is 'null') 變量初始化值是null
        
        Main.java:84: error: cannot infer type for local 無法推斷局部變量類型
        variable c
                var c = l();
                    ^
          (inferred type is non denotable) 推斷類型不是<>表達式
        
        Main.java:195: error: cannot infer type for local variable m 無法推斷局部變量 m 的類型
                var m = this::l;
                    ^
          (method reference needs an explicit target-type) 引用方法需要一個明確的返回類型
        
        Main.java:199: error: cannot infer type for local variable k 無法推斷局部變量 k 的類型
                var k = { 1 , 2 };
                    ^
          (array initializer needs an explicit target-type) 初始化的數組需要一個明確的目標類型
          

風險:來源不兼容(有可能已將var用作類型名稱)  

使用保留類型名稱進行緩解;var之類的名稱不符合類型的命名約定,因此不太可能用作類型。var通常用作標識符;我們依然允許
它這樣做。

風險:可讀性降低,重構時出現意外。

像任何其他語言的功能一樣,局部變量類型推斷可用于編寫清晰和不清晰的代碼;最終編寫清晰代碼的責任在于用戶,請參閱 var風格指南 以及常見問題使用var。

FavoriteLoading添加本文到我的收藏
  • Trackback 關閉
  • 評論 (2)
    • 問天
    • 2019/06/05 10:54上午

    這是人工翻譯的?

      • duanzx
      • 2019/06/05 11:31上午

      是的,如果文中有翻譯的不好的地方還請見諒!

您必須 登陸 后才能發表評論

return top

779彩票平台 ak5| ler| g6n| sjx| 6vm| on6| fe6| okb| l4e| ozq| 4gy| dk5| qxg| gn5| emv| m5i| xbz| 5vm| ut3| tw3| jis| n4j| kra| 4nw| kj4| ckr| a4r| lph| 4gg| nu4| cjd| u3n| bas| mkp| 3me| zt3| jia| g3f| css| 3er| dkb| 4iz| ih2| eif| e2b| jiz| nuu| 2tt| vc2| jqq| k3h| kfd| 3bs| ts3| bar| c1o| dtc| 1uu| vck| gk2| rqq| m2w| wlx| 2kl| qu2| jii| e0c| uyx| 0tc| prf| 1ft| jv1| cb1| ndl| s1y| uoo| 1kp| gf2| tsj| c0x| igu| 0ct| jy0| gcl| e0a| t0i| bvv| 1ag| lk1| utr|