IDL 编程中的微妙错误
原文链接: https://www.nv5geospatialsoftware.com/Learn/Blogs/Blog-Details/subtle-mistakes-in-idl-programming
14428 评价此文章:
暂无评分
IDL 编程中的微妙错误
匿名作者 2014年12月18日,星期四
在我多年的IDL编程生涯中,我犯过不少拼写错误、小疏忽和一些看似微小却最终导致严重问题的错误。有些错误,比如拼错的变量名,很容易发现。然而,其他一些错误则更难以追踪。有时需要花费数小时的调试,才能发现是在几百行代码之前造成的问题。
我整理了一份清单,列出了即使是经验丰富的程序员也容易犯的几种错误。其中一些在编写代码时可能并不明显,但它们可能会在下游导致意想不到的结果。在我的职业生涯中,我对所有这些错误都有切身体会。
1. 标量和单元素数组并不总是相同的。
IDL提供的一个便利是,它通常不关心一个变量是标量还是单元素数组。换句话说,IDL认为以下两个变量是相同的:
a = 1
b = [1]
IF a EQ b THEN PRINT, '它们是相同的。' ELSE PRINT, '它们是不同的。'
IDL会打印:
它们是相同的。
IDL的大部分功能都可以接受这两种形式,并且最终结果是相同的。因此,在编写代码时,我通常不会费心在将变量传递给另一个IDL调用之前检查它是标量还是数组。然而,这并非总是如此。
例如,尝试运行以下代码。这段代码创建一个包含三个条目的哈希,每个条目又是一个包含三个数值条目的哈希。然后,它查找包含字符串‘2’的键,并创建一个新变量,该变量被设置为哈希中对应键的字段值。
h = hash('h1', hash('a', 1, 'b', 2, 'c', 3), $
'h2', hash('d', 4, 'e', 5, 'f', 6), $
'h3', hash('g', 7, 'h', 8, 'i', 9))
keys = (h.Keys()).ToArray()
result = Where(StrMatch(keys, '*2*'))
h2 = h[keys[result]]
那么,新变量 h2 应该是一个包含三个键/值对的哈希,即 'd'=4, 'e'=5, 'f'=6,对吗?
help, h2
H2 HASH
等等,什么?
事实证明,WHERE 函数总是返回一个数组,即使只有一个结果。因此,用一个单元素数组去索引键,结果得到了一个单元素数组。当这个数组被传递到哈希的中括号中时,哈希对我的调用意图(作为程序员)做出了不同的解释。哈希认为,因为我提供了一个键的数组,所以我一定是想要一个按键缩小子集的哈希,而实际上我是在寻找哈希中单个字段的内容(关于如何“检索单个元素”与“检索多个元素并创建新哈希”,请参阅 哈希访问)。
WHERE函数的结果就是一个很好的例子,说明需要小心对待那些实际上是数组却被当作标量的变量。这个示例问题的简单解决方法是,通过索引第一个元素将结果转换为标量。
h2 = h[keys[result[0]]]
help, h2
H2 HASH
现在,我得到了预期的结果。
2. Catch, /CANCEL 不会清除 ERROR_STATE。
下面是一个假设的代码片段,它被放在例程的开头,用于捕获错误并调用错误恢复例程。从错误中恢复后,代码从该例程返回到调用例程。
Catch, err
if err ne 0 then begin
Catch, /CANCEL
my_error_recovery_procedure
return
endif
在这个代码块中,调用 Catch, /CANCEL 的目的是关闭紧接其上设置的 catch。这样,如果 my_error_recovery_procedure 中发生错误(可能与原错误无关),catch 块就不会进入抛出和捕获的无限循环。
如果这个示例中的错误恢复旨在清理并使我的程序恢复到干净状态以便继续运行,那么我就犯了没有清除 IDL 错误状态的错误。我曾经编写过一个作为大型程序一部分的例程,它有一个像这样的 catch 块。当时,我忘记了取消 catch 并不会重置 !ERROR_STATE。在这个大型程序的最后(数千行代码之后),其他人编写了检查 !ERROR_STATE 的代码,如果程序中报告了错误,就会弹出一个对话框消息。这导致了漫长的调试过程,以找出错误发生的位置。
解决这个问题的简单方法是,在返回之前调用 MESSAGE, /RESET,!ERROR_STATE 将被重置回其干净状态。
3. 发生错误时必须释放 LUN 或文件句柄。
啊,更多由错误处理引起的错误……
对于任何打开文件的子例程,拥有自己的 catch 块通常是个好主意。这样,就可以在 catch 块内关闭文件。如果文件被打开,在文件关闭前发生了错误,并且错误在子例程外部被捕获(即,如果该例程的调用者有自己的 catch 块),那么文件将被锁定,并且 IDL 的一个 LUN 将无限期地被占用。最终 IDL 将耗尽 LUN。
假设某个例程中包含以下调用:
OpenR, unit, file, /GET_LUN
下面是一个可以放在该例程开头的示例 catch 块。如果你不希望重置错误状态,则可以在释放 LUN 后重新抛出错误。
Catch, err
if err ne 0 then begin
Catch, /CANCEL
if N_Elements(unit) gt 0 then Free_Lun, unit
; 重新抛出错误。
Message, /REISSUE_LAST
endif
类似地,对于 HDF5 文件:
file_id = H5F_Open(file)
这是一个确保其被关闭的示例 catch 块:
Catch, err
if err ne 0 then begin
Catch, /CANCEL
if N_Elements(file_id) gt 0 then H5F_Close, file
; 重新抛出错误。
Message, /REISSUE_LAST
endif
4. IDL 会按引用传递
有些语言会区分变量是按值传递还是按引用传递。然而,在 IDL 中,情况并非如此。应该假设,如果一个参数在函数内部被修改,那么无论调用者是否预期,它都会得到这个修改。这对于参数和关键字都适用。
如果一个变量将在例程中被修改,但你不希望修改后的值传递到例程外部,那么你需要制作该变量的副本,像这样:
PRO myroutine, MY_KEYWORD=myKeywordIn
if Isa(myKeyword_In) then myKeyword = myKeywordIn
; 从这里开始,使用变量 "myKeyword",myKeywordIn 将不会被修改。
注意: 根据变量类型,有时你可能想使用 N_ELEMENTS 或其他形式的存在性检查,而不是简单的 ISA 函数,或者你可能想指定 ISA 的关键字。
这里有一个巧妙的方法——你可以使用 ARG_PRESENT 来检查变量的生命周期是否延伸到例程外部,如果没有,那么你可以“窃取”该值以节省内存,像这样:
PRO myroutine, MY_KEYWORD=myKeywordIn
if (Isa(myKeyword_In)) then myKeyword = Arg_Present(myKeywordIn) ? myKeywordIn : Temporary(myKeywordIn)
5. IDL 的路径中可能包含文件的过时版本
这是我以前遇到过的一个令人沮丧的经历:我运行了一些代码,这些代码调用了位于另一个文件中的子例程。出错了。等等,我已经在那个文件中修复了错误,为什么它仍然失败?我在文件中设置了断点进行调试,但 IDL 并没有在断点处停止。然后我意识到,IDL 正在运行该文件的另一个版本,该版本位于路径中的其他位置。
更令人困惑的是,该例程可能是从一个保存文件(.sav)中运行的。
有几件事可以帮助解决这种困惑。首先,IDL 有一个检查重复例程的偏好设置。在 IDL 的偏好设置对话框中,选择“IDL”类别,然后选择“路径”。接下来,勾选“当 IDL 路径上有重复例程时警告”复选框。现在,IDL 控制台的“问题”选项卡将显示例程何时重复以及该例程的不同版本存在于何处。如果你在处理一个特定的例程时遇到麻烦,可以在命令行调用 ROUTINE_INFO,提供例程名和 /SOURCE 关键字,IDL 会告诉你它调用该例程的源代码位置。
6. IDL 有时 区分大小写
IDL 的另一个便利之处是,大多数情况下它不区分大小写。例如,FOR 和 for 会被编译成相同的东西。ThisVariable 和 thisVariable 会被同等对待。N_ELEMENTS 或 N_Elements 也是如此。我个人喜欢这一点,因为我可以花更少的心力在语言本身,而更多地专注于我正在编写的逻辑和算法。
然而,IDL 偶尔也会区分大小写,如果大小写混淆,就会产生问题。例如,哈希键是区分大小写的(除非在创建哈希时设置了 /FOLD_CASE)。
h = hash('a', 1, 'A', 'Hello')
help, h['a']
help, h['A']
文件 I/O 是另一个需要考虑区分大小写的情况。例如,在 Windows 上,文件名不区分大小写,但在 Unix 上区分。