深入理解 UpValue 和闭包

简介: Lua 函数为第一类值,支持词法定界与闭包特性。函数可作为参数传递、返回值返回,且能访问外部变量(UpValue)。通过闭包机制,函数可携带其所需环境,实现灵活编程。

Lua 语言中,函数是严格遵循词法定界的第一类值。Lua 中,函数是第一类类型值,这意味着定义函数和其他普通类型是一样的,区别在于函数对应的数据值是对应的函数体的指令罢了。一个程序可以将某个函数保存到变量中(全局变量和局部变量均可)或表中,也可以将某个函数作为参数传递给其他函数,还可以将某个函数作为其他函数的返回值返回。

“词法定界”这个特性,使得可以在函数内再定义内嵌的函数,即函数可以访问包含其自身的外部函数中的变量。如果函数f2在函数f1中,那么将f2称为f1的内嵌函数,而于f1称为f2的外包函数,内嵌函数可以访问其外包函数中的所有局部变量,这种特性称为词法作用域,而这些局部变量就称为该内嵌函数的外部局部变量,或者常说的UpValue(也成为非局部变量)。

闭包就是函数加上它所需访问的UpValue,前者涉及代码,而UpValue则与函数环境相关。

示例:

代码语言:Lua

代码运行次数:0

自动换行运行

AI代码解释

function newCounter()
  local i=0
  return function()
    i=i+1
    return i
  end
end
n1=newCounter()
print(n1())
print(n1())
n2=newCounter()
print(n2())
print(n2())

执行结果:

代码语言:Bash

自动换行

AI代码解释

1
2
1
2

函数newCounter每次返回一个匿名函数,每次调用时将其所引用的UpValue 递增;注意到n1和n2的函数体都是一样的,但是环境( UpValue )却不相同,这也是当n1输出结果是2时,n2新创建出来的函数输出还是1的原因。

二、函数是第一类值

体验下第一类值的含义:

代码语言:Lua

自动换行

AI代码解释

> a = {p = print} --a.p 指向print 函数
> a.p ("hello world")
hello world
> print=math.sin -- print现在指向sin函数
> a.p(print(1)) 
0.8414709848079 
> math.sin = a.p -- sin 现在指向print 函数
> math.sin (10 , 20)
10 20

Lua语言中常见的函数定义方式:aly.tatielle.com55

代码语言:Lua

自动换行

AI代码解释

function first(x) return x*x end
--等价于
first=function(x) return x*x end

赋值语句右边的表达式就是函数构造器,与表构造器 {} 相似,函数定义实际上就是创建类型为 function 的值并把它赋值给一个变量的语句。

Lua 语言中,所有的函数都是匿名的;像其他所有的值一样,函数并没有名字。当讨论函数名时 ,比如print,实际上指的是保存该函数的变量,虽然通常会把函数赋值给全局变量,从而看似给函数起了一个名字, 但在很多场景下仍然会保留函数的匿名性。

三、非全局函数

由于函数是一种“第一类值”,因此一个显而易见的结果就是:函数不仅可以被存储在全局变量中,还可以被存储在表字段和局部变量中。大部分Lua语言的库就采用了这种机制(例如 io.read和math.sin )。

代码语言:Lua

代码运行次数:0

自动换行运行

AI代码解释

Lib={}
Lib.add=function(x,y) return x+y end
lib.sub=function(x,y) return x-y end
--等价于使用表构造器
Lib={
  add=function(x,y) return x+y end
  sub=function(x,y) return x-y end
}
--或者这样
Lib={}
function Lib.add(x,y) return x+y end
function Lib.sub(x,y) return x-y end

当把一个函数存储到局部变量时,就得到了一个局部函数,即一个被限定在指定作用域中使用的函数。局部函数对于包( package )而言尤其有用。词法定界保证了程序段中的其他函数可以使用这些局部函数。

值得注意的是,在定义局部递归函数时,有一点比较容易出错:aly.sonoyun.net11

代码语言:Lua

自动换行

AI代码解释

local fact=function(n)
  if n==0 then return 1
  else return n*fact(n-1)   --有问题
  end
end

上述代码中,由于局部fact还没创建,直接调用时将无法使用局部的fact,Lua解析器会去寻找全局的fact函数来调用。可以通过先定义局部变量再定义函数的方式来解决这个问题:

代码语言:Lua

自动换行

AI代码解释

local fact
fact=function(n)
  if n==0 then return 1
  else return n*fact(n-1)   --有问题
  end
end

尽管在定义函数时,这个局部变量的值尚未确定,但到了执行函数时, fact 肯定已经有了正确的赋值。当然,也可以使用如下的方式解决上述问题:

代码语言:Lua

自动换行

AI代码解释

local function fact (n) body end

Lua解析器会把它展开为:

代码语言:Lua

自动换行

AI代码解释

local fact
fact=function (n) body end

但是,这个技巧对于间接递归函数是无效的;在间接递归的情况下,必须使用与明确的前向声明等价的形式。

四、词法定界

当编写一个被其它函数A包含的函数B时,被包含的函数B可以访问包含其的函数A的所有局部变量,将这种特性称为词法定界。词法定界外加嵌套的第一类值函数可以为编程语言提供强大的功能,但很多编程语言并不支持将这两者组合使用。

UpValue之所以有趣,函数作为第一类值,能够逃逸出它们变量的原始定界范围。可以看看如下的代码:

代码语言:Lua

自动换行

AI代码解释

function newCounter()
  local i=0
  return function()
    i=i+1
    return i
  end
end
n1=newCounter()
print(n1())
print(n1())

执行结果:

代码语言:Lua

自动换行

AI代码解释

1
2

匿名函数访问了一个非局部变量( i)并将其当作计数器;然而,由于创建变量的函数(newCounter)己经返回,因此当调用匿名函数时,变量 count 似乎已经超出了作用范围。但其实不然,由于闭包概念的存在,Lua 语言能够正确地应对这种情况。简单地说,一个闭包就是一个函数外加能够使该函数正确访问非局部变量所需的其他机制

闭包在许多场合中均是一种有价值的工具。比如作为sort 这样的高阶函数参数时就非常有用。同样,闭包对于那些创建了其他函数的函数也很有用。另外,闭包对于回调 callback 函数来说也很有用。

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

代码语言:Lua

自动换行

AI代码解释

do
  local oldOpen=io.open
  local access_ok=function(filename,mode)
    --check
  end
  io.open=function(filename,mode)
    if access_ok(filename,mode) the
      return oldOpen(filename,mode)
    else
      return nil,"access fail"
    end
  end
end

五、函数式编程示例

目标是开发一个用来表示几何区域的系统,其中区域即为点的集合,能够利用该系统表示各种各样的图形 ,同时可以通过多种方式(旋转 、变换、并集等)组合和修改这些图形。鉴于一个几何区域就是点的集合,因此可以通过特征数来表示一个区域, 即可以提供一个点(作为参数)并根据点是否属于指定区域而返回真或假的函数来表示一个区域。

代码语言:Lua

自动换行

AI代码解释

print("Hello World")
-- 圆形
function disk(cx,cy,r)
    return function(x,y)
        return (x-cx)^2 + (y-cy)^2<=r^2
    end
end
-- 矩形
function rect(left,right,bottom,up)
    return function(x,y)
        return left<=x and x<=right and bottom<=y and y<=up
    end
end
-- 补集
function complement(r)
    return function(x,y)
        return not r(x,y)
    end
end
-- 并集
function union(r1,r2)
    return function(x,y)
        return r1(x,y) or r2(x,y)
    end
end
-- 交集
function intersection(r1,r2)
    return function(x,y)
        return r1(x,y) and r2(x,y)
    end
end
-- 差集
function difference(r1,r2)
    return function(x,y)
        return r1(x,y) and not r2(x,y)
    end
end
--平移
function translate(r,dx,dy)
    return function (x,y)
        return r(x-dx,y-dy)
    end
end
-- PBM 文件中绘制区域
function plot(r,M,N)
    io.write("P1\n",M," ",N,"\n")   -- 头
    for i=1,N do                    -- 行
        local y=(N-i*2)/N
        for j=1,M do                -- 列
            local x=(j*2-M)/M
            io.write(r(x,y) and "1" or "0")
        end
        io.write("\n")
    end
end
--绘制了一个南半球所能看到的娥眉月
c1=disk(0,0,1)
plot(difference(c1,translate(c1,0.3,0)),500,500)

PBM 文件的结构很简单,PBM文件的文本形式以字符串“ P1 开头,接下来的一行是图片的宽和高(以像素为单位),然后是对应每一个像素、由1和0组成的数字序列,最后是 EOF。

六、总结

  1. 闭包的表现,函数内部可以访问函数外部的变量。
  2. lua文件是一个匿名函数,lua 内部函数可以访问文件中函数体外的变量。
  3. 闭包的实现,C 函数以及绑定在 C 函数上的上值。
  4. lua语言与其他语言差异:没有入口函数;索引从 1 开始;闭包;可以有多返回值;函数是第一类型;尾递归,不占用栈空间;条件表达式是nil 或者 false 为假,非 nil 为真;支持多元运算;非运算符是 ~ 而不是 !,所以不等于为 ~=
相关文章
|
3月前
|
存储 人工智能 Shell
Lua与C语言接口编程实战指南:打造高性能、灵活的程序
本文深入介绍了 Lua 与 C 语言的交互机制,重点分析了 Lua 作为胶水语言在嵌入式系统、游戏开发(如 Skynet、OpenResty)中的应用。内容涵盖 Lua 环境搭建、虚拟栈管理、C 与 Lua 的相互调用、闭包、Userdata 和注册表的使用等核心技术,并结合代码示例讲解了如何在实际项目中实现 Lua 与 C 的高效交互,适合希望掌握 Lua 扩展与嵌入开发的工程师参考学习。
291 0
|
3月前
|
人工智能 算法 C++
浅谈 KMP
KMP算法是一种高效的字符串匹配算法,由Knuth、Morris和Pratt提出。它通过预处理模式串构建next数组,利用匹配失败时的信息减少重复比较,从而提升匹配效率。其时间复杂度为O(m+n),适用于大规模文本匹配场景。
413 0
|
5月前
|
存储 监控 关系型数据库
B-tree不是万能药:PostgreSQL索引失效的7种高频场景与破解方案
在PostgreSQL优化实践中,B-tree索引虽承担了80%以上的查询加速任务,但因多种原因可能导致索引失效,引发性能骤降。本文深入剖析7种高频失效场景,包括隐式类型转换、函数包裹列、前导通配符等,并通过实战案例揭示问题本质,提供生产验证的解决方案。同时,总结索引使用决策矩阵与关键原则,助你让索引真正发挥作用。
374 0
|
3月前
|
人工智能 网络协议 Java
skynet对半关闭状态的支持
TCP四次挥手中,半关闭状态是否需要处理取决于具体应用场景。半关闭是指连接的一端关闭读或写通道,另一端仍可继续传输数据。在游戏服务器等场景中,需关注半关闭以确保数据完整发送。Java的Netty和Skynet框架对此有解决方案。Skynet通过reactor模型和epoll机制实现半关闭支持,确保在关闭写端前发送完剩余数据。测试表明,正确处理半关闭可避免数据丢失,提升连接关闭的可靠性。
64 0
|
3月前
|
人工智能 缓存 安全
你还是没有理解CAS
在高并发场景下,使用 `count++` 统计商品浏览次数可能导致计数丢失。本文介绍了如何使用 CAS(Compare and Swap)实现无锁的原子操作来解决该问题。CAS 通过比较内存值与期望值,确保更新操作的原子性,避免了线程竞争带来的数据错误。文章详细解析了 CAS 的工作机制、优势与局限性,并结合 Java 示例展示了其底层实现与实际应用,如高性能计数器、无锁栈和缓存更新策略。此外,还探讨了 CAS 可能引发的 ABA 问题及其解决方案,如版本号机制。最后,通过性能对比分析,帮助开发者根据场景合理选择并发控制方式。
102 0
|
3月前
|
人工智能 编译器 C语言
C语言模拟面向对象三大特性与C++实现对比
C语言通过结构体和函数指针模拟面向对象特性,实现封装、继承和多态,而C++则通过原生语法支持。两者在实现原理上有相似之处,但C++在语法、编译期检查和内存管理方面更具优势,提高了代码的安全性和开发效率。
77 0
|
3月前
|
安全
电脑进入bios关闭网卡的技巧
华硕电脑开机显示字符无法进入系统,提示“PXE-MOF:Exiting PXE ROM”,表明电脑正尝试从网卡启动。解决方法为进入BIOS关闭网卡启动功能。开机时连续按F2进入BIOS,切换至“Security”选项卡,找到“I/O Interface Security”设置,选择“LAN Network Interface”并设为“LOCKED”以禁用网卡启动,最后按F10保存退出即可。
500 0
|
3月前
|
存储 人工智能 安全
合约交互的风险与防护
本文介绍了 Solidity 中外部合约调用的三种方式:通过接口类型调用、使用低级 `.call` 方法以及 `delegatecall` 与 `staticcall`。重点分析了不同调用方式的安全性、适用场景及潜在风险,如重入攻击、Gas 限制和返回值伪造等。同时,总结了防范风险的最佳实践,如使用 Checks-Effects-Interactions 模式、引入 `ReentrancyGuard` 以及限制外部调用来源。最后通过实战演练演示了调用实现和重入攻击的防御效果。
95 0
|
3月前
|
人工智能 Kotlin
Jetpack Compose中常见的核心概念总结-2
本文介绍了Kotlin中用于构建用户界面的基本组件和布局方法,涵盖文本显示、按钮、懒加载列表、弹窗及自定义组件等内容。通过示例代码讲解了如何使用LazyColumn和LazyRow实现高效列表加载,利用AlertDialog创建交互式弹窗,并通过ConstraintLayout实现复杂的界面布局。此外,还展示了如何定义和复用自定义组件,提高代码的可维护性和可重用性。适合初学者掌握Kotlin UI开发的基础知识。
88 0
|
3月前
|
人工智能 Kotlin
Jetpack Compose中常见的核心概念总结-1
本教程涵盖Jetpack Compose基础布局、修饰符使用、状态管理、样式主题及动画实现,通过Kotlin代码示例讲解Column、Row布局及常用组件样式与交互处理。
126 0