sas如何检验两个变量间有相互作用(Macro-宏变量的作用域)(1)

所谓宏变量的作用域(Scope),通俗地讲就是宏变量可见性(Visibility)和可访问性(accessibility).定义或者申明了一个宏变量之后,在什么样的环境下可以访问这个宏变量(的值)呢?

1. 从简单的例子谈起

我们用漫谈SAS Macro (1)中的示例macro来说明什么是scope。先看version 1:

%macro getNobsOf(data); /* version 1 */

在调用macro getNobsOf之前,我们事先用%GLOBAL语句声明了一个全局宏变量,并且在调用宏之后利用%put语句打印该宏变量的值(结果是19)。通过这个例子可以知道,在一个宏的内部可以访问外部的宏变量,并且可以更新外部宏变量的值。

如果在调用之前不事先声明全局宏变量nobs(并且当前环境中亦不存在同名的全局宏变量),那么在执行%put语句的时候,会产生warning:

WARNING: Apparent symbolic reference NOBS not resolved.

这说明,在宏的外部无法访问宏内部的同名宏变量。这就是所谓的作用范围(Scope)的意思。

SAS宏变量作用范围只有有两种,一种是全局的(global),另一种是局部的(local)。全局的宏变量一旦声明或者定义之后,可以在当前SAS Session的任何地方,不管是open code还是macro当中访问,而局部变量则只能在特定的范围内访问。

1) 任何在open code当中定义的宏变量一律都是全局的,不管定义的方式是通过%let语句,还是通过call symput (DATA步), 抑或select语句的into从句(into clause of select statement, SQL).

2) 任何在Macro内部定义的宏变量默认都是局部的,不过使用上述何种方式定义。局部宏变量随着macro运行的结束就被释放了,无法在macro以外的地方直接访问。注意这是默认行为,和1)不同的是,我们可以在宏内部使用%global语句声明宏变量为全局的。(顺便说一句,如果你要做一个和上面的version 1类似的,通过宏变量和外部交换值,那么可以考虑将该变量在宏的定义中声明为全局的,这样调用前就不用担心因为作用域带来的访问问题了)。

2. %local语句是干什么的?

version 2的定义当中出现了%local语句:

%macro getNobsOf(data); /* version 2 */

先说结论,%local语句的作用是显式地声明宏变量的作用范围是局部的。但是既然1) open code当中宏变量都是全局的,2) macro当中的宏变量默认都是局部的,那么%local语句岂不是多余的?我们把version 2简化成下面的例子:

%let nobs = 19;

打印的结果是19,而我们都知道如果macro定义中去掉%local nobs,那么打印结果就是20(参见 version 1)。之所以产生这样的结果是因为%local实际上在macro内部创建了一个局部宏变量nobs,macro内部访问以及更新操作都是对这个局部宏变量而言的。实际上,macro在执行当中遇到宏变量需要解析的时候,会遵循就近原则,优先解析该作用域内的宏变量,如果该作用域内不存在该宏变量,则向上递归查找该宏变量,直至最外层,如果找不到则在当前作用域内创建该宏变量。

可以利用SASHELP这个library当中的view - VMACRO来验证:

%let nobs = 19;

没有%local语句的时候,打印的结果只有一行,宏变量nobs的作用范围是GLOBAL,值是20.

%let nobs = 19;

可以看到加上%local语句的之后,在macro运行时,实际上存在着两个同名的宏变量,一个的作用范围是GLOBAL,一个的作用范围是CHGNOBS(即该macro内部,也就是局部的),两者的值。%local语句在macro内部声明一个局部宏变量,该宏变量在macro内部屏蔽了外层作用域的同名宏变量。屏蔽的意思是,在macro内部不能访问外层作用域的同名宏变量的值了。另外,局部宏变量的作用范围用macro的名字来定义。(后面我们会讲递归,到时候再回过头来讨论下一局部宏变量的作用域)。

3. 屏蔽的意义在哪里?

在什么情况下我们需要主动去屏蔽外层的同名宏变量?还是通过一个例子来说明。假定我们需要写一个macro,打印某library下面所有dataset中字符型变量的值的最长长度。为了我们的讨论,我们把这个过程拆分成两层循环:第一层循环打印用于遍历访问library下所有(non-empty)的dataset,另一层循环遍历某个dataset的字符型变量,并打印其最长长度。

%macro getMaxCharLen(data);

%macro loopData(lib);

外层循环macro调用了内层循环macro,这被称作macro的嵌套(nesting).实际上如果你补充完宏getMaxCharLen的话然后调用loopData的话,你会发现这个宏根本停不下来--它陷入了死循环(infinite loop)。具体的原因就留给各位看官了,我只指出,至少需要在getMaxCharLen的内部将循环变量i使用%local来声明。

可能你会想,如果使用不同的循环变量不就行了吗?是的,在这个例子当中,确实可以解决问题。但是现实的情况是,1) 有的时候你不清楚你写的一个包含循环的macro会被其他人或者自己用在某些什么其他场合,该场合可能包含外层循环,而你不能事先知道外层循环变量的名字,即便知道了要想避开同名也总是一件麻烦的事情; 2) 即便是外层不包含循环,一旦外层macro里有和内层macro同名的宏变量,这些宏变量的值就会被意外的修改,从而引发可能的bug。

所以,好的习惯是,在定义宏的时候,用%local声明每个宏变量,unless there is a higher purpose of not doing so.

End.

作者:鱼木

来源:知乎

,