V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
assassing
V2EX  ›  程序员

如何理解 Go 语言中作用域延伸?

  •  
  •   assassing ·
    hxz393 · 100 天前 · 816 次点击
    这是一个创建于 100 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 Go 语言中,代码块内声明的标识符,只能在代码块内使用。例如函数体是个代码块,里面定义的变量,只要没发生逃逸,在外部无法访问。 但有一种特殊情况,就是在支持初始化子语句的流程语句中,初始化子语句中定义的变量,作用域会延伸到条件对应的代码块之外。请看代码:

    package main
    
    import "fmt"
    
    func main() {
    	if a := 1; false {
    		fmt.Println(a)
    		//fmt.Println(b)   // 不能引用下面语句定义的块级变量
    	} else if b := 2; a > b { 
    		fmt.Println(a - b) // 可以引用上面语句定义的块级变量
    	} else {
    		fmt.Println(a + b) // 在最后 a 和 b 都可以引用
    	}
    }
    

    左看右看也想不明白,为什么 else if 中定义的变量 b 也能在 else 块中使用?如果有更多条件判断,那么前一个 else if 中定义的变量,在之后所有 else if 块中都能使用。

    在 C++ 中,if 中定义的变量 a,可以跨整个流程语句,else if 中定义的变量明确不能在其他 else 代码块中引用:

    #include <iostream>
    
    int main() {
        if (int a = 1; false) { // a 在整个 if-else 结构中可见
            std::cout << a << std::endl;
        } else if (int b = 2; a > b) {  // a 可访问
            std::cout << a - b << std::endl;
        } else {
            std::cout << a << std::endl;  // a 可访问
            // std::cout << b << std::endl;  // 错误:b 不可访问
        }
        return 0;
    }
    

    在 C# 中,规定更是严格,ifelse if 中定义的变量,只能在随后的代码块中使用,不能跨到代码块:

    public class Program {
        public static void Main() {
            if (int a = 1; false) {
                Console.WriteLine(a);
            } else if (int b = 2; a > b) {  // 错误:a 不可访问
                Console.WriteLine(a - b);
            } else {
                Console.WriteLine(a + b);  // 错误:a 和 b 都不可访问
            }
        }
    }
    

    有本 Go 语言书管这叫「惰性求值」行为,但我也仅仅是知道有这么个现象,不明白为什么会设计成这样?

    6 条回复
    MoYi123
        1
    MoYi123  
       100 天前
    c++的例子有文档吗? 我测试了 clang++ 18.1.6 和 g++ 14.0.1 在 else 里都能访问 b, 和 go 是一样的.
    assassing
        2
    assassing  
    OP
       100 天前
    @MoYi123 你是对的,我使用的在线 IDE ,C++ 版本比较老。换了新的 C++20 可以运行,和 Go 行为一致。
    DOLLOR
        3
    DOLLOR  
       100 天前   ❤️ 1
    确实也能算“惰性求值”吧。

    如果下一条 if 的判断需要依赖上一条 if 的值,这样设计可能会比较方便。
    比如第一个 if 计算值后判断,如果为 false ,则到下一个 if 再利用之前求的值计算再求值,一直下去,直到为 true 的 else if 语句或者 else 。

    如果不这样设计,那就要在进入第一个 if 之前,提前把每个 if 的值都求出来,这样就不够“惰性”了。
    assassing
        4
    assassing  
    OP
       100 天前
    @DOLLOR 突然懒得深究了,我把流程转成这样去理解:
    func main() {
    {
    a := 1;
    if false
    {
    fmt.Println(a)
    }
    b := 2;
    else if a > b
    {
    fmt.Println(a - b)
    }
    else
    {
    fmt.Println(a + b)
    }
    }
    }
    看起来没有破坏块级作用域规则。
    vx7298
        5
    vx7298  
       100 天前
    这个不叫惰性,惰性是表达式不会立即被执行赋值,子表达式声明的,只能在 if else 中用,你把 b:=2 换成 b:=func(),这个 func 会在 else 的时候,立即执行
    assassing
        6
    assassing  
    OP
       100 天前
    @vx7298 确实不算惰性,defer 语句才叫惰性求值。这里是涉及隐藏语句块的边界问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3172 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 13:39 · PVG 21:39 · LAX 05:39 · JFK 08:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.