词法定界

简介: 词法定界

当编写一个被其他函数 B 包含的函数 A 时,被包含的函数 A 可以访问包含其的函数 B 的所有局部变量,我们将这种特性称为 词法定界 。虽然这种可见性规则听上去很明确,但实际上并非如此。词法定界外加嵌套的第一类值函数可以为编程语言提供强大的功能,但很多编程语言并不支持将这两者组合使用


先看一个简单的例子,假设有一个表,其中包含了学生的姓名和对应的成绩,如果我们想基于分数对学生姓名排序,分数高者在前,那么可以使用如下的代码完成上述需求:

names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names,function(n1, n2)
  return grades[n1] > grades[n2]
end)


现在,假设我们想创建一个函数来完成这个需求:

function sortbygrade (names, grades)
  table.sort(names, function(n1, n2)
    return grades[n1] > grades[n2]
  end)
end


在后一个示例中,有趣的一点就在于传给函数 sort 的匿名函数可以访问 grades ,而 grades 是包含匿名函数的外层函数 sortbygrade 的形参。在该匿名函数中, grades 既不是全局变量也不是局部变量,而是我们所述的非局部变量。


提示

由于历史原因,在 Lua 语言中非局部变量也被称为上值


这一点之所以如此有趣是因为,函数作为第一类值,能够逃逸出他们变量的原始定界范围。考虑如下代码:

function newCounter()
  local count = 0
  return function ()
    count = count + 1
    return count
  end
end
c1 = newCounter()
print(c1())   --> 1
print(c1())   --> 2


在上述代码中,匿名函数访问了一个非全局变量count )并将其当做计数器。然而,由于创建变量的函数( newCounter )已经返回,因此当我们调用匿名函数时,变量 count 似乎已经超出了作用范围。但其实不然,由于闭包概念的存在, Lua 语言能够正确地应对这种情况。简单地说,一个闭包就是一个函数外加能够使该函数正确访问非局部变量所需的其他机制。如果我们再次调用 newCounter ,那么一个新的局部变量 count 和一个新的闭包会被创建出来,这个新的闭包针对的是这个新变量:

c2 = newCounter()
print(c2())   --> 1
print(c1())   --> 3
print(c2())   --> 2


因此, c1c2 是不同的闭包。他们建立在相同的函数之上,但是各自拥有局部变量 count 的独立实例。


从技术上将, Lua 语言中只有闭包而没有函数函数本身只是闭包的一种原型。不过尽管如此,只要不会引起混淆,我们就仍将使用术语“函数”来代指闭包。


闭包在许多场合中均是一种有价值的工具。正如我们之前已经见到过的,闭包在作为诸如 sort 这样的高阶函数的参数时就非常有用。同样,闭包对于创建了其他函数的函数也很有用,例如我们之前的 newCounter 实例及求导数的示例。这种机制使得 Lua 程序能够综合运用函数式编程世界中多种精妙的编程技巧。另外,闭包对于回调函数来说也很有用。对于回调函数而言,一个典型的例子就是在传统 GUI 工具箱中创建按钮。每个按钮通常都对应一个回调函数,当用户按下按钮时,完成不同的处理动作的回调函数就会被调用。


例如,假设有一个具有 10 个类似按钮的数字计算器(每个按钮代表一个十进制数字),我们就可以使用如下的函数来创建这些按钮:

function digitButton(digit)
  return Button{
    label = tostring(digit),
    action = function ()
      add_to_display(digit)
    end
  }
end


在上述示例中,假设 Button 是一个创建新按钮的工具箱函数, label 是按钮的标签, action 是当按钮按下时被调用的回调函数。回调可能发生在函数 digitButton 早已执行完后,那是变量 digit 已经超出了作用范围,但闭包仍可以访问它。


闭包在另一种很不一样的场景下也非常有用。由于函数可以被保存在普通变量中,因此在 Lua 语言中可以轻松地重新定义函数,甚至预定义函数。这种机制也正是 Lua 语言灵活的原因之一。通常,当重新定义一个函数的时候,我们需要在新的实现中调用原来的那个函数。例如,假设要重新定义函数 sin 以使其参数以角度为单位而不是以弧度为单位。那么这个新函数就可以先对参数进行转换,然后再调用原来的 sin 函数进行真正的计算。代码可能形如:

local oldSin = math.sin
math.sin = function (x)
  return oldSin(x * (math.pi / 180))
end


另一种更清晰一点的完成重新定义的写法是:

do 
  local oldSin = math.sin
  local k = math.pi / 180
  math.sin = function (x)
    return oldSin(x * k)
  end
end


上述代码使用了 do 代码段来限制局部变量 oldSin 的作用范围;根据可见性规则,局部变量 oldSin 只在这部分代码段中有效。因此,只有新版本的函数 sin 才能访问原来的 sin 函数,其他部分的代码则访问不了。


我们可以使用同样的技巧来创建安全的运行时环境,即所谓的沙盒sandbox )。当执行一些诸如从远程服务器上下载到的未受信任代码时,安全的运行时环境非常重要。例如,我们可以通过使用闭包重新定义函数 io.open 来限制一个程序能够访问的文件:

do
  local oldOpen = io.open
  local access_OK = function (filename, mode)
    check access
  end
  io.open = function (filename, mode)
    if access_OK(filename, mode) then
      return oldOpen(filename, mode)
    else
      return nil, "access denied"
    end
  end
end


上述实例的巧妙之处在于,经过重新定以后,一个程序就只能通过新的受限版本来调用原来未受限版本的 io.open 函数。示例代码将原来不安全的版本保存为闭包的一个私有变量,该变量无法从外部访问。通过这一技巧,就可以在保证简洁性和灵活性的前提下在 Lua 语言本身上构建 Lua 沙盒。相对于提供一套大而全的解决方案, Lua 语言提供的是一套 元机制 。接着这种机制可以根据特定的安全需求来裁剪具体的运行时环境。


Tip

真实的沙盒除了保护外部文件外还有更多的功能。

目录
相关文章
表达式语法分析——递归子程序法
表达式语法分析——递归子程序法
|
C语言
C语言根据协议分割获取字符串单元
C语言根据协议分割获取字符串单元
67 0
|
1月前
|
自然语言处理 编译器 C语言
软考:区分词法分析、语法分析、语义分析
本文解释了编译过程中的词法分析、语法分析和语义分析三个阶段的区别,并提供了相关练习题,帮助读者理解各阶段在编译过程中的作用和重要性。
66 4
|
6月前
语法树的画法(根据文法求字符串)
语法树的画法(根据文法求字符串)
62 1
|
6月前
分段函数求值
分段函数求值
29 1
|
资源调度
第7章 符号计算——7.4 符号表达式替换
第7章 符号计算——7.4 符号表达式替换
第7章 符号计算——7.2 符号对象和符号表达式(2)
第7章 符号计算——7.2 符号对象和符号表达式(2)
第7章 符号计算——7.2 符号对象和符号表达式(1)
第7章 符号计算——7.2 符号对象和符号表达式(1)
第7章 符号计算——7.3 符号表达式操作
第7章 符号计算——7.3 符号表达式操作