13子程序的参数
1. 如何使用子程序参数
子程序之间的接口是程序中最易出错的部分之一。研究发现,程序中有 39% 的错误都是属于内部接口错误——也就是子程序之间互相通信时所发生的错误。以下是一些可以减少接口错误的知道原则。
1.1 按照输入-修改-输出的顺序排列参数
不要随机地或按字母顺序排列参数,而应该先列出仅作为输入用途的参数,然后是既作为输入又作为输出用途的参数,最后才是仅作为输出用途的参数。这种排列方法暗含了子程序的内部操作所发生的顺序——先是输入数据,然后修改数据,最后输出结果。
//Ada示例代码
procedure InvertMatrix(
orignalMatrix: in Matrix;
resultMatrix: out Matrix
);
procedure PrintPageNumber(
pageNumber: in Integer;
status: out StatusType
);
这些排列规则与 C 函数库中所用的把会被修改的参数列在最前面的规则是不同的。然而对我来说,这种输入-修改-输出的排列规则更有意义,当然,如果你总是统一地采取某种排列规则的话,那么为代码的读者考虑,你还是延用自己的规则为好。
1.2 考虑自己创建 in 和 out 关键字
其他一些现代编程语言并没有像 Ada 那样支持 in 和 out 关键字。在使用这些语言的时候,你可能还是可以通过预处理指令来自己创建 in 和 out 关键字。
//C++ 示例
#define IN
#define OUT
void InvertMatrix(
IN Matrix originalMatrix,
OUT Matrix *resultMatrix
);
void PrintPageNumber(
IN int pageNumber,
OUT StatusType &status
);
在这里,IN 和 OUT 这两个宏关键字只是起说明性的作用。如果你想被调用的子程序修改某一个参数的值,那么还是通过指针或者引用参数来传递该参数。
在应用这一技术之前,请考虑它的以下两种显著弊端。
自行定义的 IN 和 OUT 关键字扩展了 C++ 语言,从而在某种程度上让多数阅读这一代码的人感到生疏。如果你以这种方式扩展所用的语言,请确保能够持续一致地使用该方法,最好是在整个项目的范围内。
第二个弊端在于编译器并不会强制 IN 和 OUT 关键字的使用,也就是说,你有可能把某个参数标记为 IN,但仍在子程序中修改了该参数的值。阅读代码的人可能会误认为有关代码是正确的,然而事实却并非如此。使用 C++ 中的 const 关键字来定义输入参数通常更为合适。
1.3 如果几个子程序都是用了类似的一些参数,应该让这些 参数的排列顺序保持一致
子程序的参数顺序可以产生记忆效应——不一致的顺序会让参数难以记忆。比如说在 C 语言中,fprint() 函数比 printf() 函数就对了一个放在开头的文件参数而已,其他都完全一样。而与之类似的函数 fputs(),它比 puts() 只多了一个放在最后的文件参数。本来应该很容易记住的参数,却因为这点儿毫无道理的可气的区别而变得难以记忆了。
而另一方面,C语言中的 strncpy() 函数接受的参数依次是目标字符串、源字符串和最大字节数,memcpy() 函数也是接受同样顺序相同的参数。这两个函数的相似性对记住函数中的参数不无裨益。
1.4 使用所有的参数
既然往子程序中传递了一个参数,就一定要用到这个参数。如果你不用它,就把它从子程序的接口中删去。未被用到的参数和出错率的增加不无关系。
这样去除未被使用的参数也有一个例外。如果你是在一定条件下编译某部分程序,那么久可能只是编译了子程序中使用某个参数的那一部分。对这种做法要慎重,但如果你确信这样可行,那么这样做也没有问题。总之,如果你有很好的理由不适用某个参数,那就继续留着它吧。相反,如果你没有很好的理由,那就应该花功夫来清理代码。
1.5 把状态或出错变量放在最后
按照习惯做法,状态变量和那些用于指示发生错误的变量应该放在参数表的最后。它们指示附属于程序的主要功能,而且它们是仅用于输出的参数,因此这是一种很有道理的规则。
1.6 不要把子程序的参数用作工作变量
把传入子程序的参数用做工作变量是很危险的。应该使用局部变量。
1.7 在接口中对参数的假定加以说明
- 参数是仅用于输入的、要被修改的、还是仅用于输出的
- 表示数量的参数的单位(英寸、英尺、米等)
- 如果没有用枚举类型的话,应说明状态代码和错误值的含义
- 所能接受的数值的范围
- 不该出现的特殊数值
1.8 把子程序的参数限定在大约 7 个以内
对于人的理解力来说,7 是一个神奇的数字。心理学研究发现,通常人类很难同时记住超过 7 个单位的信息。这一发现已经用于各种领域之中,因此,假定人不能同时记住超过约 7 个的子程序参数,也是合理的。
在 实践中,子程序中参数的个数到底应该限制在多少,取决于你所使用的编程语言如何支持复杂的数据类型。如果你使用的是一种支持结构化数据的现代编程语言,你就可以传递一个含有 13 个成员的合成数据类型,并将它看做一个大数据块。如果你使用的是一种更为原始的编程语言,那你可能需要分别传递全部 13 个成员。
如果你发现自己一直需要传递很多参数,这就说明子程序之间的耦合太紧密了。应该重新设计这个或这组子程序,降低其间的耦合度。如果你向很对不同的子程序传递相同的数据,就请把这些子程序组成一个类,并把那些经常使用的数据用做类的内部数据。
1.9 考虑对参数采用某种表示输入、修改、输出的命名规则
如果你觉得吧输入、修改、输出参数区分开很重要,那么就建立一种命名规则来对它们进行区分。你可以给这些参数名字加上 i_、m_、o_前缀。如果不嫌啰嗦,也可以用 Input_、Modify_、Output_来当前前缀。
1.10 为子程序传递用以维持其接口抽象的变量或对象
关于如何把对象的成员传给一个子程序这一问题,存在着两种互不相让的观点。比如说你有一个对象,它通过 10 个访问器子程序暴露其中的数据,被调用额子程序只需要其中的 3 项数据就能进行操作。
持第一种观点的人们认为,只应传递子程序所需的 3 项特定数据即可。他们的论据是,这样做可以最大限度地减少子程序之间的关联,从而降低其耦合度,使他们更容易读,更便于重用,等等。他们强调说,把整个对象传递给子程序就破坏了封装的原则,因为这样就是潜在地把所有 10 个访问器子程序都暴露给被调用的那个子程序了。
持第二种观点的人们则 认为应该传递整个对象。他们认为,如果在不修改子程序接口的情况下,让被调用子程序能够灵活使用对象的其余成员,就可以保持接口更稳定。他们争辩说,只传递 3 项特定的数据破坏了封装性,因为这样做就是把特定的数据项暴露给被调用的那个子程序了。
我认为这两种观点都过于简单,并没有击中问题的要害:子程序的接口要表达何种抽象?如果要表达的抽象是子程序期望 3 项特定的数据,但这 3 项数据只是碰巧由同一个对象提供的,那就应该单独传递这 3 项特定的数据。然而,如果子程序接口要表达的抽象是相一直拥有某个特定对象,且该子程序要对这一对象执行这样那样的操作,如果单独传递 3 项特定的数据,那就是破坏了接口的抽象。
如果你采用传递整个对象的做法,并发现自己是先创建对象,把被调用子程序所需要的 3 项数据填入该对象,在调用过子程序后又从对象中取出 3 项数据的值,那就是一个证据,说明你应该只传递那 3 项数据而不是整个对象。(一般来说,如果在调用子程序之前出现进行装配(set up)的代码,或者在调用子程序之后出现拆卸(take down)的代码,都是子程序设计不佳的表现)。
如果你发现自己经常需要修改子程序的参数表,而每次修改的 参数都是来自于同一个对象,那就说明你应该传递整个对象而不是个别数据项了。
1.11 使用具名参数
在某些语言中,你可以显式地把形式参数和实际参数对应起来。这使得参数的用法更具有自我描述性,并有助于避免因为用错参数而带来的错误。
1.12 确保实际参数与形式参数相匹配
形式参数也称为"哑参数",是指在子程序定义中声明的变量。实际参数是指在实际的子程序调用到的变量、常量或者表达式。
一个常见的错误是在调用子程序时使用了类型错误的变量——例如,在本该使用浮点类型的地方用了整型。(只有当你使用像 C 这样的弱类型编程语言,并且没有开启全部的编译器警告功能时,才会遇到这个问题;在C++或者Java这样的强类型语言中不存在该问题。)如果是仅用于输入的参数,这种情况很少会带来问题;编译器在把参数传递给子程序之前,通常会将实际类型转换成形式类型。如果有问题的haul,编译器通常会给出警告。但在某些情况下,特别是当所用的参数既用于输入也用于输出时,如果传错了参数类型,你就会遇上麻烦。