关于传递包含列表的Tcl数组

时间:2014-10-03 10:47:17

标签: tcl

首先:我可以自己解决问题,但我不明白为什么我的原始解决方案不起作用,这就是我感兴趣的内容。我试图在这里做一个简洁的例子:

我正在动态构建数组,每个数组值都是一个列表。让我们从以下程序开始:

# '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为空。

为什么它也没有以这种方式工作?

2 个答案:

答案 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 codenamespace 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的第二个参数不会对containercoll_gen引用的Tcl_Obj进行任何类型的引用:该参数只是一个包含字符串“container”的Tcl_Obj ”。如果该字符串等于其中一个堆栈帧中的有效名称,则可以upvar该名称来获取/能够在关联对象中设置值(如果您已正确管理堆栈帧,它甚至会成为您想要访问的对象。

命令upvaruplevel有重要用途,但你真的不需要它们。如果您只使用名称并且不尝试将对象拖动到每个堆栈框架中,则代码将变得更易于阅读并且更易于维护:

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

同样,从来没有任何歧义,因为名称+堆栈级别指定使对象的身份清晰。

uplevelupvar命令可以非常方便,只要您可以保持堆栈帧直,并且我一直使用它们。正如你在Donal的回答中看到的那样,即使是Tcl ace也不能总是保持堆栈框架的直线,在这种情况下,命名空间变量更加简单和安全。

文档:arraydictforeachlappendnamespaceprocputs,{{3 }},set{*}uplevel