试写一个Julia Scoping作用域的中文讲解

丢了的玩具-从UndefVarError说起

相信很多人遇到过著名的UndefVarError:变量未定义
9成情况下,肇事者写了这样一个程序,引起了Julia不满

#Scoping
a=1
for i =1:20
    a+=1
end

人家抱怨

UndefVarError: a not defined
in top-level scope at base\none
in top-level scope at Scoping.jl:4 

这是因为在一个for loop(循环)里面,所有变量默认为局部变量。你可以想象成整个程序有一个大盒子,而for-end是大盒子里面的一个小盒子,盒子里面有一些玩具。

在玩玩具的时候,程序想找a,然而他(应该说她或者它,不管了,代指Julia1.0以及以后的版本)默认先从这个for-end的小盒子里找。然而小盒子里并没有a,于是崩溃了。

于是,我们有两种办法:
第一种:在全局变量里找a
global a

a=1
for i = 1:20
    global a+=1
end
a

非常的happy,但是,如果我只有一个玩具,为啥要用两个盒子?
第二种 建立 let block

let
a=1
for i = 1:20
    a+=1
end
a
end

那么我干脆两个盒子都不管,反正我有个大大盒子,在里面找a,总是没错的吧。

Scope的global,local不影响读取,

请看这样一个涉及global,local双层循环

x=1
for i = 1:10
    a=i^2
    println("a="*string(a))
    for j = 1:10
        println(i+j+a)
        println(x)
    end
end

这是可以运行的

被滥用的let-end

于是本性暴露了,所有东西放在一个let-end里,不就完事大吉了吗?
不会,并不会。在Julia语言的设计中,let-end 并不能打穿所有盒子。事实上,众人已经在GitHub上干过架了。
附上连接,不建议围观。。。
https://github.com/JuliaLang/julia/issues/423

我们回到盒子和玩具的问题上来。玩具为什么最好分类放?
为什么呢?这样找的范围小,找的比较快(也许吧),不用担心把所有东西都翻乱了。
如果你在局部有个变量设置成了和全局一样的名字,而它在局部又有和全局不同的用途,那么这种分离是必要的。

对比一下两个loop,上面的是直接在全局操作。

a=1
for i = 1:10
    global a = 1
    global a = a+i
    show(a)
end
a

a=1
for i = 1:10
    local a = 1
    local a = a+i
    show(a)
end
a

这样我本来想输出一组数字,全局的a该干啥去干啥去,然而global不管三七二十一,把a给改了,这在小的地方也许没什么,但在过程很长或很复杂的数值计算里,造成的损失也许是致命的。

Scoping的种类以及控制

全局对应global scope
function对应hard scope
for,while等loop对应soft scope

soft scope 可以用global,local 来加以区分扔出去的东西和循环内部自己的东西
function 可以用module管理(不过如果你的程序不涉及太多函数的话没什么必要),每个module 要用import来加载

函数无global

函数可以有返回值,但通常不能在你看不见的情况下动函数外的东西(Julia不是R!)。
一个好消息是,函数里的循环并不会再加分层

function f(x)
    for i in 1:3
        x=x^2
    end
    return x
end

f(2)

···
256
···

using 和 import的一些区别

这个建议弄module的时候弄不迟。。。

常见错误

1.在多层循环里用global之前不先放一个global变量在global上

for i in 1:10
    for j in 1:10
        global a =i*j
    end
    if i*j>1
        show(1)
    end
end

看着办吧,一般都是因为玩的太浪了出的错

5 个赞

更新,关于函数里面复杂数据结构(例如矩阵)的索引scoping问题

一般来说,函数并不擅自动用它外面的变量,比如:

x = [1 2;3 4]
x

function times2(x::Array)
    x*2
end

times2(x)
x

在这段代码,调用times2()并不改变原来x的值

but…

function times2!(x::Array)
    x[:,:] = x[:,:].*2
    return x
end
times2!(x)
x

这个times2!()函数(使用了索引并赋值给函数中的x的)却会修改x的值,为了避免错误我们把这样的函数以!结尾命名。
修改函数中的变量,或者用 = 的方法复制,并不能改变这一现象,例如:


function times2_2!(x::Array)
    y=x
    y[:,:] = x[:,:]*2
    return x
end

x
times2_2!(x)
x

这对保留原来x的值毫无意义。

一个方法是,做一个新矩阵,并且复制内容。

function times2_2(x::Array)
    y = zeros(size(x))
    y[:,:] = x[:,:]
    y[:,:]*=2
    y
end
x
times2_2(x)
x

这很笨,如果你有什么更好的写法,请和我们分享一下。

2 个赞

用copy()函数来强制复制,可行


function times2_3(x::Array)
    y = copy(x)
    y[:,:] *=2
    return y
end

x
times2_3(x)
x
1 个赞

作者把for循环的变量作用域讲解的非常清晰!点赞!

copy的存在使得编程的便利性增强,Julia的规则真是让人用起来太不顺心了

如果你觉得楼主解决了你的问题,那其实你没注意的只是简单的一点: 全局变量的写入要么是在全局作用域,要么是在当前作用域有global标记。

for循环这些实现成non-scoped一样有一堆人骂。目前这个设计就是最好的,你觉得复杂,原因不外乎变量写入时的作用域规则。

我个人觉得允许变量可变就不好,无端引入复杂度。个人建议把所有的可变变量全部用Ref代替:

a = Ref(value)
a[] #读可变变量
a[] = value #写可变变量