# Julia语言反直觉的循环迭代作用域 ## 体验 趁着`Julia 1.0`发布的东风,今天(2018年08月13日)玩了一把`Julia`。总体感觉像是一个融合了`Python`与`MATLAB`中数值计算相关的便利、支持(但不强求)静态类型、没有`MATLAB`的历史包袱,舍弃了`Python`中为通用程序设计提供的一些特点(从而在做计算时比Python更方便),专注于数值计算和技术分析的语言。从目前的性能体验来看,广告中说的“不需要向量化代码也能达到类似C的速度”并不是吹牛。 缺点是目前1.0版本还是太新。大约半年到一年前的文档/教程们出现了大面积的与1.0不兼容的情况。绘图库虽然有不少,但居然有将近半数编译不出来(内置包管理器下载的是源码,需要编译),让人不得的怀疑这个1.0版本是不是赶工期了。 ## 正题 前面说了,`Julia`语法给人的感觉像是了`Python`与`MATLAB`的融合,但毕竟还是有不同的。很尴尬的一点是,正是由于学习的太自然,遇到问题时反而让人更难跳出以前的经验。比如下面这个循环: y = 30; for i=1:10 println(y + i) end 乍一看几乎看不来是什么语言,运行也很正常。然后,我们经常需要在循环中迭代修改某数的值: y = 30; for i=1:10 y = y + i end 简单朴素,直截了当地……出错了…… ``` ERROR: LoadError: UndefVarError: y not defined Stacktrace: [1] top-level scope at /Users/metorm/dev/Quasi1DSRMCFD/test.jl:3 [inlined] [2] top-level scope at ./none:0 [3] include_string(::Module, ::String, ::String) at ./loading.jl:1002 [4] (::getfield(Atom, Symbol("##120#124")){String,String,Module})() at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:117 [5] withpath(::getfield(Atom, Symbol("##120#124")){String,String,Module}, ::String) at /Users/metorm/.julia/packages/CodeTools/8CjYJ/src/utils.jl:30 [6] withpath at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:46 [inlined] [7] #119 at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:114 [inlined] [8] hideprompt(::getfield(Atom, Symbol("##119#123")){String,String,Module}) at /Users/metorm/.julia/packages/Atom/jJn7Y/src/repl.jl:76 [9] macro expansion at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:113 [inlined] [10] (::getfield(Atom, Symbol("##118#122")){Dict{String,Any}})() at ./task.jl:85 in expression starting at /Users/metorm/dev/Quasi1DSRMCFD/test.jl:2 ``` > 错误信息太复杂,这是目前的又一个缺点 `y`怎么能是未定义呢?前面一个循环运行的好好的,说明循环中按规则是可以访问到`y`的。难道还有只读什么的问题?但是读写问题那也不该报未定义错误。 再测试: y = 30; for i=1:10 print(i) y = y + i end 依然报错,但我注意到了一个细节:报错总是在循环中试图迭代式修改y,且第一次访问`y`的出现在修改`y`之前时候出现。比如上面的代码,第一次`print(i)`是可以执行的,而下面这样写就不会报错: y = 30; for i=1:10 y=i print(i) y = y + i end 翻文档。这么普遍的用法,文档里面居然没有专门说明。不过我还是找到了一句貌似相关的解释: > For 循环及 Comprehensions 有特殊的行为:在其中声明的新变量,都会在每次循环中重新声明。 这样解释起来,那就是我所以为的迭代式修改,并不是在**修改**`y`——按`Julia`的逻辑,`y`只是某个值的一个别名——而使用重新声明了一个名叫`y`的变量,在循环作用域内覆盖掉了外面的`y`的定义。由于变量的作用域是按块来的,这么一声明,造成了整个循环体中的`y`指代的都是这个新变量(恰好与外面的`y`重名而已,实际上完全无关)。这时候我再用那个迭代式赋值的话,就相**当于让一个未定义的变量给自己赋值**,自然会出错。 > 晕了的话,去上面再看看代码。 最后一版程序中,循环中同样新声明了一个`y`,但它的初始化是正常地用`i`进行的,所以不会出错。运行结果也证明,里面的`y`与外面的`y`完全没有关系: y = 30; for i=1:10 y=i print(i) y = y + i end --- 1 2 3 4 5 6 7 8 9 10 30 当然,最后一个写法,仅仅是没出错而已,根本没有达到迭代更新的目的。如果要实现本来预想的流程,可以用`global`语句:声明`global x`使得`x`引入当前作用域和更内层的作用域。 y = 30 for i=1:10 global y; println(y) y = y + i end --- 30 31 33 36 40 45 51 58 66 75 此外可以看出,`Julia`虽然看上去是一个脚本语言,是实际上并不是像`MATLAB`一样老老实实一行一行解析的,而是首先经过了一次从头到尾的预处理——否则运行到第一次读取`y`的时候,怎么就知道后面我又声明了一个`y`呢?