为什么Tcler建议支持你的`expr`essions?

mad*_*dia 11 tcl

我们可以用两种可能的方式来评估这两个表达式:

   set a 1
   set b 1
   puts [expr $a + $b ]
   puts [expr {$a + $b } ]
Run Code Online (Sandbox Code Playgroud)

但是为什么第一个讨厌有经验的Tclers,并认为这是不好的做法?第一次使用是否expr有一些安全问题?

kos*_*tix 18

"问题" expr在于它实现了自己的"迷你语言",其中包括变量替换(用$a它们的值替换那些-s)和命令替换(用[command ...]运行commands 的结果替换那些东西),所以基本上评估的过程expr $a + $b如下:

  1. Tcl解释解析出四个字- ,,expr 和出来的源字符串.由于两者的这些话开始,变量替换发生这样真的会出现,,,和.$a+$b$expr1+2
  2. 由于通常情况下,第一个词被认为是一个命令的名字,而其他一些参数,所以Tcl解释查找一个名为命令expr,并执行它传递了三个参数:1,+2.
  3. 实施如果expr串接传递给它解释成字符串,得到一个字符串的所有参数1 + 2.
  4. 然后再次解析该字符串- 这次是expr机器,根据其自己的规则,包括变量和命令替换,如已经提到的.

以下内容:

  • 如果您支持您的expr会话,例如expr {$a + $b},那些花括号提供的分组会禁止Tcl解释器1对要由其expr自身解析的脚本进行解释.这意味着在我们的玩具示例中,expr命令将只看到一个参数$a + $b,并且将自己执行替换.
  • 上面解释的"双解析"可能会导致安全问题.

    例如,在以下代码中

    set a {[exec echo rm -rf $::env(HOME)]}
    set b 2
    expr $a + $b
    
    Run Code Online (Sandbox Code Playgroud)

    expr命令本身将解析一个字符串[exec echo rm -rf $::env(HOME)] + 2.它的评估将失败,但到那时,你的主目录的内容将被取消.(请注意,是一种Tcler放在echo前面rm在后面的编辑我的答案,企图挽救随机copypasters的脖子,这样写不会调用命令rm,但如果你删除echo的话,它会的.)

  • 双重解析禁止Tcl引擎在处理调用时可以做的某些优化expr.

1嗯,几乎 - "反斜杠+换行"序列仍然在{...}块内处理.

  • "某些优化"就像80%的代码大幅加速.(根据http://wiki.tcl.tk/10225) (2认同)
  • 表达越久,加速越快. (2认同)

Don*_*ows 12

它当然有安全问题.特别是,它会将变量的内容视为表达式片段而不是值,这样就可以解决所有类型的问题.如果这还不够,同样的问题也完全杀死了性能,因为没有办法为它生成合理的最佳代码:生成的字节码效率会低得多,因为它可以做的就是组装表达式字符串并将其发送到第二轮解析.

让我们深入了解细节

% tcl::unsupported::disassemble lambda {{} {
    set a 1; set b 2
    puts [expr {$a + $b}]
    puts [expr $a + $b]
}}
ByteCode 0x0x50910, refCt 1, epoch 3, interp 0x0x31c10 (epoch 3)
  Source "\n    set a 1; set b 2\n    puts [expr {$a + $b}]\n    put"
  Cmds 6, src 72, inst 65, litObjs 5, aux 0, stkDepth 6, code/src 0.00
  Proc 0x0x6d750, refCt 1, args 0, compiled locals 2
      slot 0, scalar, "a"
      slot 1, scalar, "b"
  Commands 6:
      1: pc 0-4, src 5-11          2: pc 5-18, src 14-20
      3: pc 19-37, src 26-46       4: pc 21-34, src 32-45
      5: pc 38-63, src 52-70       6: pc 40-61, src 58-69
  Command 1: "set a 1"
    (0) push1 0     # "1"
    (2) storeScalar1 %v0    # var "a"
    (4) pop 
  Command 2: "set b 2"
    (5) startCommand +13 1  # next cmd at pc 18
    (14) push1 1    # "2"
    (16) storeScalar1 %v1   # var "b"
    (18) pop 
  Command 3: "puts [expr {$a + $b}]"
    (19) push1 2    # "puts"
  Command 4: "expr {$a + $b}"
    (21) startCommand +14 1     # next cmd at pc 35
    (30) loadScalar1 %v0    # var "a"
    (32) loadScalar1 %v1    # var "b"
    (34) add 
    (35) invokeStk1 2 
    (37) pop 
  Command 5: "puts [expr $a + $b]"
    (38) push1 2    # "puts"
  Command 6: "expr $a + $b"
    (40) startCommand +22 1     # next cmd at pc 62
    (49) loadScalar1 %v0    # var "a"
    (51) push1 3    # " "
    (53) push1 4    # "+"
    (55) push1 3    # " "
    (57) loadScalar1 %v1    # var "b"
    (59) concat1 5 
    (61) exprStk 
    (62) invokeStk1 2 
    (64) done 
Run Code Online (Sandbox Code Playgroud)

特别是,查看地址30-34(汇编expr {$a + $b})并与地址49-61(汇编expr $a + $b)进行比较.最优代码从两个变量中读取值,只读取add它们; unbraced代码必须读取变量并与表达式的文字部分连接,然后触发结果exprStk,即"计算表达式字符串"操作.(字节码的相对数量不是问题;问题是运行时评估.)

对于如何根本性的这些差异可能是,考虑设置a1 || 0b[exit 1].在预编译版本的情况下,Tcl将尝试将双方视为要添加的数字(两者都不是实际数字;您将收到错误).在动态版本的情况下......好吧,你能通过检查预测它吗?

所以你会怎么做?

最佳Tcl代码应始终限制其执行的表达式的运行时评估量; 除非你正在做一些采用用户定义的表达式或类似的东西,否则你通常可以将它归结为零.你必须拥有它,尝试在变量中生成一个表达式字符串,然后使用expr $thatVar而不是更复杂的东西.如果您想要添加数字列表(或通常应用任何运算符来组合它们),请考虑使用此:

set sum [tcl::mathop::+ {*}$theList]
Run Code Online (Sandbox Code Playgroud)

代替:

set sum [expr [join $theList "+"]]
Run Code Online (Sandbox Code Playgroud)

(另外,永远不要使用动态表达式if,for否则while会抑制大量编译.)

请记住,使用Tcl(通常)安全代码是快速代码的情况.你想要快速安全的代码,对吧?