如何从 Call_Procedure 检索输出关键字
14158 对此文章评分:
暂无评分
如何从 Call_Procedure 检索输出关键字
匿名 2015年2月26日,星期四
在 IDL 中工作时,常常会觉得有些事情就是无法实现。直到你与知道一些你从未想到过的语言技巧的合适人选交谈后,情况才会改变。就我而言,这发生在我为封装任何 IDL 过程而开发新的 ENVITask 子类时。我想使用 Call_Procedure 来调用它,但无法弄清楚如何从过程中获取输出关键字的值,因此不得不采用更灵活但更慢的 Execute() 方法。后来,我与 ENVI 原始版本创作者、全方位的 IDL 专家 Adam Lefkoff 进行了交谈,他向我展示了一个可以利用并与 Call_Procedure 配合使用的技巧。
在演示解决方案之前,让我先退一步更好地描述这个问题。假设有一个你想调用的 IDL 过程,但你只有作为字符串字面量的关键字名称,而不是在编译时拥有它们的先验知识。也许它们来自数据库或外部源。调用这个过程你有两种选择:Execute 和 Call_Procedure:
pro testProcedure, FOO=foo, BAR=bar, _REF_EXTRA=refExtra
compile_opt idl2
help, foo, OUTPUT=fooOut
help, bar, OUTPUT=barOut
print, fooOut, barOut
if (ISA(refExtra)) then begin
foreach keyword, refExtra do begin
value = Scope_VarFetch(keyword, /REF_EXTRA)
help, value, OUTPUT=valueOut
print, keyword, valueOut
endforeach
endif
end
pro Execute_Wrapper, procName, _REF_EXTRA=refExtra
compile_opt idl2
params = Hash()
foreach keyword, refExtra do begin
params[keyword] = Scope_VarFetch(keyword, /REF_EXTRA)
endforeach
print, 'in Execute_Wrapper'
command = procName + ', _EXTRA=params.ToStruct()'
ret = Execute(command)
end
pro Call_Procedure_Wrapper, procName, _REF_EXTRA=refExtra
compile_opt idl2
params = Hash()
foreach keyword, refExtra do begin
params[keyword] = Scope_VarFetch(keyword, /REF_EXTRA)
endforeach
print, 'in Call_Procedure_Wrapper'
Call_Procedure, procName, _EXTRA=params.ToStruct()
end
pro test_procedure_wrapping
compile_opt idl2
Execute_Wrapper, 'testProcedure', FOO='foo', BAR=!pi, BAZ=List(1,2), QUX=Hash(1,2)
Call_Procedure_Wrapper, 'testProcedure', FOO='foo', BAR=!pi, BAZ=List(1,2), QUX=Hash(1,2)
end
这个例子可能有点刻意,但它演示了如何 Scope_VarFetch() _REF_EXTRA 中的每个值并将它们放入 Hash 对象中。然后在调用过程时将 Hash 转换为结构体,并通过将该结构体分配给 _EXTRA 关键字,它将适当地连接所有关键字。我们从输出中可以看到 Execute 和 Call_Procedure 都能工作:
in Execute_Wrapper
FOO STRING = 'foo'
BAR FLOAT = 3.14159
BAZ VALUE LIST <ID=1 NELEMENTS=2>
QUX VALUE HASH <ID=6 NELEMENTS=1>
in Call_Procedure_Wrapper
FOO STRING = 'foo'
BAR FLOAT = 3.14159
BAZ VALUE LIST <ID=31 NELEMENTS=2>
QUX VALUE HASH <ID=36 NELEMENTS=1>
问题是这个过程只有输入关键字,没有输出关键字。如果我们希望能够动态调用具有输出关键字的过程,则需要付出更多的努力。以下是更新后的 Execute 包装器版本:
pro testProcedure, FOO=foo, BAR=bar, OUT_HASH=outHash, _REF_EXTRA=refExtra
compile_opt idl2
outHash = Hash('FOO', foo, 'BAR', bar)
if (ISA(refExtra)) then begin
foreach keyword, refExtra do begin
outHash[keyword] = Scope_VarFetch(keyword, /REF_EXTRA)
endforeach
endif
end
pro Execute_Wrapper, procName, OUT_NAMES=outNames, _REF_EXTRA=refExtra
compile_opt idl2
params = Hash()
foreach keyword, refExtra do begin
if (outNames.HasValue(keyword)) then continue
params[keyword] = Scope_VarFetch(keyword, /REF_EXTRA)
endforeach
print, 'in Execute_Wrapper'
command = procName + ', _EXTRA=params.ToStruct()'
foreach name, outNames do begin
command += ', ' + name.ToUpper() + '=' + name.ToLower()
endforeach
ret = Execute(command)
foreach name, outNames do begin
(Scope_VarFetch(name, /REF_EXTRA)) = Scope_VarFetch(name)
endforeach
end
pro test_procedure_wrapping
compile_opt idl2
execOutHash = 0
Execute_Wrapper, 'testProcedure', FOO='foo', BAR=!pi, BAZ=List(1,2), QUX=Hash(1,2), OUT_NAMES='OUT_HASH', OUT_HASH=execOutHash
print, execOutHash, /IMPLIED
end
Execute_Wrapper 例程现在需要知道 _REF_EXTRA 中的哪些关键字是输入,哪些是输出。因此,当我们构造 Hash 时,会省略输出关键字,但随后将它们作为局部作用域中的新变量附加到命令字符串中。调用过程后,这些局部变量将具有正确的输出值,但我们需要那行看起来非常奇怪的、包含两次 Scope_VarFetch() 调用的代码,将它们复制到 _REF_EXTRA 包中,以便将它们传递回调用 Execute_Wrapper 的例程。左边的 Scope_VarFetch() 调用使用 /REF_EXTRA 关键字与调用栈连接,但我们需要将其封装在括号中以使其成为赋值操作。右边的 Scope_VarFetch() 调用用于获取要赋值的命名局部变量的值。我们还需要确保 OUT_HASH 关键字存在于对 Execute_Wrapper 的调用中,以便这个 _REF_EXTRA 技巧能够奏效。
虽然这可以工作,但看起来有点复杂,并且 OUT_HASH 关键字作为字符串字面量重复出现,显得冗余。另一个问题是它仍然在使用 Execute(),效率不如 Call_Procedure。但我们不能以同样的方式修改 Call_Procedure_Wrapper 例程并取得成功。原因在于,当构建要传递给 Call_Procedure 的结构体时,它包含的是每个关键字值的副本,而不是指向用于构建它的变量的引用。因此,我们需要引入一个新的函数来获取输出关键字的值:
pro testProcedure, FOO=foo, BAR=bar, OUT_HASH=outHash, _REF_EXTRA=refExtra
compile_opt idl2
outHash = Hash('FOO', foo, 'BAR', bar)
if (ISA(refExtra)) then begin
foreach keyword, refExtra do begin
outHash[keyword] = Scope_VarFetch(keyword, /REF_EXTRA)
endforeach
endif
end
function Procedure_Wrapper, procName, _REF_EXTRA=refExtra
compile_opt idl2
Call_Procedure, procName, _EXTRA=refExtra
results = Hash()
foreach keyword, refExtra do begin
results[keyword] = Scope_Varfetch(keyword, /REF_EXTRA)
endforeach
return, results
end
pro Call_Procedure_Wrapper, procName, OUT_NAMES=outNames, _REF_EXTRA=refExtra
compile_opt idl2
params = Hash()
foreach keyword, refExtra do begin
params[keyword] = Scope_VarFetch(keyword, /REF_EXTRA)
endforeach
print, 'in Call_Procedure_Wrapper'
results = Procedure_Wrapper(procName, _EXTRA=params.ToStruct())
foreach name, outNames do begin
(Scope_VarFetch(name, /REF_EXTRA)) = results[name]
endforeach
end
pro test_procedure_wrapping
compile_opt idl2
callOutHash = 0
Call_Procedure_Wrapper, 'testProcedure', FOO='foo', BAR=!pi, BAZ=List(1,2), QUX=Hash(1,2), OUT_NAMES='OUT_HASH', OUT_HASH=callOutHash
print, callOutHash, /IMPLIED
end
这个新函数的行为类似于修改后的 Execute_Wrapper 例程,但由于它将其所有关键字都传递给 Call_Procedure,因此它使用的是引用而不是值。然后,它可以使用 Scope_VarFetch(/REF_EXTRA) 将这些关键字中的值复制到一个 Hash 中,并返回给其调用者。在那里,我们再次使用 Scope_VarFetch(/REF_EXTRA) 将该 Hash 中的输出值复制到其调用例程的变量中。使用 OUT_NAMES 关键字并非完全必要,但它避免了将输入变量复制回调用者,这可能会也可能不会带来开销。
在 ENVITask 上下文中,我们知道哪些关键字是输入,哪些是输出,因此,与其传入 OUT_NAMES 关键字并返回一个值的 Hash,不如将 Procedure_Wrapper 设为一个成员函数,并直接设置输出参数值。但正如你在 ENVI 5.2 SP1 中将会看到的,我们可以将任何使用关键字的 IDL 过程包装在一个 ENVITaskFromProcedure 对象中,该对象知道如何自动将输入和输出关键字映射到 ENVITaskParameters。