一、简短函数
1.1 总述
函数的第一规则是要简短,凝练。
1.2 说明
长函数有时是合理的,因此并不硬性限制函数的长度。但如果函数超过
40
行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。函数20行封顶最佳,最大不超过80
行,每行字符数不应超过120个。即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的Bug。使函数尽量简短,以便于他人阅读和修改代码。
在处理代码时,你可能会发现复杂的长函数。不要害怕修改现有代码:如果证实这些代码使用、调试起来很困难,或者你只需要使用其中的一小段代码,考虑将其分割为更简短并易于管理的若干函数。
if语句、else语句、while语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。这意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。
二、抽象层级
2.1 总述
每个函数只做一件事。
每个函数一个抽象层级。
2.2 说明
- 函数应该做一件事。做好这件事。只做这一件事。
代码清单2-1 HtmlUtil.java(FitNesse 20070619)
public static String testableHtml(
PageData pageData,
boolean includeSuiteSetup
) throws Exception {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage
);
if (suiteSetup != null) {
WikiPagePath pagePath =
suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName)
.append("\n");
}
}
WikiPage setup =
PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
if (setup != null) {
WikiPagePath setupPath =
wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName)
.append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardown =
PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .")
.append(tearDownPathName)
.append("\n");
}
if (includeSuiteSetup) {
WikiPagePath pagePath =
suitTeardown.getPageCrawler().getFullPath(suiteTeardown);
buffer.append("!include -teardown .")
.append(pagePathName)
.append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
代码清单2-2 HtmlUtil.java(重构之后)
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
代码清单2-1手忙脚乱,而代码清单2-2则只做一件简单的事。它将设置和拆解包纳到测试页面中。
代码清单2-2只做了一件事,对吧?其实也很容易看作是三件事:
(1)判断是否为测试页面;
(2)如果是,则容纳进设置和分拆步骤;
(3)渲染成HTML。
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还只是做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。代码清单2-1明显包括了处于多个不同抽象层级的步骤。
所以,要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。代码清单2-1违反了这条规矩。那里面有getHtml()等位于较高抽象层的概念,也有String pagePathName = PathParser.render(pagePath)等位于中间抽象层的概念,还有.append("\n")等位于相当低的抽象层的概念。
自顶向下读代码:向下规则(我们想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。)
三、函数参数
3.1 总述
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)。
3.2 一元函数
一元函数的普遍形式是有输入参数而无输出参数。对于转换,使用输出参数而非返回值令人迷惑。如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。
3.3 二元函数
二元函数不算恶劣,而且你当然也会编写二元函数。不过,你得小心,使用二元函数要付出代价。你应该尽量利用一些机制将其转换为一元函数。例如,可以把writeField方法写成outputStream的成员之一,从而能这样用:outputStream.writeField(name)。或者,也可以把outputStream写成当前类的成员变量,从而无需再传递它。还可以分离出类似FieldWriter的新类,在其构造器中采用outputStream,并且包含一个write方法。
3.4 参数顺序
- 函数的参数顺序为:输入参数在先,后跟输出参数。
- 输入参数通常是值参或者
const
引用,输出参数或输入、输出参数则一般为非const
指针。在排列参数顺序时,将所有的输入参数置于输出参数之前。特别要注意,在加入新参数时不要因为它们是新参数就置于参数列表最后,而是仍然要按照前述的规则,即将新的输入参数也置于输出参数之前。
3.5 引用参数
3.5.1 总述
所有按引用传递的参数必须加上const
。
3.5.2 定义
在C语言中,如果函数需要修改变量的值,参数必须为指针,如int foo(int *pval)
。在C++中,函数还可以声明为引用参数:int foo(int &val)
。
3.5.3 优点
定义引用参数可以防止出现(*pval)++
这样丑陋的代码。引用参数对于拷贝构造函数这样的应用也是必需的。同时也更明确地不接受空指针。
3.5.4 缺点
容易引起误解,因为引用在语法上是值变量却拥有指针的语义。
3.5.5 结论
函数参数列表中,所有引用参数都必须是const
:
void Foo(const string &in, string *out);
有时候,在输入形参中用const T*
指针比const T&
更明智。比如:
- 可能会传递空指针。
- 函数要把指针或对地址的引用赋值给输入形参。
总而言之,大多时候输入形参往往是const T&
。若用cosnt T*
则说明输入另有处理。所以若要使用cosnt T*
,则应给出相应的理由,否则会使得读者感到迷惑。
3.6 缺省参数
3.6.1 总述
只允许在非虚函数中使用缺省参数,且必须保证缺省参数的值始终一致。缺省参数与 函数重载 遵循同样的规则。一般情况下建议使用函数重载,尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下。
3.6.2 优点
有些函数一般情况下使用默认参数,但有时需要又使用非默认的参数。缺省参数为这样的情形提供了便利,使程序员不需要为了极少的例外情况编写大量的函数。和函数重载相比,缺省参数的语法更简洁明了,减少了大量的样板代码,也更好地区别了“必要参数”和“可选参数”。
3.6.3 缺点
缺省参数实际上是函数重载语义的另一种实现方式,因此所有 不应当使用函数重载的理由 也也都适用于缺省参数。
虚函数调用的缺省参数取决于目标对象的静态类型,此时无法保证给定函数的所有重载声明的都是同样的缺省参数。
缺省参数是在每个调用点都要进行重新求值的,这会造成生成的代码迅速膨胀。作为读者,一般来说也更希望缺省的参数在声明时就已经被固定了,而不是在每次调用时都可能会有不同的取值。
缺省参数会干扰函数指针, 导致函数签名与调用点的签名不一致. 而函数重载不会导致这样的问题.
3.6.4 结论
对于虚函数,不允许使用缺省参数,因为在虚函数中缺省参数不一定能正常工作。如果在每个调用点缺省参数的值都有可能不同,在这种情况下缺省函数也不允许使用。(例如,不要写像 void f(int n = counter++);
这样的代码。)
在其他情况下,如果缺省参数对可读性的提升远远超过了以上提及的缺点的话,可以使用缺省参数。如果仍有疑惑,就使用函数重载。
四、函数重载
4.1 总述
若要使用函数重载,则必须能让读者一看调用点就胸有成竹,而不用花心思猜测调用的重载函数到底是哪一种。这一规则也适用于构造函数。
4.2 定义
你可以编写一个参数类型为 const string&
的函数,然后用另一个参数类型为 const char*
的函数对其进行重载:
class MyClass {
public:
void Analyze(const string &text);
void Analyze(const char *text, size_t textlen);
};
4.3 优点
通过重载参数不同的同名函数,可以令代码更加直观。模板化代码需要重载,这同时也能为使用者带来便利。
4.4 缺点
如果函数单靠不同的参数类型而重载(acgtyrant 注:这意味着参数数量不变),读者就得十分熟悉 C++ 五花八门的匹配规则,以了解匹配过程具体到底如何。另外,如果派生类只重载了某个函数的部分变体,继承语义就容易令人困惑。
4.5 结论
如果打算重载一个函数,可以试试改在函数名里加上参数信息。例如,用 AppendString()
和 AppendInt()
等,而不是一口气重载多个 Append()
。如果重载函数的目的是为了支持不同数量的同一类型参数,则优先考虑使用 std::vector
以便使用者可以用 列表初始化 指定参数。
。
五、分隔指令与询问
5.1 总述
指令与询问分隔:函数要么修改其对象的状态,要么返回该对象的有关信息,一个函数只做一件事。
5.2 说明
public boolean set(String attribute, String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个属性则返回false。这样就导致了以下语句:
if (set("username", "unclebob"))...
从读者的角度考虑一下,这是什么意思呢?它是在问username属性值是否之前已设置为unclebob吗?或者它是在问username属性值是否成功设置为unclebob呢?从这行调用很难判断其含义,因为set是动词还是形容词并不清楚。
作者本意,set是个动词,但在if语句的上下文中,感觉它像是个形容词。该语句读起来像是说“如果username属性值之前已被设置为unclebob”,而不是“设置username属性为unclebob,看看是否可行,然后······”。要解决这个问题,可以将set函数重命名为setAndCheckIfExists,但这对提高if语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止混淆的发送:
if (attributeExists("username")) {
setAttribute("username", "unclebob");
···
}
• 由 Leung 写于 2019 年 4 月 30 日
• 参考:Google 开源项目风格指南——4.函数
[代码整洁之道]