代码风格(2)——函数

一、简短函数

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.函数
    [代码整洁之道]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352