Java模式变量作用域

6

我正在阅读 Oracle 官方文档 Java 17 中模式变量作用域 ,在下面的示例中,testScope1 方法按照文档中所述产生了效果,但是 testScope2 方法会出现编译错误。我无法确定为什么方法的返回类型为 void 会导致此问题。

interface Vehicle{}
class Car implements Vehicle{}

class Pattern{
    public int testScope1(Vehicle v){
        if(!(v instanceof Car c)){
            return 1;
        }
        System.out.println(c.toString());  //WORKS FINE
        return 2; 
    }
    public void testScope2(Vehicle v){
        if(!(v instanceof Car c)){
                 
        }
        System.out.println(c.toString());  //COMPILE TIME ERROR: Cannot resolve symbol c
    }
}

3
如果"instanceof"运算符为真,那么模式变量的作用域就是程序可以到达的地方。 - tgdavies
3
显然,返回类型并不是原因——而是返回语句。 - tgdavies
1
我也对此感到困惑。由于没有进行调查,所以给出的答案都不是很令我满意。无论在运行时的值如何,变量的作用域对我来说都很模糊。由于c是在if表达式中引入的,我希望它的作用域覆盖表达式,可能还包括“then”和“else”代码块,但不包括其他任何内容。这样,我已经惊讶地读到第一种方法是好的。然而,我还读到“模式变量的作用域可以超出引入它的语句”。 - Queeg
4个回答

9

模式变量(在模式中声明的绑定变量)使用流敏捷作用域。与普通局部变量不同,后者仅在连续区域内处于作用域中,而模式变量则在其声明模式明确指定的位置处于作用域中。

如果您有一个if语句:

if (x instanceof Foo(var v)) { 
    A;
}
else {
    B;
}

如果我们到达的是A,那么v就在作用域内,但对于B来说,则不是,因为我们无法保证v在到达B时一定会被赋值。 如果我们使用明显的重构反转测试:

if (!(x instanceof Foo(var v))) { 
    B;
}
else {
    A;
}

同样的道理,vA 的作用域内,但不在 B 的作用域内。规则与局部变量的“明确赋值”规则完全相同——“如果我到达这里,该值是否保证已被赋值。”。
其他条件构造,例如短路运算符 &&||,也参与此作用域。例如,下面的代码是有效的:
if (x instanceof Foo(var v) && v != null) { 
    A;
}

但以下内容不是:
if (x instanceof Foo(var v) || v != null) { 
    A;
}

因为在后者,当我们到达v != null子句时不能保证已经对v赋值。

该规则甚至包含非局部控制流程,例如异常。例如,如果我们有:

if (!(x instanceof Foo(var v)) { 
    System.out.println("Not a Foo");
}
B(v);

这将是一个错误,因为当我们到达 B(v) 时,不能保证 v 已经被赋值。但如果 if 块突然结束:
if (!(x instanceof Foo(var v)) { 
    throw new NotFooException();
}
B(v);

如果在达到B(v)的时候,变量v已被赋值,则可以保证vB(v)处可用。

这看起来可能很复杂,但实际上很简单:根据您对诸如ifthrow等控制流构造的了解,是否保证模式变量在给定点已被分配一个值?如果是这样,那么它就在该点处处于范围之内。


你使用了一个术语 流敏感作用域。你能提供相关的链接、书籍或文章来支持吗?我第一次听到这个词。同时,如果有与 模式变量 相关的文章也会很有帮助。实际上,它与 if-elseinstanceof、变量声明、赋值和普通的 变量作用域 概念没有什么不同。 - Aqeel Ashiq
1
@SyedAqeelAshiq 例如,参见JLS 6.3.1中的“表达式中模式变量的作用域”。这些规则一开始可能看起来很难理解,但它们反映了明确赋值的结构,这是一种基于流的分析。在https://openjdk.org/projects/amber/design-notes/patterns/pattern-match-semantics中有一个更易于理解的解释,其中包括“模式变量的作用域”。(你似乎正在努力说服自己这与你已经理解的某些内容“相同”,但现实更加微妙。) - Brian Goetz
OP的问题是关于为什么他必须放置一个return或者else语句。在这种情况下,了解编译器在幕后所做的工作非常有帮助。如果这是关于设计模式或语言架构等方面的辩论,那么你肯定是正确的,但必须从OP的上下文和困惑中看待它。 - Aqeel Ashiq
1
@SyedAqeelAshiq 第一句话已经回答了这个问题:模式变量在它们被明确赋值的范围内是有效的。因为你可以在模式匹配成功或失败的情况下都能到达对 c 的使用,所以不能保证在第二个 println 语句之前 c 已经被明确赋值。 - Brian Goetz

6

考虑一下如果v不是Car实例会发生什么:

  • testScope1中,return 1;语句导致方法退出。方法的后续语句都不会执行。一切顺利。
  • testScope2中没有return,所以控制流程到达c.toString()。但是v不是Car,那么c是什么?它不能存在,因为它必须是Car类型,但这是一个反事实的情况。这就是为什么你会得到“无法解析符号c”的错误。

1

加上一个else语句。只有当instanceof运算符返回true时,模式变量才会被创建。否则,在作用域中就没有变量,因此会出现编译器错误。

  public void testScope2(Vehicle v){
    if(!(v instanceof Car c)){

    } else {
      System.out.println(c.toString());
    }
  }

编辑: testScope1方法不需要else语句,因为return语句已经存在。如果v不是Car的实例,return语句确保控制永远不会到达c.toString()

模式变量只是在Java 14中添加的一种句法糖。生成字节码时,它与简单的instanceofif-else语句没有区别。例如,我使用Java 17编译了你的Pattern类,并使用Java 8进行反编译。这是testScope1方法的反编译版本。Java 17编译器自己加了一个else

public int testScope1(Vehicle var1) {
  if (var1 instanceof Car) {
    Car var2 = (Car)var1;
    System.out.println(var2.toString());
    return 2;
  } else {
    return 1;
  }
}

因此,您可以看到Java编译器已修改您的if语句,以便仅在vCar实例时才创建Car变量。使用了简单的概念instanceofTypeCastingvariable scopesif-else,这些概念已经存在很长时间。

2
@Steve 因为它不需要:如果由于“return”v不是Car,则使用c的语句永远不会被执行。 - Konrad Rudolph
2
testScope2 needs the else since there is NO return inside the previous if block - testScope1 has a return, which basically works as an else for subsequent statements, so there is no need for an explicit else - user16320675
2
@Steve(在我看来,在testScope1中不加else并不是最佳实践。另外要注意,尽管两种方法的返回声明不同,它们在逻辑上也是不同的——例如,在testScope2中,if后面的println语句总是会被执行) - user16320675
2
@SyedAqeelAshiq 我认为你这样看待它是对自己的不利。按照这个论点,每种编程语言中的所有内容都可以描述为图灵机的“句法水果蛋糕”——但这对于理解事物的工作原理没有任何帮助。模式匹配将许多新概念引入了语言中,其中大部分并非句法性质(例如,新的作用域规则、有条件和无条件的模式、模式组合等)。试图将其视为“纯粹的语法”是反而无益的。 - Brian Goetz
2
@SyedAqeelAshiq,如果您想集中关注原始问题而不是讨论语言设计方面的问题,请不要引入“语法糖”或“语法黑巧克力”等语言设计方面的内容。讨论字节码甚至也没有帮助。仅仅说在这个特定的例子中,源代码与您发布的内容是等价的就足够了。 - Holger
显示剩余9条评论

1
经过更深入的思考,我有了一个解释:只有在代码片段的 instanceof 明确为 true 时,才能访问该变量。
在 testScope1 中,您无法在 if 语句中访问 c。但是,由于 if 语句后面的代码只有在 instanceof 为 true 时才会执行,因此 c 是可访问的。
在 testScope1 中,您无法在 if 语句中访问 c。但是,由于 if 语句后面的代码无论 instanceof 是否为 true 都会执行,因此 c 不可访问。毕竟我们可能会遇到 false 的情况,编译器也会相应地处理。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接