• Lua 调试库( debug )


    一、调试库作用

    使用调试库可以获取 Lua 环境运行过程中的变量和跟踪代码执行。

    调试库主要分为两类函数:自省函数和钩子函数

    • 自省函数:允许检查一个正在运行中的程序,例如活动函数的栈、当前正在执行的代码行、局部变量的名称和值。
    • 钩子函数:允许跟踪一个程序的执行。

    值得注意:

    调试库的某些功能性能不高,而且会打破语言的一些固有规则。

    二、自省函数

    1、debug.getinfo(thread, f, what)

    该函数会返回包含函数信息的表。

    参数:

    • thread: 表示在 thread 中获取相应的信息
    • f: 可以给两种模式,函数或数值。第一种则是给函数,获取给定函数的信息表。第二种则是给一个数字作为 f 的值,表示栈层级:当为 0 时表示当前函数( 即 getinfo 本身), 1 表示调用 getinfo 的函数(尾调用除外,它们不计入堆栈),以此类推。如果 f 是一个大于活跃栈层级的数字,则 getinfo 返回 nil
    • what: 可选项,表示要获取哪些指定的信息。因为 getinfo 的效率不高,所以为了效率好些,可以只选择需要的内容,如果需要多个值时,可以将多个拼凑,例如 nfS
    what 取值获取的值
    n选择 name 和 namewhat
    f选择 func
    S选择 source、short_src、what、linedefined 和 lastlinedefined
    l选择 currentline
    L选择 activelines
    u选择 nup、nparams 和 isvararg

    这些字段的含义如下:

    字段描述
    name该字段是该函数的一个适当的名称,例如保存该函数的全局变量的名称。(可能没有值,也可能有多个名称)(只有当 f 为数值时才有该值)
    namewhat该字段用于说明 name 字段的含义,可能是 “global”、“local”、“method”、“field” 或 “”(空字符串)。空字符串表示 Lua 语言找不到该函数的名称。
    func该字段是该函数本身
    source该字段用于说明函数定义的位置。如果函数定义在一个字符串中(通过调用 load),那么 source 就是这个字符串;如果函数定义在一个文件中,那么 source 就是使用 @ 作为前缀的文件名
    short_src该字段是 source 的精简版本(最多 60 个字符),对于错误信息十分有用。
    what该字段用于说明函数的类型。
    - 如果 foo 是一个普通的 Lua 函数,则为 “Lua”
    - 如果是一个 C 函数,则为 “C”
    - 如果是一个 Lua 语言代码段的主要部分,则为 “main” 。
    linedefined该字段是该函数定义在源代码中第一行的行号
    lastlinedefined该字段是该函数定义在源代码中最后一行的行号
    currentline表示当前该函数正在执行的代码所在的行 (只有当 f 为数值时才有该值)
    istailcall返回一个布尔值,为真表示函数是被尾调用所调起(尾调用时,函数真正的调用者不在栈中)(只有 f 为数值时才有该值)
    activelines该字段是一个包含该函数所有活跃行的集合。活跃行是指除空行和只包含注释的行外的其它行(该字段的典型用法是用于设置断点。大多数调试器不允许在活跃行外设置断点,因为非活跃行是不可达的)。
    nups该字段是该函数的上值的个数
    nparams该字段是该函数的参数个数
    isvararg该字段表明该函数是否为可变长函数

    返回值:

    如果传递的是一个函数或是一个合理的数值(小于等于栈层级),则会返回对应函数的信息表。如果超出的栈层级,则返回 nil

    值得注意:

    如果假设 foo 是一个 C 函数,Lua 语言没有多少关于该函数的信息。只有字段 what、name、namewhat、nups 和 func 是有意义的。

    举两个例子:

    输出一个函数的信息

    function foo(a, b, ...)
        print("江澎涌")
    end
    local info = debug.getinfo(foo)
    for k, v in pairs(info) do
        print(k, "---", v)
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用数值调用

    foo1 = function(...)
        local table = debug.getinfo(1)
        for k, v in pairs(table) do
            print(k, "---", v)
        end
    end
    foo1()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面例子中调用栈的层次如下:

    2、traceback(thread, message, level)

    返回调用栈信息

    参数:

    • thread: 表示在 thread 中获取相应的信息
    • message:该参数没有限定为字符串,可以是任意的类型。如果为字符串或 nil ,则会返回调用栈的描述字符串,并且在最开始的地方拼接该 message (如果为 nil ,则不拼接)。如果为其他类型,则直接放回该值。
    • level:调用层级,0 表示 traceback 函数,1 表示调用 traceback 函数的函数,2 表示调用 traceback 函数的函数的函数 …

    返回值:

    如果 message 为字符串或 nil ,则会返回调用栈的描述字符串,并且在最开始的地方拼接该 message (如果为 nil ,则不拼接)。如果为其他类型,则直接返回该值。

    举个例子

    print("没有参数")
    local function foo1()
        print(debug.traceback())
    end
    foo1()
    
    --> 没有参数
    --> stack traceback:
    --> 	...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:58: in local 'foo1'
    --> 	...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:60: in main chunk
    --> 	[C]: in ?
    
    print("携带 message(字符串)")
    local function foo2()
        print(debug.traceback("track back message."))
    end
    foo2()
    
    --> 携带 message(字符串)
    --> track back message.
    --> stack traceback:
    --> 	...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:64: in local 'foo2'
    --> 	...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:66: in main chunk
    --> 	[C]: in ?
    
    print("携带 message(非字符串)")
    local function foo3()
        print(debug.traceback({}))
    end
    foo3()
    
    --> 携带 message(非字符串)
    --> table: 0x600000cf0d80
    
    print("携带 message(非字符串)且携带 level ")
    local function foo4()
        --print(debug.traceback("track back message.", 0))
        --print(debug.traceback("track back message.", 1))
        print(debug.traceback("track back message.", 2))
        --print(debug.traceback(nil, 2))
    end
    foo4()
    
    --> 携带 message(非字符串)且携带 level 
    --> track back message.
    --> stack traceback:
    --> 	...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:81: in main chunk
    --> 	[C]: in ?
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    3、getlocal(thread, f, var)

    通过 f (可以是函数也可以栈层次)和 var 指定的索引,返回对应的变量的名称和值。此处的变量包括:局部变量,参数和临时变量。

    参数

    • thread: 表示在 thread 中获取相应的信息
    • f:该值可以是函数也可以是栈层次。如果是函数,则该函数只会参数的名称,不会有其他的局部变量和临时变量(因为这个时候函数还未运行,所以对于运行时来说,都是不知道的)。如果是栈层级,则可以返回局部变量,参数和临时变量(但仅限于活跃的变量)。
    • var:参数或局部变量的索引。如果为正数,则按照变量的申明顺序(包括参数),从 1 开始往后访问所有的变量。如果为负数,则范围的是可变参数,从 -1 开始访问( -1 表示第一个)。如果索引没有对应的值,则返回 nil

    值得注意的是如果栈层级传递超出可用范围,则会导致抛出异常,可以使用 getinfo 先检测栈层级是否正确( getinfo 对非法的栈层级只会返回 nil ,不会有异常)。

    返回值:

    返回变量名和变量值。

    如果该值没有已知的名称(例如 for 循环中的临时变量),则会是以 “( ” 括号开头的变量来代替名称

    举个例子:

    用栈层级进行调用

    local outerParam = "外部局部变量"
    globalParam = "全局变量"
    local function foo(funParamA, funParamB, ...)
        local localParamX
        do
            local localParamC = funParamA - funParamB
        end
        local localParamA = 1
        print("------------------------------------")
        print("遍历变量:")
        -- for 循环只是为了临时模拟临时变量,所以一次循环就退出
        for i = 1, 2 do
            while true do
                local name, value = debug.getlocal(1, localParamA)
                if not name then
                    break
                end
                print(name, "---", value)
                localParamA = localParamA + 1
            end
            break
        end
    
        print("------------------------------------")
        print("遍历可变参数:")
        localParamA = -1
        while true do
            local name, value = debug.getlocal(1, localParamA)
            if not name then
                break
            end
            print(name, "---", value)
            localParamA = localParamA - 1
        end
    end
    foo(1, 200, "jiang", "pengyong")
    
    --> ------------------------------------
    --> 遍历变量:
    --> funParamA	---	1
    --> funParamB	---	200
    --> localParamX	---	nil
    --> localParamA	---	4
    --> (for state)	---	1
    --> (for state)	---	1
    --> (for state)	---	1
    --> i	---	1
    --> ------------------------------------
    --> 遍历可变参数:
    --> (vararg)	---	jiang
    --> (vararg)	---	pengyong
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    对函数进行调用

    local function foo(funParamA, funParamB, ...)
        local localParamX = "江澎涌"
    end
    for i = 1, math.huge do
        local name, value = debug.getlocal(foo, i)
        if not name then
            break
        end
        print(name, "---", value)
    end
    
    -- 不会有局部变量和临时变量,因为此时函数还未运行
    --> 遍历局部变量(函数):
    --> funParamA	---	nil
    --> funParamB	---	nil
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    4、debug.setlocal(thread, level, var, value)

    将值 value 设置给位于栈层级为 level 的函数中索引为 var 的局部变量。

    参数:

    • thread: 表示在 thread 中获取相应的信息
    • level:栈层级,和 getlocal 的 f 是一样的(只是不能传递函数,必须是数值)
    • var:变量索引
    • value:需要设置的值

    返回值:

    • 如果没有具有给定索引(var)的局部变量,该函数将返回 nil
    • 如果 level 超出合理的栈层级,则会抛出异常。 可以调用 getinfo 来检查级别是否有效
    • 如果一切正常则返回局部变量的名称

    举个例子:

    function showLocalParam()
        for i = 1, math.huge do
            local name, value = debug.getlocal(2, i)
            if not name then
                break
            end
            print(name, "---", value)
        end
    end
    
    local name = "jiang pengyong"
    local age = 29
    do
        local name = "江澎涌"
    
        print("-------------------------------------")
        print("设置前:")
        showLocalParam()
    
        print("-------------------------------------")
        -- 修改的是外部的 name ,而非内部的 name
        print("debug.setlocal",debug.setlocal(1, 1, "jiang"))
    
        print("-------------------------------------")
        print("设置后:")
        showLocalParam()
    end
    
    --> 设置前:
    --> name	---	jiang pengyong
    --> age	    ---	29
    --> name	---	江澎涌
    --> -------------------------------------
    --> debug.setlocal	name
    --> -------------------------------------
    --> 设置后:
    --> name	---	jiang
    --> age	    ---	29
    --> name	---	江澎涌
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    5、getupvalue(f, up)

    返回函数 f 索引为 up 的上值的名称和值。如果给定索引没有上值,则该函数返回 nil

    如果无法知道变量的名称,则会返回以 “(” 开头的变量名。(例如临时变量)

    举个例子:

    通过下面代码可以遍历闭包的所有上值

    要区分好上值和全局变量的概念,我们平常所说的全局变量其实是存储在上值的 _ENV 中,编译器会做一层转换(这个在 “环境(_G 和 _ENV)” 一章中已有详细分享);而上值指的是函数所能访问到的非局部变量(例如下面代码段的 age、heavy 变量)。如果函数中不使用该变量,则该变量就不会被算作是上值(例如 heavy),如果函数中没有使用到任何的全局变量,则 _ENV 都会不存在(注意这里连函数 print 都不能为全局的,否则达不到该条件,具体可以看第二个例子)。

    name = "江澎涌"
    local age = 29
    function foo()
        local heavy = 120
        return function()
            print(age)
            -- 如果没有调用 heavy ,则 heavy 就不是该闭包的上值
            --print(heavy)
            local func = debug.getinfo(1, "f").func
            for i = 1, math.huge do
                local n, v = debug.getupvalue(func, i)
                if not n then
                    break
                end
                print(n, "--------", v)
            end
        end
    end
    foo()()
    
    --> 29
    --> _ENV	--------	table: 0x600001470200
    --> age	    --------	29
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    getupvalue 获取上值(不使用全局变量,连 _ENV 都会没有)

    这里的 print、getinfo、getupvalue、huge 都需要转为 loacl ,才能避免编译过程中编译器自动为全局变量加上 _ENV ,导致上值引入了 _ENV 变量。

    name = "江澎涌"
    local print = print
    local getinfo = debug.getinfo
    local getupvalue = debug.getupvalue
    local huge = math.huge
    local age = 29
    function foo()
        local heavy = 120
        return function()
            print(age)
            local func = getinfo(1, "f").func
            for i = 1, huge do
                local n, v = getupvalue(func, i)
                if not n then
                    break
                end
                print(n, "--------", v)
            end
        end
    end
    foo()()
    
    -- 可以看到 _ENV 变量不见了,因为没有引用到任何的 “全局变量”
    --> 29
    --> print	--------	function: 0x106ac4ac0
    --> age	    --------	29
    --> getinfo	--------	function: 0x106ac5e30
    --> huge	--------	inf
    --> getupvalue	--------	function: 0x106ac6500
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    6、debug.setupvalue(f, up, value)

    将值 value 设置给给函数 f 的索引为 up 的上值。

    如果给定索引没有上值,则该函数返回 nil。 否则,它返回上值的名称。

    name = "江澎涌"
    local age = 29
    local function foo()
        local heavy = 120
        function showinfo(func)
            for i = 1, math.huge do
                local n, v = debug.getupvalue(func, i)
                if not n then
                    break
                end
                print(n, "--------", v)
            end
        end
    
        return function()
            print(age)
            local func = debug.getinfo(1, "f").func
            print("设置上值前:")
            showinfo(func)
    
            -- 这里的 2 刚好就是指 age 变量
            debug.setupvalue(func, 2, 116)
    
            print("设置上值后:")
            showinfo(func)
        end
    end
    foo()()
    
    --> 29
    --> 设置上值前:
    --> _ENV	--------	table: 0x600000f38040
    --> age	--------	29
    --> 设置上值后:
    --> _ENV	--------	table: 0x600000f38040
    --> age	--------	116
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    7、debug.debug()

    该函数可以让用户进入交互模式,运行用户输入的每个字符串。

    可以使用简单的命令和其他调试工具,可以检查全局和局部变量、更改它们的值、计算表达式等。

    使用 cont 结束此函数

    值得注意,debug.debug 命令并未按词法嵌套在任何函数中,因此无法直接访问局部变量。

    8、debug.setmetatable(value, table)

    table(可以是nil)设置为 value 作为元表,会将 value 返回。

    setmetatable 的区别在于,debug.setmetatable 绕开了元表原有的检测机制。例如:无论 value 的元表是否设置了 __metatable ,对元表进行保护,不被外部篡改,都能进行修改。

    例子在下面小节结合 debug.getmetatable 给出

    9、debug.getmetatable(value)

    返回给定 value 的元表,如果没有元表则返回 nil 。

    getmetatable 的区别在于,debug.getmetatable 也是绕开了元表的检测机制。例如:无论 value 的元表是否设置了 __metatable ,都能获取到真正的元表。

    举个例子

    local table = {}
    local mt = { __metatable = "protect metatable" }
    
    -- 给 table 设置一个带有 __metatable 的元表,用于保护元表
    print(setmetatable(table, mt))
    -- 如果使用 getmetatable 则只能获取到 __metatable 的值
    print(getmetatable(table))
    -- 如果使用 debug.getmetatable 则可以获取到 table 的元表
    print(debug.getmetatable(table), mt)
    
    -- setmetatable 设置会报错,因为 table 的元表被保护者,会抛 "cannot change a protected metatable" 异常
    --setmetatable(table, nil)
    -- debug.setmetatable 则可以正常设置
    print(debug.setmetatable(table, nil))
    -- 通过打印,验证设置有效
    print(getmetatable(table))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    10、debug.setuservalue(udata, value, n)

    将给定的 value 设置为给定的 udata 相关联的第 n 个用户值。

    但 udata 必须是一个完整的 userdata。

    如果设置成功,则会返回 udata;如果 userdata 没有该值,则返回 nil。

    11、debug.getuservalue(u, n)

    获取与 u (userdata 类型)相关联的第 n 个用户值,会返回该值以及一个布尔值。

    如果 userdata 没有该值,布尔值为 false。

    12、debug.getregistry()

    返回注册表。注册表是一个特殊的 Lua 表,可以被用于存储全局数据或跟踪引用。但正常的使用不建议使用它来作为全局变量的储存点。

    registry = debug.getregistry()
    registry.name = "江澎涌"
    for i, v in pairs(registry) do
        print(i, "-----", v)
    end
    
    --> 1	-----	thread: 0x7f88ed008e08
    --> 2	-----	table: 0x60000293c200
    --> FILE*	-----	table: 0x60000293c540
    --> name	-----	江澎涌
    --> _IO_output	-----	file (0x7ff85810fc98)
    --> _CLIBS	-----	table: 0x60000293c300
    --> _IO_input	-----	file (0x7ff85810fc00)
    --> _LOADED	-----	table: 0x60000293c280
    --> _PRELOAD	-----	table: 0x60000293c440
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    13、debug.upvalueid(f, n)

    从给定函数返回编号为 n 的上值的唯一标识符(作为轻型用户数据)。

    这些唯一标识符允许程序检查不同的闭包是否共享 upvalues。 共享一个上值(即访问同一个外部局部变量)的 Lua 闭包将为这些上值索引返回相同的 id。

    14、debug.upvaluejoin(f1, n1, f2, n2)

    使 Lua 闭包 f1 的第 n1 个上值引用 Lua 闭包 f2 的第 n2 个上值。

    三、传入协程参数

    在第二节中,可以看到大多函数的第一个函数是 thread ,这意味着可以传入一个协程。

    以 trackback 为例,对一个协程进行栈的获取

    co = coroutine.create(function()
        local x = 10
        coroutine.yield(x)
        error("coroutine error............")
    end)
    
    print("运行返回", coroutine.resume(co))
    print("打应栈", debug.traceback(co))
    
    --> 运行返回	true	10
    --> 打应栈	stack traceback:
    --> 	[C]: in function 'coroutine.yield'
    --> 	.../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:9: in function <.../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:7>
    
    print("运行返回", coroutine.resume(co))
    print("打应栈", debug.traceback(co))
    
    --> 运行返回	false	.../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:10: coroutine error............
    --> 打应栈	stack traceback:
    --> 	[C]: in function 'error'
    --> 	.../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:10: in function <.../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:7>
    
    print("获取局部变量", debug.getlocal(co, 1, 1)) --> 获取局部变量	x	10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    四、钩子函数

    1、debug.sethook(thread, hook, mask, count)

    设置钩子

    参数:

    • thread: 表示在 thread 中设置相应的钩子
    • hook:钩子被触发时,便会表用该函数。函数的第一个参数是事件类型,包括 call(也包括尾调用)、returnlinecount,对于 line 事件则会有第二个参数,表示行号。
    • mask:字符串掩码,可以是以下的任意字符组合
    掩码描述
    c每次 Lua 调用函数时都会调用钩子
    r每次 Lua 从函数返回时调用钩子
    l每次 Lua 进入一个新的代码行时调用这个钩子
    对于检测频率,只需要填入 count 参数就行
    • count:指定以什么频率获取 count 事件

    关闭钩子:

    当不带参数调用时,debug.sethook 关闭钩子

    钩子的使用:

    在一个钩子里面,可以用 level 为 2 调用 getinfo 来获取更多关于正在运行的函数的信息(0 级是 getinfo 函数,level 1 是钩子函数)

    举个例子:

    debug.sethook(print, "l")
    
    print("江澎涌")
    --> line	15
    --> 江澎涌
    
    print("小朋友")
    --> line	16
    --> 小朋友
    
    debug.sethook()
    --> line	17
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2、debug.gethook(thread)

    返回 thread 的当前挂钩设置,

    会返回三个值:当前挂钩函数、当前挂钩掩码和当前挂钩计数(这些值都是 debug.sethook 函数设置)。

    五、写在最后

    Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)

    如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

    公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。

  • 相关阅读:
    危机重重下的企业发展,数字化转型到底是不是企业未来救星
    【JVM】一文带你了解JVM中的垃圾回收机制(GC)——内含图解
    Visual Studio Code配置开发Maven项目、Spring Boot项目
    C语言——文件操作(2)文件的读写操作
    Python精髓之括号家族:方括号、花括号和圆括号,你真的会用吗?
    动物园(虚函数与多态)
    力扣(LeetCode)6. Z 字形变换(C++)
    Django——数据库
    Exch:Exchange Server 2013 即将终止支持
    站在开源云的新周期,EasyStack布局数字原生企业云
  • 原文地址:https://blog.csdn.net/weixin_37625173/article/details/132661883