只调用一次的单行函数


120

考虑一个无参数(无需编辑)函数,该函数执行一行代码,并且在程序中仅被调用一次(尽管将来再次使用它并非不可能)。

它可以执行查询,检查某些值,执行涉及正则表达式的操作……任何晦涩或“ hacky”的操作。

其背后的理由是避免难以理解的评估:

if (getCondition()) {
    // do stuff
}

getCondition()单行函数在哪里。

我的问题很简单:这是一种好习惯吗?对我来说似乎不错,但我不知道长期的事情...


9
除非您的代码片段来自带有隐式接收器的OOP语言(例如,此代码),否则这肯定是一个坏习惯,因为getCondition()最有可能依赖于全局状态...
Ingo

2
他可能是想暗示getCondition()可以有参数?
user606723 2011年

24
@Ingo-有些东西确实具有全局状态。当前时间,主机名,端口号等均为有效的“全局”。设计错误使“全球”从本质上不是全球的东西中脱颖而出。
James Anderson

1
为什么不只是内联getCondition?如果它像您说的那样小且不经常使用,那么给它起个名字并不能完成任何事情。
davidk01 2011年

12
davidk01:代码可读性。
wjl 2011年

Answers:


240

取决于那一行。如果该行本身可读且简洁,则可能不需要该功能。简单的例子:

void printNewLine() {
  System.out.println();
}

OTOH,如果函数为包含例如复杂且难以阅读的表达式的代码行取了好名,那么(对我而言)它是完全合理的。人为的示例(为了便于阅读,分成多行):

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

99
+1。这里的魔术词是“自我记录代码”。
Konamiman 2011年

8
鲍勃叔叔称之为“封装条件”的一个很好的例子。
Anthony Pegram

12
@Aditya:taxPayer在这种情况下,没有什么是全球性的。此类可能是TaxReturn,并且taxPayer是一个属性。
亚当·罗宾逊

2
另外,它允许您以一种可由Javadoc拾取的方式公开记录该功能。

2
@dallin,鲍勃·马丁(Bob Martin)的“干净代码”(Clean Code)显示了许多半现实的代码示例。如果单个类中的函数太多,那么您的类太大了吗?
彼得Török

67

是的,可以用来满足最佳实践。例如,最好是使用一个命名明确的函数来完成某些工作,即使它只有一行的长度,也比将一行代码包含在一个较大的函数中并需要用一行注释来说明其功能更好。同样,相邻的代码行应以相同的抽象级别执行任务。一个反例是这样的

startIgnition();
petrolFlag |= 0x006A;
engageChoke();

在这种情况下,最好将中间线移动到一个合理命名的函数中。


15
0x006A是可爱的:DA众所周知的带有添加剂a,b和c的碳化燃料常数。
编码员

2
+1我全都是自记录代码:)顺便说一句,我想我同意通过块保持恒定的抽象级别,但是我无法解释原因。您介意扩大吗?
vemv 2011年

3
如果您只是在petrolFlag |= 0x006A;没有任何决策的情况下说,最好是简单地说petrolFlag |= A_B_C; 没有其他功能。大概engageChoke()只有在petrolFlag满足特定条件时才应调用它,并且应明确说出“我在这里需要一个功能”。只是个小问题,这个答案基本上可以在其他地方找到:)
Tim Post

9
+1指出方法中的代码应处于相同的抽象级别。@vemv,这是一个好习惯,因为它避免了在阅读代码时在抽象级别之间跳来跳去的需要,从而使代码更易于理解。将抽象级别的开关绑定到方法调用/返回(即结构化“跳转”)是使代码更流畅和更干净的好方法。
彼得Török

21
不,这是完全虚构的价值。但是,您对此感到疑惑的事实只是强调了这一点:如果代码已经说过mixLeadedGasoline(),您就不必这样做!
Kilian Foth 2011年

37

我认为在许多情况下,这样的函数是好的样式,但是当您不需要在其他地方的其他地方使用此条件时,可以考虑使用局部布尔变量作为替代。

bool someConditionSatisfied = [complex expression];

这将为代码阅读器提供提示,并避免引入新功能。


1
如果条件函数名称比较困难或可能引起误解(例如IsEngineReadyUnlessItIsOffOrBusyOrOutOfService),则布尔值特别有用。
dbkk 2011年

2
我觉得这个建议是个坏主意:bool 状态分散了对功能核心业务的注意力。此外,更喜欢局部变量的样式使重构更加困难。
狼”

1
@Wolf OTOH,我更喜欢此方法而不是函数调用,以减少代码的“抽象深度”。IMO,跳转到函数是比显式和直接的几行代码更大的上下文切换,尤其是如果它仅返回布尔值时。
卡切,2016年

@Kache我认为这取决于您是否编写面向对象的代码,在OOP中,成员函数的使用使设计更加灵活。这确实取决于上下文...
Wolf

23

除了Peter的回答外,如果将来可能需要更新该条件,则可以通过将其封装为建议的方式,即只拥有一个编辑点。

按照彼得的例子,如果这

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

变成这个

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isMutant() 
        && (taxPayer.getNumberOfThumbs() > 2 
        || (taxPayer.getAge() > 123 && taxPayer.getEmployer().isXMan()));
}

您只需进行一次编辑,它就会被普遍更新。在可维护性方面,这是一个加号。

在性能方面,大多数优化的编译器都会删除函数调用并内联小代码块。优化这样的事情实际上可以缩小块的大小(通过删除函数调用,返回等所需的指令),因此即使在可能会阻止内联的条件下,这样做通常也是安全的。


2
我已经意识到保持单个编辑点的好处-这就是为什么问题说“ ...被调用一次”的原因。尽管如此,很高兴了解这些编译器优化,我一直认为它们确实遵循了此类说明。
vemv 2011年

拆分测试简单条件的方法往往意味着可以通过更改方法来更改条件。如果确实如此,则这种暗示很有用,但如果并非如此,则可能很危险。例如,假设代码需要将事物划分为轻量级对象,绿色重对象和非绿色重对象。对象具有快速的“ seemsGreen”属性,该属性对于重的对象是可靠的,但对于轻量的对象可能会返回误报。它们还具有“ measureSpectralResponse”功能,该功能虽然很慢,但将对所有对象可靠地工作。
supercat 2014年

seemsGreen如果代码从不关心轻量级对象是否为绿色,则与轻量级对象不可靠的事实无关紧要。但是,如果“轻量级”的定义发生变化,以致某些未返回真值的非绿色对象seemsGreen不会报告为“轻量级”,那么对“轻量级”定义的这种更改可能会破坏测试对象的代码是“绿色”。在某些情况下,与单独的方法相比,在代码中一起测试绿色和重量可以使测试之间的关系更清晰。
supercat 2014年

5

除了可读性(或作为其补充),这还允许在适当的抽象级别上编写函数。


1
恐怕我不明白您的意思……
vemv 2011年

1
@vemv:我认为他的意思是“适当的抽象级别”,您不会最终得到混合了不同抽象级别的代码(即Kilian Foth已经说过的话)。当周围的代码正在考虑对当前情况的更高级了解(例如,运行引擎)时,您不希望代码处理看似无关紧要的细节(例如,燃料中所有键的形成)。前者会使后者混乱,因为您将不得不随时考虑所有抽象级别,从而使您的代码难以阅读和维护。
Egon

4

这取决于。有时最好将表达式封装在函数/方法中,即使只有一行。如果阅读起来很复杂,或者您需要在多个地方阅读它,那么我认为这是一个好习惯。从长远来看,由于您已经引入了单点更改和更好的可读性,因此维护起来更容易。

但是,有时只是您不需要的东西。如果该表达式无论如何都易于阅读和/或仅出现在一个位置,则不要包装它。


3

我认为,如果您只有少数几个,那就可以了,但是当您的代码中有很多它们时,就会出现问题。而当编译器运行或插入器(取决于您使用的语言)时,它将进入内存中的该函数。因此,假设您有3个,我认为计算机不会注意到,但是如果您开始拥有100个这些小东西,则系统必须在内存中注册仅被调用一次且不会销毁的功能。


根据Stephen的回答,这可能并不总是会发生(尽管盲目地相信编译器的魔力还是不好的)
vemv 2011年

1
是的,应根据许多事实将其清除。如果它是一种插补的语言,则系统每次都将使用单行功能,除非您为高速缓存安装了某些东西,但仍然无法解决问题。现在,当涉及到编译器时,仅在虚弱的一天和刨刃的位置发生问题才重要,如果编译器将成为您的朋友并清除您的小麻烦,或者它是否会思考您是否真的需要它。我想提醒一下,如果您知道循环的确切时间,则总是将其运行好一些,只需复制并粘贴多次,然后循环即可。
WojonsTech 2011年

+1对准行星:)但您的最后一句话听起来对我来说完全是疯了,您真的做到了吗?
vemv 2011年

这实际上取决于大多数情况下,不,除非我获得正确的付款金额以检查它是否确实加速了诸如此类的速度,否则我不会这样做。但是在较早的编译器中,最好先将其复制并粘贴,然后让编译器将其找出9/10。
WojonsTech 2011年

3

我刚刚在重构的应用程序中做了这件事,以明确代码的实际含义(不带注释):

protected void PaymentButton_Click(object sender, EventArgs e)
    Func<bool> HaveError = () => lblCreditCardError.Text == string.Empty && lblDisclaimer.Text == string.Empty;

    CheckInputs();

    if(HaveError())
        return;

    ...
}

只是好奇,那是什么语言?
vemv 2011年

5
@vemv:在我看来像C#。
Scott Mitchell

与注释相比,我也更喜欢使用额外的标识符。但这是否真的与引入局部变量以保持if简短不同?这种局部(lambda)方法使PaymentButton_Click函数(整体上)更难以阅读。该lblCreditCardError在你的例子似乎是一个成员,所以也HaveError就是一个(私人)谓词是有效的对象。我倾向于对此表示反对,但是我不是C#程序员,所以我拒绝了。
狼”

@Wolf嘿,是的。我很早以前就写了这篇文章:)如今,我做事的方式肯定与现在完全不同。实际上,查看标签内容以查看是否有错误会使我感到畏缩……为什么我不只是从CheckInputs()??? 返回布尔值?
joshperry 2015年

0

将这一行移到一个命名良好的方法中可使您的代码更易于阅读。许多其他人已经提到了这一点(“自我记录代码”)。将其移至方法中的另一个优点是,它使单元测试更加容易。当将其隔离到自己的方法中并进行了单元测试时,您可以确定是否/当发现错误时,将不在此方法中。


0

已经有很多好的答案,但是有一个特殊情况值得一提

如果您的单行语句需要注释,并且您能够清楚地标识(即:命名)其用途,请考虑在提取注释的同时将函数增强为API文档。这样,您可以使函数调用更轻松,更容易理解。

有趣的是,如果当前无事可做,可以做同样的事情,但是有一条注释提醒您需要扩展(在不久的将来1))),因此,

def sophisticatedHello():
    # todo set up
    say("hello")
    # todo tear down

可以改成这个

def sophisticatedHello():
    setUp()
    say("hello")
    tearDown()

1)您应该对此非常确定(请参阅YAGNI原理)


0

如果语言支持,我通常使用带标签的匿名函数来完成此任务。

someCondition = lambda p: True if [complicated expression involving p] else False
#I explicitly write the function with a ternary to make it clear this is a a predicate
if (someCondition(p)):
    #do stuff...

恕我直言,这是一个很好的折衷方案,因为它为您提供了可读性的好处,即if避免复杂的表达式使条件混乱,同时又避免了带有小的一次性标签的全局/软件包名称空间的混乱。它的另一个好处是,函数“ definition”位于正确的位置,因此可以轻松修改和读取定义。

它并不仅是谓词函数。我也喜欢将重复的样板包含在这样的小函数中(它对于生成Pythonic列表特别有效,而且不会弄乱括号的语法)。例如,在python中使用PIL时,以下过度简化的示例

#goal - I have a list of PIL Image objects and I want them all as grayscale (uint8) numpy arrays
im_2_arr = lambda im: array(im.convert('L')) 
arr_list = [im_2_arr(image) for image in image_list]

为什么要使用“ lambda p:如果[涉及p的复杂表达式]否,则为true”,而不是“ lambda p:[涉及p的复杂表达式]”?8
汉斯·彼得·斯托尔2011年

@hstoerr其在该行下方的注释中。我想明确指出我们someCondition是一个谓词。尽管这完全没有必要,但我写了很多科学脚本,但编写的代码不那么多,我个人认为,具有额外的简洁性比让同事感到困惑更合适,因为他们不知道这些[] == False或其他内容并非总是“直观”的类似pythonic等价物。实际上,它实际上是标记someCondition是谓词的一种方式。
crasic 2011年

只是为了澄清我明显的错误[] != False,但[]转换为布尔时是假的
crasic

@crasic:当我不希望读者知道[]的值为False时,我宁愿这样做len([complicated expression producing a list]) == 0,而不是使用True if [blah] else False仍然需要读者知道[]的值为False的方法。
Lie Ryan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.