【问题标题】:About passing around Tcl arrays holding lists关于传递持有列表的 Tcl 数组
【发布时间】:2014-10-03 10:47:17
【问题描述】:

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

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

# '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}

现在我的问题是:正如您已经从代码中的 cmets 猜到的那样,我首先将 coll_gen 编写为

proc coll_gen { container_name } {
  upvar $container_name container
  generate [namespace code { collect }] container
}

我的理由是,由于容器是数组的别名,其名称是通过参数列表传递的,我同样可以将此别名的名称传递给“生成”函数。但是,当我运行代码(Tcl 8.5)时,发现 containerB 是空的。

为什么它也不是这样工作的?

【问题讨论】:

    标签: tcl


    【解决方案1】:

    问题是评估范围之一。

    让我们在你在collect里面的点写出调用堆栈,以防万一事情不工作:

    ::
       coll_gen containerB
          generate {namespace inscope :: { collect }} container
             namespace inscope :: { collect } container first XXX YYY
                collect container first XXX YYY
    

    哎呀!那是什么namespace inscope? upvaring 的内层在哪里? namespace code 的结果是用 namespace inscope 包装(您不应该直接编写;使用 namespace code 或 namespace eval),它安排通过附加其他参数(具有适当的元字符保护)形成的脚本为在给定的命名空间中运行(我假设你的情况是::)。这种“在给定的命名空间中运行”需要添加另一个堆栈帧,这就是 upvar 然后插入的内容(它可能创建了一个名为 container 的全局数组,因为 namespace inscope 帧是命名空间耦合的,不是“过程本地”堆栈帧)。

    您可以在collect 中使用upvar 2 甚至upvar 3(我不太确定是哪个)来解决这个问题,但这太可怕了,而且很脆弱。 你最好这样写你的代码:

    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)
    

    【讨论】:

    • upvar 2 及以上通常表明您做错了,因为它使代码比应有的更加严格。
    • 非常感谢。将您的解决方案与 Hoodiecrow 的解决方案进行比较很有趣。以我的背景,我发现你的更容易理解,但这可能是因为我来自 Ruby/Perl/Python 方面,还没有习惯将变量视为“只是字符串”。如果你看看你的解决方案,以及 Hoodiecrow 的解决方案,你会认为哪一个更“Tcl-ish”?
    • @user1934428:呵呵。我涉足 Tcl; Donal 是 Tcl 的实现者之一。我认为这回答了你的问题;)
    【解决方案2】:

    Tcl 是非常字面的,我发现尽可能用字符串来思考是有帮助的,这与你在使用 Lisp 时用符号来思考的方式类似,但更加普遍。当你使用upvar 时,你得到的不是一些其他语言中的引用变量。您只需使用本地名称来引用一个 Tcl_Obj,该 Tcl_Obj 最初是在另一个堆栈帧(或相同的堆栈帧,如果您 upvar 0)中引用的。在调用中

    generate [namespace code { collect }] container
    

    generate 的第二个参数不会继承任何对 container 在 coll_gen 中引用的 Tcl_Obj 的引用:该参数只是一个包含字符串“容器”的 Tcl_Obj。如果该字符串等于其中一个堆栈帧中的有效名称,您可以upvar 获取/能够在关联对象中设置值的名称(并且如果您已正确管理堆栈帧,它将甚至是你想要访问的对象)。

    命令upvar 和uplevel 有重要用途,但在这里你真的不需要它们。如果您只使用 names 并且不尝试将对象拖过每个堆栈帧,那么您的代码将变得更易于阅读和维护:

    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 命令)将是一个独立于堆栈帧而存在的命名空间变量:程序中的每个 proc 都可以访问它使用绝对引用(例如由namespace which 创建或只是将命名空间添加到变量名之前)。

    局部变量 OTOH 通过名称和堆栈帧来消除歧义。在堆栈帧中,每次使用某个变量名都会引用同一个对象。在简单的情况下,proc 将仅在一个堆栈帧中执行,但uplevel 命令可能会导致某些代码在另一个堆栈帧中执行。在这种情况下,可以使用相同的名称来指代同一代码体中的不同对象。不过没有歧义:执行级别决定了名称所指的对象。

    使用upvar 命令时,可以使用两个不同的名称 + 堆栈帧排列来引用位于某个堆栈级别的相同对象,或者可以使用相同的名称来引用来自不同堆栈级别的对象:

    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 王牌也不能始终保持堆栈帧的正确性,在这些情况下,命名空间变量更加简单和安全。

    文档:array, dict, foreach, lappend, namespace, proc, puts, set, {*}, uplevel, uplevel, p>

    【讨论】:

    • 这真是一个很好的解释。我只是不明白:如果一切都只是传递一个字符串,而不是将对象拖过堆栈,那么如何安全地区分堆栈不同级别的同名局部变量?例如,如果 - 在您的解决方案中 - 'generate' 碰巧有一个名为 'containerB' 的局部变量,并且绑定到 container_name 的 containerB 也将是 proc 的本地变量,那么 'generate' 中的 eval 是如何知道的,应该使用两个不同的 containerB 变量中的哪一个?
    • @user1934428:这就是namespace which 的用途。解释很长,所以我把它放在答案中。
    猜你喜欢
    • 2016-07-12
    • 2010-12-08
    • 2011-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多