我发布了几个问题,这些问题最终涉及稳健性的各个方面,这些问题是在其他问题的背景下出现的,每次都有可能混淆问题的要点。我认为在这里集中讨论一下需要考虑什么才能实现稳健性,以及人们可以尽早做哪些事情来最大限度地减少以后升级自己的代码的痛苦是合适的。
最近有一场关于向后兼容性的讨论,有人表达了复杂的情绪,认为某位作者放弃向后兼容性是否为用户做了正确的事情,所以这显然是一个重要的话题。讨论这个话题时,最好不要对任何一位作者对最佳实践的应用发表意见,而只讨论原则本身。
作为一名初出茅庐的软件包编写者,我正在编写一个软件包,并且出现了各种问题,这些问题基本上与代码的稳健性有关。它是否能正确访问全局变量,是否可以在任何上下文中使用,或者在不破坏代码的情况下,对它可以在何处使用的合理期望是什么?
当我输入此问题时,出现的其他建议问题都没有集中在这个特定问题上。我不断想出一些与此主题相关的小问题,这些问题会被埋没在评论问题中,并会作为单独的问题在各处传播讨论。希望这被认为是一个有价值的元讨论。
答案可以是一般原则,也可以是具体使用示例,例如:\pgfmathsetlength
可以设置以 为前缀的变量\global
,而\pgfmathaddtolength
不能。前一种情况是最佳实践吗?后一种情况必须使用 使值全局化。
\pgfmathsetlength{\testvar}{0}\global\testvar\testvar
以防万一这种情况将来发生变化?我想无论争论如何,坚持不懈的实践必须是一个重要的指导原则。
答案1
好的,下面是我的行动:
- 使用 LaTeX3。调用
texdoc expl3
并texdoc interface3
阅读。这是我见过的第一次尝试以系统、全面和完整的范围处理 LaTeX 的界面设计。 - 使用 LaTeX3。
- 使用 LaTeX3。
- (请注意,我并不完全了解 LaTeX3 提供的具体功能,主要是因为我不是那种注重稳定性的人,而更像是一个裸机爱好者。因此,下面的每一条建议都可以简单地用“使用 LaTeX3”来代替。读完文档后,你会比我更了解。)
- 不要为软件包的用户级宏选择太常见的名称。TeX 具有全局名称空间。即使像 这样的命令
\newcommand
有助于避免冲突,但您可能无法让用户将您的软件包与另一个软件包一起使用。如果可行,请使用软件包的名称作为所有用户级宏的名称组成部分。 如果一个宏只能在另一个宏的参数中(或在环境内部)合理使用,则在本地定义它。
例如,如果你定义一个这样的环境
\begin{diagram} \line... \circle... \arc... \end{diagram}
或命令,
\mycommand[\option{foo}{...}\option{bar}{...}]{...}
则应仅在环境内部定义,并且仅在解析的可选参数时定义。\line
\circle
\arc
diagram
\option
\mycommand
一些包仍然给出全局定义,它将在正确的上下文之外生成错误消息,但如果宏名非常通用(如我的例子所示),最好完全保留它们以避免与其他包冲突。
一定要不惜一切代价避免内部宏的名称冲突(与内核或其他软件包)。始终使用软件包独有的名称组件!(LaTeX3 很可能会处理这个问题)。
- 如果加载其他包或类,请始终使用第二个可选参数指定最低发布日期。
- 切勿依赖其他包或类的内部宏。指定发布日期不会阻止加载更高版本,并且向后兼容性永远不会适用于内部宏。
- 永远不要依赖 LaTeX 内核的内部宏。虽然它比大多数软件包更稳定,但可能会发生变化。
- 如果您确实需要来自其他地方的内部宏,那么最好复制代码(但这当然高度依赖于上下文)。
- 如果您需要修补来自其他软件包的宏,请确保采取最低限度的侵入性措施。该软件包提供了用于此目的的
etoolbox
命令\patchcmd
、(也许 LaTeX3 也有一些用于此目的的命令)。\apptocmd
pretocmd
- 如果你需要修补另一个软件包中的宏,请确保你的补丁能够正确执行。我上面提到的命令会给出补丁是否可以应用的反馈;用它!在极端情况下,我有时会将要修补的命令逐字复制到我自己的包中(当然是用另一个名字),以预先检查
\ifx
定义是否仍然相同。 - 永远不要做
\global\somemacro
、、、、、或\immediate\somemacro
其他极度依赖于其他宏的内部结构的事情。位于赋值前面的 belong;如果你需要它,至少打个补丁\protected\somemacro
。\long\somemacro
\unless\somemacro
\the\somemacro
-\somemacro
\global
- 不要重新发明轮子。诸如此类的软件包
etoolbox
(LaTeX3 软件包中可能有很多此类软件包)提供了处理常见情况(如数字比较)的工具,这些工具比自定义的临时宏更加灵活和强大。 - 注意
type
您正在处理的数据。创建脆弱代码的可能性是无穷无尽的,例如,无论数字参数是以数字序列、计数器寄存器的名称、包含某个数字的宏还是数字表达式的形式给出,您的宏的行为都可能不同。最好只是用 括起来以防\numexpr#1\relax
万一。 - 妥善处理用户错误。同样,可能性太多,无法在此一一列出。示例:如果您期望参数具有特定格式(例如,cs 名称或长度),则可以通过某些方法测试输入是否符合要求。同样,该软件包
etoolbox
提供了有用的工具。 - 如果您使用 构造 cs 名称
\csname
,请格外小心。使用\ifcsname
可以检查即将定义的控制序列是否已存在。请格外小心,不要(无意中)覆盖某些内容。如果您要维护一个字母列表,并且#1
包含字母的名称,那么通过定义,\csname let#1\endcsname
如果由于某些用户错误而为空,您可能会破坏整个系统#1
。此外,请确保构造内部的组件\csname
完全扩展为可接受的字符序列。 - 关闭您的组和 if。没有什么错误比忘记
\fi
或更难发现\endgroup
。熟悉代码格式约定,这样可以轻松发现所有结构都是完整的。 - 永远,永远,永远为宏提供正确数量的参数。最难发现的一个错误是,调用宏时参数太少,导致从输入流中吞噬一些完全随机的标记并将其插入到完全意想不到的位置。
- 确保将宏包装
\loop
在一个组中。您不希望在循环中调用的任何宏包含未分组的循环。这可不是什么好事。 - 不要使用花哨的分隔参数技巧。分隔参数对于解析一些奇怪的语法很有用,但不要用它们来设计酷炫的宏接口。这种东西在用户错误下很容易崩溃,而且很难正常恢复。想想这种“酷炫用户界面”类型的键值解析总是出错。坚持使用标准的 LaTeX 传递参数的方式。
- 尽可能使用本地分配。一般来说,您希望宏是可重入的。
- 如果您编写了一个宏,该宏将生成可能导致分页符的文本,请记住代码是在页眉和页脚中执行的。当列表或部分标题应放在页眉或页脚中时,您可能会对可能出现的奇怪效果感到惊讶,因为它们会进行一些全局分配(作者无疑认为这是安全的)。
- 确保数字和维度/粘合表达式正确终止。例如
\hskip
,TeX 会继续扩展内容,直到完全确定粘合表达式已完成。这包括维度以及可能跟随的plus
and/orminus
组件。我曾经写过一个宏,plus
如果该单词不小心跟在单词后面,它就会吞掉该单词 - 很难发现!此外,“仅仅”导致相邻内容过早扩展的宏可能会导致各种无法解释的行为。用显式空格或甚至 来终止此类表达式\relax
。 - 要小心使用
\edef
,因为其中不可扩展的未受保护的代码可能会中断。\write
及其派生词,如或 属于同一类别。LaTeX 提供了 、 、\typeout
等\addtocontens
保护机制\DeclareRobustCommand
,\protect
但在扩展上下文中仍然应该非常小心地处理不可扩展的东西(还要注意不可扩展的东西进入 TeX 正在扩展东西以寻找输入的上下文,例如当 正在寻找数字时)。使用 e-TeX,您可以定义一个宏,这样它就不会在或其他 TeX 正在构建扩展标记列表的上下文中扩展。但是,它将在所有“完全”扩展的东西的地方扩展,例如在寻找数字时。有关更多信息,请参阅 e-TeX 手册( )。(感谢@cgnieder 的提示)\protected@edef
\protected@write
\ifnum
\protected\def
\edef
texdoc etex
- 如果您希望“仅对某些内容进行分组”,则使用
\begingroup
...更为可靠,\endgroup
因为\bgroup
...\egroup
(与{
...基本相同}
)用于许多其他目的(例如框分隔符),尤其是{
...}
可以与参数分隔符等混淆,从而隐藏结构错误。如果您仅使用\begingroup
...\endgroup
,则在出现结构错误时更有可能收到错误消息。 - 测试期间务必通读
log
文件,不要忽略任何错误消息或警告。如果您收到消息\end occurred inside a group
或\end occurred before \ifx was complete
其他信息,则表明您的代码结构严重损坏,会给其他人带来麻烦。 - 使用 LaTeX3。
- 使用 LaTeX3。
- 使用 LaTeX3。(为了更好)
请注意,我自己并不使用 LaTeX3,但通过观察项目进展,我知道稳健性和清晰的界面设计是该项目的核心考虑因素,并且那些已经发布的软件包已经足够完整和复杂,它们为开发稳健的软件包提供了比任何其他基于 TeX 的开发环境更优越的基础。
所有其他建议(除了第一条和最后三条:)都源自我日常的 TeX/LaTeX2 编程经验,可能适用于任何类型的 TeX 编程(尽管 LaTeX3 可能会通过提供更具体的工具来设计简洁的界面,从而使其中一些建议过时)。
答案2
以下是一般推荐的一些基本做法。它们都遵循管理任何复杂软件项目的一般原则:采取措施使组件在设计时尽可能保持独立(“松散耦合”) 及其实施情况。
保护 TeX 命名空间。由于 TeX 只有一个持久命名空间(全球的),包应该非常小心它们使用的宏名。
a. 我见过一些软件包无条件地将
\a
、\b
、\c
等定义为“快捷方式”,而完全不担心它们可能会踩到其他软件包的快捷方式。这种事情永远不应该默认做。它应该(如果有的话)通过非默认软件包选项、显式命令或在软件包提供的环境中完成。b. 使用特定于包的前缀,如
ld@
内部命令名称。避免
catcode
改变。它们特别容易在随后加载其他软件包时触发不兼容问题,这对最终用户来说既神秘又令人沮丧(即使他们对此非常熟悉)。Catcode 实际上是命名空间的低级部分,因为例如,与是@/11
不同的符号。@/12
补丁宏
\footnotetext
而不是直接替换它们。例如,修改宏的原因有很多。有许多好的解决方案可以使修补变得简单且(相对)安全。使用为软件包作者提供的文件加载命令,而不是相应的用户空间版本。例如,使用
\RequirePackage
而不是\usepackage
。(说实话,不确定这样做有什么好处,但它可以让生态系统变得更加清洁。)要求所含软件包的最低版本(必要时)。
使用 LaTeX 结构而不是相应的 TeX 基元。这在用户空间中也是一种很好的做法。(当然,除非 LaTeX 构造缺少原始构造所需的功能:例如,
\def
比 更强大、更灵活\newcommand
。)