森林蝙蝠
本帖最后由 森林蝙蝠 于 2021-6-6 13:52 编辑

翻译自https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html
本文档旨在探求让Java支持模式匹配的可能方向,只是探讨,并不代表具体计划,无论在什么版本或者什么语言特性;本文档也可能引用正在研究的其他语法,纯粹是为了说明,也不代表任何提供这些语法的计划和承诺。

做模式匹配的动机
几乎每个程序都包括一些逻辑,负责测试一个表达式有没有确定类型或结构,并按条件提取状态分量进行下一步处理,比如所有Java程序员都会熟悉的“instanceof-and-cast”习惯程序:
  1. if (obj instanceof Integer) {
  2.     int intValue = ((Integer) obj).intValue();
  3.     // use intValue
  4. }
复制代码
该程序有三样东西,一个测试(x是不是Integer类型),一个转换(将obj转为Integer类型),一个解构(从Integer类型中,把intValue这个int值提出来),非常直观,易于理解,但几个原因决定了它只是个次优解:代码很没意思,而且类型测试和转换两个都做,本来也没必要(instanceof测试后,你还打算做啥?),此外类型转换和解构的样板代码模糊了接下来更重要的逻辑。但最重要是,无用地重复类名,可能会不经意间把错误带到程序中。
如果想在多个可能的目标类型中做测试,情况会更糟;有时需要一连串if…else,如复读机般测试同一个目标:
  1. String formatted = "unknown";
  2. if (obj instanceof Integer) {
  3.     int i = (Integer) obj;
  4.     formatted = String.format("int %d", i);
  5. }
  6. else if (obj instanceof Byte) {
  7.     byte b = (Byte) obj;
  8.     formatted = String.format("byte %d", b);
  9. }
  10. else if (obj instanceof Long) {
  11.     long l = (Long) obj;
  12.     formatted = String.format("long %d", l);
  13. }
  14. else if (obj instanceof Double) {
  15.     double d = (Double) obj;
  16.     formatted = String.format(“double %f", d);
  17. }
  18. else if (obj instanceof String) {
  19.     String s = (String) obj;
  20.     formatted = String.format("String %s", s);
  21. }
  22. ...
复制代码
以上代码也很眼熟,但是有很多难看的特性,上文提过,每个分支都重复一遍类型转换,又烦又多余,业务逻辑很容易就会被样板代码淹没,这么写同样会带来隐藏错误——因为控制结构过于笼统。以上代码本意是给这堆if……else里每个分支中的formatted变量指定内容,但没有信息可供编译器确定分支情况是否会发生,某些代码块在实际中很少被执行,如果我们忘了把它们的结果分配给formatted变量,bug就发生了(将formatted变量声明成一个blank local或者blank final变量至少可以谋求“显式赋值”分析,但不总有效)。最后,以上代码可优化性更差;若无编译器优化,这条if…else链时间复杂度是O(n),但是真正要解决的问题往往是O(1)复杂度。(译注:blank指声明时未直接赋值的变量)
改善这类问题有很多专门解决方案(ad-hoc),比如“流动类型“(flow typing),obj的类型在经过instanceof Integer测试后,已经被细化了(确定它是与不是Integer类型),所以不需要转型;或者“类型切换”(type switch),即用switch进行判断,case的内容可以是常量,也可以是类型;但是这些解决方案只能说是应急,存在更好的解决方案。

模式
与其专门为“测试与提取分析”这种小问题搞解决方案,不如拥抱模式匹配功能!模式匹配早在上世纪60年代就被应用于各种范式语言当中,包括SNOBOL4和AWK这类面向文本语言,Haskell和ML这类函数式语言,后来还扩展到了Scala这类面向对象语言上,以及最近它进入了C#。
一个“模式”分为两个部分,一个是断言(predicate,原意为‘谓词’),决定了模式是否和目标相匹配,另一个是一组“模式变量”,如果模式与目标匹配,这些变量就会按条件提取出来进行下一步。很多语言都会将输入测试编成instanceof和switch形式,也都可以泛化为“匹配输入的模式”。
“类型模式”(type pattern)便是其中之一,包含一个类名以及变量名以绑定结果,比方说下面的instanceof示例:
  1. if (x instanceof Integer i) {
  2.     // can use i here, of type Integer
  3. }
复制代码
这里x正在与类型模式“Integer i”相匹配,x首先要检查是不是Integer实例,是的话就会转型为Integer,结果指派给i。i这个变量名并非已有变量的复用,而是一个模式变量的声明(和一般变量声明相似,这并非偶然)。
将模式和instanceof结合起来简化了不少乱七八糟的操作,比如equals()方法实现。比方说一个叫Point的类吧,以前equals()方法可能这么实现:
  1. public boolean equals(Object o) {
  2.     if (!(o instanceof Point))
  3.         return false;
  4.     Point other = (Point) o;
  5.     return x == other.x
  6.         && y == other.y;
  7. }
复制代码
有了模式匹配后,就可以把它并成一个表达式,消除重复代码,简化控制流:
  1. public boolean equals(Object o) {
  2.     return (o instanceof Point other)
  3.         && x == other.x
  4.         && y == other.y;
复制代码
同样,类型模式也可以简化上面的if…else链,不必类型转换,减少模板代码:
  1. String formatted = "unknown";
  2. if (obj instanceof Integer i) {
  3.     formatted = String.format("int %d", i);
  4. }
  5. else if (obj instanceof Byte b) {
  6.     formatted = String.format("byte %d", b);
  7. }
  8. else if (obj instanceof Long l) {
  9.     formatted = String.format("long %d", l);
  10. }
  11. else if (obj instanceof Double d) {
  12.     formatted = String.format(“double %f", d);
  13. }
  14. else if (obj instanceof String s) {
  15.     formatted = String.format("String %s", s);
  16. }
  17. ...
复制代码
这也是个大提升,业务逻辑更清楚了,不过我们可以做得更好。

多路条件下的模式
上面的if…else链依旧有些冗余需要排除掉,也是因为有潜在bug,并且读起来也更费力。尤其是,if部分(obj instanceof…)又重复了。我们的目标是“选出最符合目标对象的代码块”,并确保这些分支中必然有一个会被执行。
在编程语言多分支等概率测试中,已经有了成熟的switch机制,但是switch现在功能有限,只能在几个给定类型间切换,比如数字、字符串和枚举,也只能测试常量是否相等。但这些限制基本只是历史的偶然,switch本身其实是模式匹配完美的“匹配”过程。既然instanceof的类型操作符可以泛化到模式,那么switch case标签(label)自然也可以。用带模式的switch表达式,就可以这么写格式化示例:
  1. String formatted =
  2.     switch (obj) {
  3.         case Integer i -> String.format("int %d", i);
  4.         case Byte b    -> String.format("byte %d", b);
  5.         case Long l    -> String.format("long %d", l);
  6.         case Double d  -> String.format("double %f", d);
  7.         case String s  -> String.format("String %s, s);
  8.         default        -> String.format("Object %s", obj);
  9.     };
  10. ...
复制代码
现在由于用了正确的控制结构,代码也更清楚了——表示出了“表达式obj最多匹配一种情况,找出来并计算相应情况”,更加简洁,但更重要的是,也更安全——我们已经利用语法,确保formatted变量总可以被赋值,编译器也可以验证给出来的这些情况足够详尽。此外,这段代码可优化性更好,更可能在O(1)时间内完成这项工作(给formatted赋值)。

常量模式
有种模式你绝不会陌生,那就是switch语句里面现成的常量(constant)case标签。现在呢,case标签只能是数,字串或者枚举类型,在未来,这些都会被归类为常量模式。很显然,将目标匹配到常量模式就一件事:看它等不等于常量。以前,一个常量case标签只能匹配和它相同类型的目标;而在未来,可以用常量模式将类型测试和相等测试连起来,这样就能匹配Object和特定常量。

多态数据操作
在上述示例中,我们持有一个Object,根据它动态变化的类型去做不同的事,可以算作是一种“专门多态”,没有通用父类,也就没有虚方法或者虚分派(virtual dispatch)可让我们区分各种子类型,因此我们只能用动态类型测试解决问题。
我们常常可以把类层次化(下层继承上层),这样类型系统就能更方便地完成此类问题。比如下面这个,把四则运算编成类进行分层:
  1. interface Node { }
  2. class IntNode implements Node {
  3.     int value;
  4. }
  5. class NegNode implements Node {
  6.     Node node;
  7. }
  8. class MulNode implements Node {
  9.     Node left, right;
  10. }
  11. class AddNode implements Node {
  12.     Node left, right;
  13. }
复制代码
这种层次结构,不用说就是拿来做算术的,虚方法可以这么用:
  1. interface Node {
  2.     int eval();
  3. }
  4. class IntNode implements Node {
  5.     int value;   
  6.     int eval() { return value; }
  7. }
  8. class NegNode implements Node {
  9.     Node node;   
  10.     int eval() { return -node.eval(); }
  11. }
  12. class MulNode implements Node {
  13.     Node left, right;   
  14.     int eval() { return left.eval() * right.eval(); }
  15. }
  16. class AddNode implements Node {
  17.     Node left, right;   
  18.     int eval() { return left.eval() + right.eval(); }
  19. }
复制代码
更大的程序可能在一个层次上定义许多操作,有些类似于示例中eval()方法,本质上对层次结构敏感(每个子类都应该实现一遍),所以一般将其编成虚方法实现。但有些操作太专了(例如“此表达式是否包含任何中间节点,使其结果是42”),把它列入层次当中(让子类再实现一遍)就很蠢了,只会污染API。

观察者模式
观察者模式是从操作中分离出层次的标准操作,将数据结构遍历和数据结构本身的定义分离开。比如,如果数据结构是棵树,代表CAD软件中的一个设计,差不多每个操作都要遍历树中的某个部分——保存,打印,搜索元素标签中的文本,计算重量与花费,验证设计规范,等等。我们尽可以将这些操作表示成根类型上的虚方法,但很快就会显得过于臃肿,观察者模式则可以帮我们从数据结构本身代码中,为任何遍历操作解耦出相应的代码,经常是种组织代码的良好方法。
但是观察者模式也有开销,必须要为观察设计一个层次才能用,涉及到给每个节点(node)一个accept(观察者)方法,并定义一个观察者接口:
  1. interface NodeVisitor<T> {
  2.     T visit(IntNode node);
  3.     T visit(NegNode node);
  4.     T visit(MulNode node);
  5.     T visit(AddNode node);
  6. }
复制代码
如果我们想把相等操作定义成一个Node类上的观察者,就要这么做:
  1. class EvalVisitor implements NodeVisitor<Integer> {
  2.     Integer visit(IntNode node) {
  3.         return node.value;
  4.     }
  5.         Integer visit(NegNode node) {
  6.         return -node.accept(this);
  7.     }
  8.         Integer visit(MulNode node) {
  9.         return node.left.accept(this) * node.right.accept(this);
  10.     }
  11.     Integer visit(AddNode node) {
  12.         return node.left.accept(this) + node.right.accept(this);
  13.     }
  14. }
复制代码
如果层次和遍历操作都比较简单,那这也不坏,但我们已经受够了为了准备观察者模式写的样板代码(每个节点类都要一个accept方法,和一个观察者接口),然后每个操作都要写个观察者,最痛苦的是,还要给观察者方法返回的常量装箱(box);随着观察者越发复杂,一个简单的遍历操作就可能有多级观察者,于是观察者模式就变得冗长僵化。
“从层次定义中分离出操作”的理念是正确的,但是结果却不如人意,而且如果这个层次不是给观察者模式设计,或者更差吧,要遍历的元素连公共父类都没有,那就糟了。下一章节中,我们将看到如何从模式匹配中获得观察者模式提供的类型驱动遍历,并摆脱它的冗余、侵入性和限制。

解构(Destruction)模式

包括我们Node类在内的很多类,都只是承载数据结构而已;一般来说,类都是从构造函数和工厂模式中,根据其状态构建出来的,然后用访问器(accessor)方法去访问。如果能访问到传入构造函数中的所有状态组件(state components),就可以认为构造本身是可逆的,而解构就是构造的逆操作。
“解构”模式看上去是倒过来的构造函数;它匹配特定类型的实例对象,然后将状态组件提取出来,比方说我们声明了一个Node对象,语句是“new IntNode(5)”,然后就能以“case IntNode(int n) ->……”的形式解构它(假设IntNode支持解构),以下是用解构模式在Node类上实现eval()方法的做法:
  1. int eval(Node n) {
  2.     return switch(n) {
  3.         case IntNode(int i) -> i;
  4.         case NegNode(Node n) -> -eval(n);
  5.         case AddNode(Node left, Node right) -> eval(left) + eval(right);
  6.         case MulNode(Node left, Node right) -> eval(left) * eval(right);
  7.     };
  8. }
复制代码
解构模式AddNode(Node left, Node right)首先会测试n是不是AddNode,是的话就转成AddNode类型,然后提出left和right子树,放入模式变量以便下一步判断相等。
显然这代码比观察者模式更紧密,也更直观,甚至不需要Node类型支持观察者,也不需要有一个公用父类,只需要Node类型足够透明,能用解构模式给“解”出来就行了。

花絮:模式与数据驱动多态
观察者模式的好处是,稳定层次上的操作,可以和层次本身分开(你啰嗦几次了),不过代价是——基于观察者的代码很大,容易出错,读写麻烦,而模式匹配则完全等效,不需要观察者机械介入,出结果更干净简单透明灵活。以及,模式匹配甚至不需要“稳定层次”,不,任何观察者需要的层次它都不需要。
用模式匹配支持专门多态并不代表继承和虚方法就是错的——这只能说明解决问题有很多方式。正如我们看到的eval()方法一样,有时候把操作包含在层次结构中效果比较理想,但有时候就不行了,甚至不可能做到——比如终端侦听各种消息时,不是什么消息都有公共父类(甚至会来自不同程序库),这时模式匹配就可以提供干净简单的数据驱动多态。
有说法认为很多“设计模式”是语法缺失时的变通办法,虽然这种说法可能很不严谨,但是用在观察者模式上还挺准,如果语言的模式匹配功能足够强,那么观察者模式就几乎完全无用。

组合模式
解构模式威力惊人,上例我们匹配AddNode(Node x, Node y)的时候,Node x和Node y看上去就像是模式变量声明一样,但实际上它们就是模式本身!假设AddNode类有个构造函数,为左右子树获取Node值,以及一个解构器将左右子树表示成Node,那么模式AddNode(P, Q),其中P,Q是模式本身,符合以下条件就会匹配目标:
    目标是个an AddNode;
    AddNode的左节点与P匹配;
    AddNode的右节点与Q匹配;
因为P和Q都是模式,所以它们有自己的变量;如果整个模式都能匹配上,那么子模式中的任何绑定变量都可以匹配,例如此代码“case AddNode(Node left, Node right)) -> ...”the nested patterns
嵌套的模式Node left和Node right就是刚才看到的类型模式(因为类型信息是静态的,所以这里肯定能匹配上),效果是检查目标是不是AddNode,是就立刻把left和right立刻绑定到左右子节点上,听上去有些复杂,但是实际很简单:匹配AddNode和绑定其参数(component)可以一步到位。(译注:原文component在这里是作为构造器参数而存在的,便于理解故如此翻译,下同)
但我们还能再深入些:把解构器模式嵌套进其他模式里面,要么进一步细化匹配内容,要么接下来分解结果。

穷举
在switch表达式中,我们比对了switch的一个分支,同时成为了switch表达式本身的值,也就是说无论什么输入,switch都会执行一个分支——否则switch表达式的值就成未定义了。如果switch有个default分支,那倒没问题,不过对于那些在枚举类型切换的switch,所有枚举常量都会得以处理,再写个期望中永远不会发生的default分支就很恼人了;更坏点,如果写了default条目,编译器就没法验证我们已经穷举了所有情况。
类似地,我们可能在很多类层次上用模式匹配,比如上面的Node类,如果我们确保已经列出了所有子类,那么再写一个永不执行的default也很烦人;在这里就是,可以保证Node的子类只有IntNode, AddNode, MulNode, 和NegNode时,编译器就可以用这些信息,验证switch已经穷举了这些类型。
可以用个老技术:层次密封(hierarchy sealing)。现在假设我们写个“sealed interface Node { }”,把Node类声明成sealed(密封)的,那么Node类就只允许那些和它一起编译出来(co-compile)的类去继承它(注:在隶属于Java15的JEP 360中,可以用permits子句,声明能继承密封类的子类)。
密封是“弱化的终结(finality)”,final类型没有子类,密封类型在指定的子类集合外没有子集,这一细节将会单独讨论。

模式与类型推断
有时我们想要编译器推断一个var声明的局部变量类型,而不用显式指定类型,那么类型模式也应如此。虽然在AddNode示例中,显式声明类型,使用类型模式可能有点用(不过还是会被编译器用静态类型信息优化掉),但也能嵌套一个var(隐式类型)模式,而非类型模式。一个var模式会用类型推断映射到一个等同类型模式(能高效地匹配一切),并将其匹配目标绑定到推断类型的模式变量上。能匹配一切的模式听起来很蠢——实际上也很蠢——但是作为嵌套模式还挺有用。eval()方法可以如下转换:
  1. int eval(Node n) {
  2.     return switch(n) {
  3.         case IntNode(var i) -> i;
  4.         case NegNode(var n) -> -eval(n);
  5.         case AddNode(var left, var right) -> eval(left) + eval(right);
  6.         case MulNode(var left, var right) -> eval(left) * eval(right);
  7.     };
  8. }
复制代码
这个版本与全部显式声明类型的版本等效——随着var可以用在局部变量声明中,编译器完全可以给我们推断出正确的类型。和局部变量一样,选择嵌套类型模式,抑或是选择嵌套var模式,只取决于显式指明类型,是提高还是降低了可读性与可维护性。

嵌套常量模式
常量模式于它自己有用(所有现有的switch语句都用常量,等效于常量模式,前文已经说过),但是作为嵌套模式也行。比如,假设我们想要优化一些eval()方法中的特例,比如“0乘以谁都是0”,这种情况下我们不需要判断其他子树(有一个子树是0就行了)。
如果IntNode(var i)和某个IntNode匹配上了,嵌套模式IntNode(0)匹配到一个持有0的IntNode(0是常量模式),这时首先要测试目标,看它是不是IntNode,是就把IntNode里的数提出来,然后再去匹配,是不是0常量模式。现在可以再深入些:首先匹配MulNode,若其左参数是带有0的IntNode,就可以直接优化掉两个子树的判等:
  1. int eval(Node n) {
  2.     return switch(n) {
  3.         case IntNode(var i) -> i;
  4.         case NegNode(var n) -> -eval(n);
  5.         case AddNode(var left, var right) -> eval(left) + eval(right);
  6.         case MulNode(IntNode(0), var right),
  7.              MulNode(var left, IntNode(0)) -> 0;
  8.         case MulNode(var left, var right) -> eval(left) * eval(right);
  9.     };
  10. }
复制代码
第一个MulNode模式有三层嵌套,都过了才能匹配成功:首先要测试是不是个MulNode,然后测试MulNode左面参数是不是IntNode,再测试这个IntNode的int部分是不是0。如果目标匹配上了这个复杂模式,我们就知道,可以把MulNode直接改成0;否则的话就走下一步,像以前那样匹配MulNode,再递归地判断左右子节点。
把这个和观察者模式放在一起会显得太绕,不好读;即便观察者能够轻松地处理最外层,但是内层的话就需要显式条件逻辑,或者更多观察者。以这种方式(var隐式类型模式)组合模式,我们可以更清楚简单地指定复杂匹配,这样代码可读性更高,错误也更少。

任意模式
var模式能匹配任何东西,并将其目标绑定到模式上,同样_模式也能匹配一切,但它什么都不绑定。_模式作为一个独立模式没太大用处,但如果想说“我不关心模式的组成部分是什么”就很有用了。如果模式的子组件跟匹配无关,就可以把它声明成_模式(也可以阻止意外访问)。举个例子,我们可以把“乘以0”示例用_模式重写一遍:“case MulNode(IntNode(0), _), MulNode(_, IntNode(0)) -> 0;”意思就是说,只要某个数是0,其他部分是什么不重要,也不需要给名字,更不需要被提出来使用。

构造函数以及字面量都可以对应模式
模式貌似是一种聪明的句法技巧,将几个常见的操作连接起来,但实际上还有些更深的东西——模式是我们用以构造,表示,或者其他获取值的操作的对偶。字面量0就是0,但是当用到模式时,它就匹配数字0;表达式new Point(1,2)从特定的(x,y)序对构造了一个Point对象,那么模式Point(x,y)就匹配到了所有Point,并且可以提出相应的(x,y)值。构造或者获取一个值,不管是构造函数,还是静态工厂方法,等等所有方式都有相应模式,将这个值再分解为相应的组成部分。构造和解构之间高度的句法相似性并非偶然。

静态模式
解构模式是由类似于构造函数的类成员实现的,但是运行与构造相反,先拿出一个实例对象,然后将其分解为一群组分。类可以有静态工厂和构造函数,那么静态模式也能有。而且,既然静态工厂能被创建对象取代,那么静态模式也可以等效于类型解构模式,而不暴露其构造函数。例如,Optional对象是由Optional.of(v)和Optional.empty()这两个工厂方法构造出来的,可以相应地暴露出Optional静态模式,在Optional对象值上操作,并且取出相关状态:
  1. switch (opt) {
  2.     case Optional.empty(): ...
  3.     case Optional.of(var v): ...
  4. }
复制代码
刚才说过,对象如何被构造和如何被解构,其句法相似性并非偶然。一个显而易见的问题:实例模式有没有意义?有,而且它给API设计者提供了比我们现有的解决方案更好的选择。静态和实例模式将在单独文档中进行更深入的讨论。

模式绑定语句
我们已经看到了两个能扩展来支持模式的例子:instanceof和switch,而另一个有用的,对模式敏感的控制结构是模式绑定语句(pattern binding statement),用模式将目标解构,比方说:
  1. record Point(int x, int y);
  2. record Rect(Point p0, Point p1);
复制代码
有一个叫Rect的记录(record),需要解构成它绑定的Point对象。一个无条件解构看上去可能像如下代码(使用熟悉的“双下划线”句法规则,说明这个句法只是个占位符,以便说明):
  1. Rect r = ...
  2. __let Rect(var p0, var p1) = r;
  3. // use p0, p1
复制代码
然后,我们断言(编译器会检查)模式会匹配成功,这样就能解构目标,将其参数绑定到新变量上。如果模式只能部分匹配目标(操作数),那就保证不了能匹配上,只能再写一条:
  1. Object r = ...
  2. __let Rect(var p0, var p1) = r
  3. else throw new IllegalArgumentException("not a Rect");
  4. // use p0, p1
复制代码
甚至能用一个嵌套模式,将直角坐标(Point)一步提出:
  1. Rect r = ...
  2. __let Rect(Point(var x0, var y0), Point(var x1, var y1)) = r;
  3. // use x0, x1, y0, y1
复制代码
如switch一样,如果尝试解构一个null变量还不写else来处理这种情况,那么let语句可能在运行时抛出NullPointerException异常。

模式与控制流结构总结
现在已经列举了好几种模式:
    常量模式,测试目标等不等于常量;
    类型模式,执行instanceof测试,转换目标类型,将其绑定到模式变量上;
    解构模式,执行instanceof测试,转换目标类型,解构目标,然后将其组件递归地和子模式匹配;
    方法模式,比解构模式更普遍;
    var模式,可以匹配一切,并绑定上目标;
    _模式,匹配一切但啥也不绑定。
以及几个能用模式的地方:
    一个switch语句或者表达式;
    一个instanceof谓词;
    一个__let或__bind语句。
其他可能的模式,比如集合(collection)模式,稍后也会加入,类似的,其他语言结构,比如catch,以后也可能支持模式匹配。

模式匹配与记录
模式匹配和另一个在开发的功能,记录(records,数据类)有很好的联动。数据类顾名思义,就是数据的透明载体而已,作为回报,数据类隐式支持解构模式(其他类该有的东西它也有,比如构造函数,访问器,equals(),hashCode(),等等)。可以把Node层次定义成记录,更加紧凑:
  1. sealed interface Node { }
  2. record IntNode(int value) implements Node;
  3. record NegNode(Node node) implements Node;
  4. record SumNode(Node left, Node right) implements Node;
  5. record MulNode(Node left, Node right) implements Node;
  6. record ParenNode(Node node) implements Node;
复制代码
现在已知Node的子类就这些,所以例子中的switch表达式可以被进行穷举分析,不需要default分支(聪明的读者可能已经注意到,我们得到了一个广为人知的构造,代数数据类型algebraic data types;在做乘类型时,记录给我们提供了一个紧凑的表达式,而密封提供了另一半代数运算,加类型)。
(译注:代数数据类型最基本的运算就是对类型做加法和乘法,https://www.zhihu.com/question/24460419/answer/86158686

作用域
对模式敏感的(Pattern-aware)结构,比如instanceof有个新特性:它们可能从表达式中间引入变量。不过有个显明的问题:这些模式变量的作用域是哪里?看几个有意思的例子吧(细节在别的文档里再说):
  1. if (x instanceof String s) {
  2.     System.out.println(s);
  3. }
复制代码
模式变量在这里用在了if语句判定中,有意义;当执行程序体(println一句),模式必须匹配上,所以s是良定义的,应该包括在这个if程序体作用域里的变量集合中。可以再扩大些:
  1. if (x instanceof String s && s.length() > 0) {
  2.     System.out.println(s);
  3. }
复制代码
这么做也有意义:因为&&是短路(short-circuiting)的,那么不管什么时候执行第二个条件(s.length>0),匹配都会成功,所以s还是良定义的,应该包括在条件表达式的第二个子表达式作用域内,换言之,如果我们将AND换成OR,写成“if (x instanceof String s || s.length() > 0)”,那就会得到一个错误;s在这里不是良定义的,因为匹配在第二个子表达式中可能失败。与之类似,s在下面的else语句中也不是良定义:
  1. if (x instanceof String s) {
  2.     System.out.println(s + "is a string");
  3.     // OK to use s here
  4. }
  5. else {
  6.     // error to use s here
  7. }
复制代码
不过,如果条件判断反过来了:
  1. if (!(x instanceof String s)) {
  2.     // error to use x here
  3. }
  4. else {
  5.     System.out.println(s + "is a string");
  6.     // OK to use s here
  7. }
复制代码
在这我们想让s进入else分支的作用域中(如果没进去,我们就不能通过反转条件判断和交换分支的方式,自由重构if-else代码块。)
本质上我们需要一个作用域结构,模拟语言的显式赋值规则;需要已经被赋值的模式变量进入作用域中,没赋值的就出去。这种机制允许复用模式变量名,而不是为每个模式指派个新名字,如下:
  1. switch (shape) {
  2.     case Square(Point corner, int length): ...
  3.     case Rectangle(Point rectCorner, int rectLength, int rectHeight): ...
  4.     case Circle(Point center, int radius): ...
  5. }
复制代码
如果模式变量的作用域与局部变量相似,那我们就很倒霉,不得不如上述代码般,针对每种case给模式起名,而不是如理想中能复用length这样的名字;将作用域匹配到显式赋值让我们可以这样(复用),并且符合开发者的期望——什么时候能用模式变量,什么时候不能。来自群组: Nuclear Fusion

豆沙2333
讲得好详细啊 爱辣

3TUSK
JEP 394:Pattern Matching for instanceof https://openjdk.java.net/jeps/394 已于 Java 16 正式发布
JEP 406:Pattern Matching for switch (Preview) https://openjdk.java.net/jeps/406 预定 Java 17 正式发布

下一位。

下一页 最后一页