首先:我可以自己解决问题,但我不明白为什么我的原始解决方案不起作用,这就是我感兴趣的内容。我试图在这里做一个简洁的例子:
我正在动态构建数组,每个数组值都是一个列表。让我们从以下程序开始:
# 'collector' is a callback function, expecting a container array, and some
# data used to populate the array.
proc generate { collector arr_name } {
eval $collector $arr_name first XXX YYY
eval $collector $arr_name second UUU VVV
}
# This is the callback function used in our example
proc collect { container_name key valuex valuey } {
upvar $container_name container
lappend container($key) [list $valuex $valuey]
}
# Procedure to write out an array
proc dump { arr_name } {
upvar $arr_name arr
puts $arr_name:
foreach key [array names arr] {
puts "$key : $arr($key)"
}
}
# Main program
array set containerA {}
generate [namespace code { collect }] containerA
dump containerA
到目前为止,没什么了不起的。运行此程序会生成输出
containerA:
second : {UUU VVV}
first : {XXX YYY}
但现在让我们稍微扩展这个程序
# Wrapper function to call 'generate' using a fixed collector function
# ("Currying" the first argument to generate)
proc coll_gen { container_name } {
upvar $container_name container
generate [namespace code { collect }] $container_name ; # This works
# This would not work:
#generate [namespace code { collect }] container
}
array set containerB {}
coll_gen containerB
dump containerB
正如这里所写,这也可以,我们得到输出
containerB:
second : {UUU VVV}
first : {XXX YYY}
现在我的问题:正如您已经可以从代码中的注释中猜到的那样,我首先将coll_gen写为
proc coll_gen { container_name } {
upvar $container_name container
generate [namespace code { collect }] container
}
我的理由是,由于容器是数组的别名,其名称是通过参数列表传递的,我同样可以将此别名的名称传递给'生成'功能。但是,当我运行代码(Tcl 8.5)时,事实证明containerB为空。
为什么它也没有以这种方式工作?
答案 0 :(得分:2)
问题是评估范围之一。
在事情不起作用的情况下,让我们在collect
内写出调用堆栈:
::
coll_gen containerB
generate {namespace inscope :: { collect }} container
namespace inscope :: { collect } container first XXX YYY
collect container first XXX YYY
糟糕! namespace inscope
的内容是什么?内层upvar
在哪里? namespace code
的结果是包含namespace inscope
(您不应该直接写;使用namespace code
或namespace eval
)来安排通过附加其他参数(具有适当的元字符保护)将在给定的命名空间中运行(在您的情况下为::
,我假设)。这个“在给定的命名空间中运行”需要添加另一个堆栈帧,这就是upvar
正在探究的内容(它可能创建了一个名为container
的全局数组,因为namespace inscope
帧是命名空间耦合的帧,而不是“过程本地”堆栈帧。
你可以在upvar 2
内使用upvar 3
或甚至collect
(我不太确定哪个)来解决这个问题,但那可怕而且非常脆弱。
你最好像这样编写你的代码:
proc coll_gen { container_name } {
upvar $container_name container
generate [namespace which collect] container
}
proc generate { collector arr_name } {
upvar 1 $arr_name collectorVar
eval $collector collectorVar first XXX YYY
eval $collector collectorVar second UUU VVV
}
这样,调用栈就会变成这样:
::
coll_gen containerB
generate ::collect container
::collect collectorVar first XXX YYY
注释每个级别内调用数组的内容......
:: ### containerB
coll_gen containerB ### container (→ containerB)
generate ::collect container ### collectorVar (→ container → containerB)
::collect collectorVar first XXX YYY ### container (→ collectorVar → container → containerB)
答案 1 :(得分:1)
Tcl是非常直观的,我觉得尽可能用字符串来思考是有帮助的,类似于你在使用Lisp时对符号的看法,但更为普遍。当您使用upvar
时,您得到的并不像其他语言中的参考变量。您可以使用本地名称来引用最初在另一个堆栈帧(或upvar 0
中的相同堆栈帧)中引用的Tcl_Obj。在调用中
generate [namespace code { collect }] container
generate
的第二个参数不会对container
中coll_gen
引用的Tcl_Obj进行任何类型的引用:该参数只是一个包含字符串“container”的Tcl_Obj ”。如果该字符串等于其中一个堆栈帧中的有效名称,则可以upvar
该名称来获取/能够在关联对象中设置值(如果您已正确管理堆栈帧,它甚至会成为您想要访问的对象。
命令upvar
和uplevel
有重要用途,但你真的不需要它们。如果您只使用名称并且不尝试将对象拖动到每个堆栈框架中,则代码将变得更易于阅读并且更易于维护:
proc generate args {
# use eval $args first XXX YYY if you have Tcl 8.4 or earlier
{*}$args first XXX YYY
{*}$args second UUU VVV
}
proc collect {container_name key args} {
lappend ${container_name}($key) $args
}
proc dump arr_name {
puts $arr_name:
dict for {key val} [array get $arr_name] {
puts "$key : $val"
}
}
proc coll_gen container_name {
generate [namespace code collect] $container_name
}
array set containerB {}
set container_name [namespace which -variable containerB]
foreach cmd {coll_gen dump} {$cmd $container_name}
在全局范围内创建的变量(通过赋值或variable
命令)将是一个独立于堆栈帧的 namespace 变量:程序中的每个proc都能够使用绝对引用(例如由namespace which
创建或仅将名称空间添加到变量名称)来访问它。
局部变量OTOH通过名称和堆栈帧消除歧义。在堆栈框架内,每次使用某个变量名称都将引用同一个对象。在简单的情况下,proc将仅在一个堆栈帧中执行,但uplevel
命令可能导致某些代码段在另一个堆栈帧中执行。在这种情况下,可以使用相同的名称来指代同一代码体中的不同对象。但是,没有歧义:执行级别决定了名称所指的对象。
使用upvar
命令时,可以使用两个不同的名称+堆栈帧排列来引用驻留在某个堆栈级别的相同对象,或者可以使用相同的名称来引用来自不同堆栈级别的对象: / p>
proc foo {} {set abc foo ; bar}
proc bar {} {set abc bar ; baz}
proc baz {} {set abc baz ; qux}
proc qux {} {
set abc qux
foreach n {3 2 1 0} {
upvar $n abc var
lappend res $var
}
puts [join $res { }]
}
foo
# => foo bar baz qux
同样,从来没有任何歧义,因为名称+堆栈级别指定使对象的身份清晰。
uplevel
和upvar
命令可以非常方便,只要您可以保持堆栈帧直,并且我一直使用它们。正如你在Donal的回答中看到的那样,即使是Tcl ace也不能总是保持堆栈框架的直线,在这种情况下,命名空间变量更加简单和安全。
文档:array,dict,foreach,lappend,namespace,proc,puts,{{3 }},set,{*},uplevel