在8.4中工作的代码导致8.6中的崩溃,是否有更好的方法来实现功能

时间:2017-04-14 19:12:21

标签: tcl

我有一个Tcl实用程序,可以很容易地确保在控制流离开当前范围(proc)时运行一段代码。它在Tcl 8.6.6中崩溃,所以我想知道是否有一种“更好”的方法来实现Tcl 8.6中的功能?

示例用法是:

proc test {file} {
   set fh [open $file]
   ::Util::Defer [list close $fh]
   # ... do a bunch of stuff
   # and even if we hit an error
   # [close $fh] will be evaluated as we return
   # from the proc
}

它在Tcl 8.4中运行良好,我在代码中使用它。

由于我还在快速了解Tcl 8.6中提供的所有功能,我问如何编写::Util::Defer proc以最好地利用Tcl 8.6?

以下是8.4实现:

namespace eval ::Util {}
proc ::Util::Defer_impl {cmd args} {
  uplevel 1 $cmd
}
proc ::Util::Defer {cmd} {
  set vname _u_defer_var
  # look for a unique variable name
  while {[uplevel 1 [list info vars $vname]] != ""} {
    set vname ${vname}_
  }
  uplevel 1 [list set $vname $cmd]
  # when the variable is unset, trigger a call to the command
  uplevel 1 [list trace add variable $vname unset [list ::Util::Defer_impl $cmd]]
  # return a chunk of code enabling the user to cancel this if desired
  return [list variable $vname unset [list ::Util::Defer_impl $cmd]]
}

已编辑添加: 我很感激答案。说实话,我已经有了文件句柄的其他语法糖,这个:

proc test {file} {
   set fh [::Util::LocalFileHandle $file]
   # do stuff
}

我只是希望更多地使用::Util::Defer的通用解决方案 - 因为我偶尔在同一个proc中有两次或三次使用(在不同的位置)。是的,如果不存在或不可读,我将省略错误处理。

注意:我已向ActiveState报告错误并提交了bug at core.tcl.tk

编辑添加错误的代码:这是导致我崩溃的Tcl代码,它略微削弱了本质(而不是成为全面的::Util::Defer )。

# ---------------begin script-------------------
package require Itcl

proc ::try_uplevel {} {
  return [uplevel 1 [list ::info vars _u_defer_var]]
}

itcl::class ::test_class {
  constructor {} {}

  public proc test_via_proc {} {
    ::try_uplevel
  }
}

::test_class::test_via_proc
# ---------------end script-------------------

3 个答案:

答案 0 :(得分:1)

您描述的模式是受支持的模式;它不应该崩溃(实际上我无法用8.6.3或8.6支持分支的尖端重现崩溃)。它唯一的问题是,如果您在close(或任何其他延迟脚本)中出现错误,则无法报告该错误,正如您可以从此代码段中看到的那样(%是提示):

% apply {{} {
    ::Util::Defer [list error boo]
    puts hi
}}
hi
% 

这是为什么我花了很多精力在8.6中提供try命令的部分原因。有了它,你可以这样做:

proc test {filename} {
    set f [open $filename]
    try {
        # Do stuff with $f
    } finally {
        close $f
    }
}

它还会处理棘手的事情,例如在体内抛出的拼接错误和finally子句(身体异常信息位于-during子句的finally选项中&#39 ; s错误异常信息)所以,如果两个地方都有错误,你可以找到两者。

% catch {
    try {
        error a
    } finally {
        error b
    }
} x y
1
% puts $x
b
% puts $y
-errorstack {INNER {returnImm b {}}} -errorcode NONE -errorinfo {b
    while executing
"error b"} -errorline 5 -during {-code 1 -level 0 -errorstack {INNER {returnImm a {}}} -errorcode NONE -errorinfo {a
    while executing
"error a"} -errorline 3} -code 1 -level 0

就个人而言,我更倾向于写这个:

proc withreadfile {varName filename body} {
    upvar 1 $varName f
    set f [open $filename]
    try {
        return [uplevel 1 $body]
    } finally {
        close $f
    }
}

proc test {file} {
    withreadfile fh $file {
        # Do stuff with $fh
    }
}

您的里程可能会有所不同。

答案 1 :(得分:0)

未经测试的代码(这个确切的片段,我多次使用过此模式):

proc test file {
    try {
        open $file
    } on ok fh {
        # do stuff with fh
        # more stuff 
    } finally {
        catch {close $fh}
    }
}

应该差不多。无论您是否使用try结构处理错误,(或者您是否收到错误),finally子句中的代码都会在结束时运行。如果您希望能够取消操作,请在子句中使用简单的if

修改

如果有人想看到频道关闭时产生的任何错误,将它包装在catch中是一个坏主意,如果无法打开文件和频道ID,这是必要的变量未创建。替代方案包括:

  • 检查存在:if {[info exists fh]} {close $fh}
  • 传播结算错误:使用result的{​​{1}}和options变量名称参数。

答案 2 :(得分:0)

在周末,我想到了这个重量级的解决方案。它利用itcl::local功能来实现相同的效果。它确实依赖于Itcl - 但由于问题是与Itcl的交互,这似乎是一个合理的解决方案,即使它不是纯粹的Tcl。

itcl::class Defer_impl {
  constructor {cmd} {} {
    set _to_eval $cmd
  }
  destructor {
    uplevel 1 $_to_eval
  }
  private variable _to_eval {}
}

proc ::Util::Defer {cmd} {
  uplevel 1 [list itcl::local ::Defer_impl #auto $cmd]
}