是否有关于函数应接受多少个参数的准则?


114

我注意到我使用的一些函数有6个或更多参数,而在大多数我使用的库中,很少会找到一个需要3个以上参数的函数。

通常,这些额外的参数中有很多是二进制选项,可以更改功能行为。我认为其中一些参数设置的函数可能应该重构。有多少个数字的准则吗?


4
@Ominus:这个想法是您想让您的课程集中注意力。重点关注的类通常没有太多的依赖项/属性,因此构造函数的参数较少。人们此时提出的一些流行语是高凝聚力单一责任原则。如果您认为没有违反这些规则并且仍然需要大量参数,请考虑使用Builder模式。
c_maker 2012年

2
绝对不要遵循MPI_Sendrecv()的示例,该示例需要12个参数!
chrisaycock 2012年

6
我当前正在从事的项目使用某个框架,在该框架中,具有10个以上参数的方法很常见。我在几个地方调用一个带有27个参数的特定方法。每次看到它,我都会在里面死一些。
2012年

3
切勿添加布尔开关来更改功能行为。拆分功能。将常见行为分解为新功能。
凯文·克莱恩

2
@Ominus什么?只有10个参数?没什么,还需要更多。:D
maaartinus

Answers:


106

我从未见过任何指导方针,但是以我的经验,使用超过三个或四个参数的函数表示以下两个问题之一:

  1. 该功能执行过多。应该将其拆分为几个较小的函数,每个函数具有较小的参数集。
  2. 那里还有另一个物体。您可能需要创建另一个包含这些参数的对象或数据结构。有关更多信息,请参见有关参数对象模式的本文。

没有更多信息,很难说出您正在寻找什么。您可能需要进行的重构是将函数拆分为一些较小的函数,这些函数根据当前传递给该函数的那些标志从父级调用。

这样做有一些好处:

  • 它使您的代码更易于阅读。我个人发现,阅读“规则列表” if要比用一个方法全部完成的结构要容易得多,“规则列表”由一个调用具有描述性名称的方法的结构组成。
  • 它可以进行单元测试。您已将问题分解为几个较小的任务,这些任务分别非常简单。然后,单元测试集合将由一个行为测试套件组成,该套件将检查通过主方法的路径以及每个单独过程的较小测试的集合。

5
参数抽象已经变成了设计模式?如果您有3个参数类,会发生什么情况。是否再添加9个方法重载来处理参数的不同可能组合?这听起来像一个讨厌的O(n ^ 2)参数声明问题。哦,等等,您只能在Java / C#中继承1个类,因此需要更多的biolerplate(也许更多的子类化)才能使其在实践中起作用。抱歉,我不相信。忽略一种语言可能会提供的支持复杂性的更具表现力的方法,这感觉是错误的。
Evan Plaice 2012年

除非您使用Pattern Object模式将变量包装在对象实例中并将其作为参数传递。这对于打包有效,但是可能只是为了简化方法定义而创建不相似变量的类。
Evan Plaice 2012年

@EvanPlaice我并不是说只要有多个参数就必须使用该模式-绝对正确,它比第一个列表还糟。在某些情况下,您确实确实需要大量参数,而将它们包装在一个对象中根本行不通。我还没有遇到一个企业发展中的案例,但这个案例并没有落入我在回答中提到的两个桶中的一个。这并不是说一个不存在。
Michael K

@MichaelK如果您从未使用过它们,请尝试使用Google搜索“对象初始值设定项”。这是一种相当新颖的方法,可显着减少清晰度。从理论上讲,您可以一次性消除类的构造函数,参数和重载。不过实际上,最好保留一个通用的构造函数,并在其余的晦涩/利基属性中依靠“对象初始值设定项”语法。恕我直言,它是您最接近动态类型语言在静态类型语言中的表现力的方法。
Evan Plaice 2012年

@Evain Plaice:自从什么时候可以动态表达语言?
ThomasX 2012年

41

根据“清洁代码:敏捷软件技巧手册”,理想情况是零,一个或两个是可以接受的,在特殊情况下为三个,四个或更多,从不!

作者的话:

函数的理想参数个数为零(尼拉度)。接下来是一个(单声道),紧接着是两个(双声道)。在可能的情况下,应避免使用三个参数(三重性)。超过三个(多义词)需要非常特殊的理由-因此无论如何都不应使用。

本书有一章只讨论了讨论大量参数的函数,因此我认为这本书可以很好地指导您需要多少参数。

在我个人看来,一个参数总比没有参数好,因为我认为更清楚发生了什么。

例如,在我看来,第二种选择更好,因为更清楚该方法正在处理什么:

LangDetector detector = new LangDetector(someText);
//lots of lines
String language = detector.detectLanguage();

LangDetector detector = new LangDetector();
//lots of lines
String language = detector.detectLanguage(someText);

关于许多参数,这可能表明某些变量可以分组为一个对象,或者在这种情况下,很多布尔值可以表示函数/方法在完成某件事上的作用,在这种情况下,更好地将这些行为中的每个行为重构为不同的功能。


8
“在特殊情况下,三个,四个或四个以上,永不!” BS。Matrix.Create(x1,x2,x3,x4,x5,x6,x7,x8,x9)怎么样??
Lukasz Madon '04

71
零是理想的吗?函数如何获取信息?全局/实例/静态/任何变量?UCK
Peter C

9
那是一个不好的例子。答案显然是:String language = detectLanguage(someText);。在任何一种情况下,您传递的参数数量都完全相同,只是由于语言不佳而将函数执行分为两部分。
Matthieu M.

8
@lukas,在语言支持这种奇特结构数组或(哇!)名单,怎么样Matrix.Create(input);在那里input的,比方说,一个.NET IEnumerable<SomeAppropriateType>?这样,你还当你想创建一个矩阵保存10个元素,而不是9,不需要单独的过载
一个CVN

9
零参数为“理想”是一个麻烦,也是我认为Clean Code被高估的原因之一。
user949300

24

如果应用程序中的域类设计正确,则传递给函数的参数数量将自动减少-因为这些类知道如何执行其工作,并且它们具有足够的数据来执行其工作。

例如,假设您有一个经理班级,要求三年级班级完成作业。

如果建模正确,

3rdGradeClass.finishHomework(int lessonId) {
    result = students.assignHomework(lessonId, dueDate);
    teacher.verifyHomeWork(result);
}

这很简单。

如果您没有正确的模型,则方法将如下所示

Manager.finishHomework(grade, students, lessonId, teacher, ...) {
    // This is not good.
}

正确的模型总是会减少方法调用之间的函数参数,因为正确的函数被委派给了自己的类(单一职责),并且它们具有足够的数据来完成其工作。

每当看到参数数量增加时,我都会检查模型以查看是否正确设计了应用程序模型。

但是,有一些例外:当我需要创建一个传输对象或配置对象时,在构造一个大的配置对象之前,我将首先使用一个生成器模式来生成一个小的生成的对象。


16

其他答案不会占用的一个方面是性能。

如果使用足够低级的语言(C,C ++,汇编语言)进行编程,则大量参数可能会对某些体系结构的性能造成极大的损害,尤其是在调用函数多次的情况下。

例如,当在ARM中进行函数调用时,前四个参数将放置到的寄存器r0r3,其余的参数必须被压入堆栈。将关键参数的数量保持在五个以下可以对关键函数产生很大的影响。

对于被称为非常频繁的功能,甚至认为程序有设置参数之前每次调用可能会影响性能(其实r0r3可以被调用的函数被覆盖,并且将在下次调用之前被替换),所以在这方面,零参数是最好的。

更新:

KjMag提出了有趣的内联主题。内联将以某种方式减轻这种情况,因为内联将使编译器执行与纯汇编编写时相同的优化。换句话说,编译器可以看到被调用函数使用了哪些参数和变量,并且可以优化寄存器使用率,从而使堆栈的读/写操作最小化。

但是内联有一些问题。

  1. 内联会导致编译后的二进制代码增长,因为如果从多个位置调用相同的代码,则会以二进制形式复制相同的代码。当涉及到I-cache使用时,这是有害的。
  2. 编译器通常只允许内联到某个级别(3个IIRC步骤?)。想象一下从内联函数中的内联函数调用内联函数。如果inline在所有情况下都将其视为强制性的,二进制增长将会爆炸。
  3. 有很多编译器inline在遇到它们时会完全忽略或实际上给您错误。

从性能的角度来看,传递大量参数是好是坏取决于选择。如果一种方法需要十几条信息,而其中的十一条将使用相同的值调用数百次,那么使用该方法采用数组可能要比使用十二个参数更快。另一方面,如果每个调用都需要唯一的一组十二个值,则为每个调用创建和填充数组比直接传递值容易慢。
supercat 2014年

内联不解决这个问题吗?
KjMag '17

@KjMag:是的,在一定程度上。但是有很多陷阱取决于编译器。函数通常只会内联到一定级别(如果您调用的内联函数调用了内联函数,内联函数又调用了内联函数...。)。如果函数很大并且在很多地方都被调用,则到处内联会使二进制文件更大,这可能意味着I高速缓存中的丢失次数更多。因此内联可以有所帮助,但这不是万灵丹。(更不用说有很多不支持的旧嵌入式编译器inline。)
Leo

7

当参数列表增加到五个以上时,请考虑定义“上下文”结构或对象。

这基本上是一个结构,其中包含所有可选参数以及一些合理的默认设置。

在C程序世界中,将使用简单的结构。在Java,C ++中,一个简单的对象就足够了。不要弄乱getter或setter方法,因为对象的唯一目的是保存“ public”可设置的值。


我同意,当函数参数结构开始变得相当复杂时,上下文对象可以派上用场。我最近写了一篇关于使用具有访问者模式的上下文对象的
Lukas Eder 2012年

5

不,没有标准指南

但是,有些技术可以使带有许多参数的函数更易于使用。

您可以使用list-if-args参数(args *)或Dictionary-of-args参数(kwargs **

例如,在python中:

// Example definition
def example_function(normalParam, args*, kwargs**):
  for i in args:
    print 'args' + i + ': ' + args[i] 
  for key in kwargs:
    print 'keyword: %s: %s' % (key, kwargs[key])
  somevar = kwargs.get('somevar','found')
  missingvar = kwargs.get('somevar','missing')
  print somevar
  print missingvar

// Example usage

    example_function('normal parameter', 'args1', args2, 
                      somevar='value', missingvar='novalue')

输出:

args1
args2
somevar:value
someothervar:novalue
value
missing

或者您可以使用对象文字定义语法

例如,这是一个JavaScript jQuery调用,用于启动AJAX GET请求:

$.ajax({
  type: 'GET',
  url: 'http://someurl.com/feed',
  data: data,
  success: success(),
  error: error(),
  complete: complete(),
  dataType: 'jsonp'
});

如果您看一下jQuery的ajax类,则可以设置很多(大约30个)更多的属性。主要是因为ajax通信非常复杂。幸运的是,对象文字语法使生活变得轻松。


C#intellisense提供了有效的参数文档,因此看到重载方法的非常复杂的排列并不少见。

像python / javascript这样的动态类型语言没有这种功能,因此,看到关键字参数和对象文字定义的情况要普遍得多。

我更喜欢使用对象文字定义(甚至在C#中)来管理复杂的方法,因为在实例化对象时,您可以显式查看正在设置的属性。您将需要做更多的工作来处理默认参数,但是从长远来看,您的代码将更具可读性。使用对象字面量定义,您可以摆脱对文档的依赖,从而乍看之下您的代码在做什么。

恕我直言,重载的方法被高估了。

注意:如果我没记错的话,只读访问控制应该适用于C#中的对象文字构造函数。它们的工作原理与在构造函数中设置属性相同。


如果您从未用过基于动态类型(python)和/或基于Java脚本的功能/原型的语言编写任何重要的代码,我强烈建议您尝试一下。这可能是一个启发性的经历。

首先,打破对功能/方法初始化的所有一切方法的参数的依赖可能会很可怕,但是您将学习到用代码做更多的事情而不必增加不必要的复杂性。

更新:

我可能应该提供示例来演示在静态类型语言中的用法,但是我目前不在静态类型上下文中考虑。基本上,我在动态类型的上下文中做了太多的工作,以至于突然间切换回去。

知道的是对象文字定义语法在静态类型的语言中(至少在C#和Java中)是完全可能的,因为我以前使用过它们。在静态类型的语言中,它们称为“对象初始化器”。以下是一些链接,显示了它们在JavaC#中的用法。


3
我不确定我喜欢这种方法,主要是因为您失去了各个参数的自我记录价值。对于类似项目的列表,这是很有意义的(例如,采用字符串列表并将它们连接起来的方法),但是对于任意参数集,这比长方法调用差。
Michael K

@MichaelK再看看对象初始化器。通过它们,您可以显式定义属性,而不是在传统方法/函数参数中隐式定义属性。请阅读msdn.microsoft.com/en-us/library/bb397680.aspx,以了解我的意思。
Evan Plaice 2012年

3
创建一个仅用于处理参数列表的新类型听起来就像不必要的复杂性的定义一样。当然,动态语言可以避免这种情况,但是您会得到一个goo参数。无论如何,这不能回答所提出的问题。
Telastyn

@Telastyn您在说什么?没有创建新类型,您可以直接使用对象文字语法声明属性。这就像定义一个匿名对象,但是该方法将其解释为key = value参数分组。您正在查看的是方法实例化(而不是参数封装对象)。如果您的牛肉带有参数包装,请查看其他问题之一中提到的Parameter Object模式,因为这正是它的含义。
Evan Plaice 2012年

@EvanPlaice-除了静态编程语言一般都需要声明(通常是新的)类型以允许使用参数对象模式。
Telastyn

3

就个人而言,我的代码气味警报触发的地方是2以上。当您将函数视为操作(即从输入到输出的转换)时,在一个操作中使用两个以上的参数并不常见。程序(即实现目标的一系列步骤)将需要更多的投入,有时是最好的方法,但如今在大多数语言中,这已不再是常态。

但这又是准则,而不是规则。由于异常情况或易于使用,我经常使用带有两个以上参数的函数。


2

就像Evan Plaice所说的那样,我非常喜欢在可能的情况下简单地将关联数组(或您的语言的类似数据结构)传递给函数。

因此,代替(例如)这样做:

<?php

createBlogPost('the title', 'the summary', 'the author', 'the date of publication, 'the keywords', 'the category', 'etc');

?>

去做:

<?php

// create a hash of post data
$post_data = array(
  'title'    => 'the title',
  'summary'  => 'the summary',
  'author'   => 'the author',
  'pubdate'  => 'the publication date',
  'keywords' => 'the keywords',
  'category' => 'the category',
  'etc'      => 'etc',
);

// and pass it to the appropriate function
createBlogPost($post_data);

?>

WordPress用这种方式做了很多事情,我认为它运作良好。(尽管我上面的示例代码是虚构的,本身并不是Wordpress的示例。)

这种技术使您可以轻松地将大量数据传递到函数中,而无需记住每个函数必须传递的顺序。

当需要重构时,您还将欣赏此技术-不必潜在地更改函数参数的顺序(例如,当您意识到需要传递“其他参数”时),而无需更改函数的参数列出所有。

这不仅使您不必重新编写函数定义,而且使您不必每次调用函数时都更改参数的顺序。那是一个巨大的胜利。


看到此发布会突出显示“哈希传递”方法的另一个好处:请注意,我的第一个代码示例很长,它会生成一个滚动条,而第二个代码示例恰好适合页面。在您的代码编辑器中可能也是如此。
克里斯·艾伦·莱恩

0

先前的答案提到一位可靠的作者,他说您的函数参数越少,您所做的越好。答案并没有解释原因,但书上对此做了解释,这是两个最令人信服的原因,因为您需要采用这种哲学,我个人也同意:

  • 参数属于与函数不同的抽象级别。这意味着您的代码读者将不得不考虑函数参数的性质和目的:这种想法比其相应函数的名称和目的“低级”。

  • 对函数使用尽可能少的参数的第二个原因是测试:例如,如果您有一个带有10个参数的函数,请考虑要为一个单元覆盖所有测试用例的参数组合的数量测试。更少的参数=更少的测试。


0

为了在Robert Martin的“ Clean Code:A Agile Software Craftsmanship手册”中为理想的函数参数个数为零的建议提供更多的背景信息,作者说了以下几点:

争论很难。他们具有很大的概念力。这就是为什么我从示例中删除了几乎所有它们的原因。例如,考虑StringBuffer示例中的。我们本可以将其作为参数传递,而不是将其作为实例变量,但是这样,我们的读者将不得不在每次看到它时对其进行解释。当您阅读模块讲述的故事时,includeSetupPage() 比容易理解includeSetupPageInto(newPageContent)。参数处于不同的抽象层次上,即函数名称会强制您知道一个细节(StringBuffer即),该细节在那时并不特别重要。

对于includeSetupPage()上面的示例,这是本章末尾重构的“干净代码”的一小段:

// *** NOTE: Commments are mine, not the author's ***
//
// Java example
public class SetupTeardownIncluder {
    private StringBuffer newPageContent;

    // [...] (skipped over 4 other instance variables and many very small functions)

    // this is the zero-argument function in the example,
    // which calls a method that eventually uses the StringBuffer instance variable
    private void includeSetupPage() throws Exception {
        include("SetUp", "-setup");
    }

    private void include(String pageName, String arg) throws Exception {
        WikiPage inheritedPage = findInheritedPage(pageName);
        if (inheritedPage != null) {
            String pagePathName = getPathNameForPage(inheritedPage);
            buildIncludeDirective(pagePathName, arg);
        }
    }

    private void buildIncludeDirective(String pagePathName, String arg) {
        newPageContent
            .append("\n!include ")
            .append(arg)
            .append(" .")
            .append(pagePathName)
            .append("\n");
    }
}

作者的“思想流派”主张使用小的类,较少的函数参数(理想情况下为0)和很小的函数。尽管我也不完全同意他的观点,但我发现它发人深省,并且我认为将零函数参数作为理想的想法值得考虑。另外,请注意,即使上面的小代码段也具有非零参数函数,所以我认为这取决于上下文。

(正如其他人指出的那样,他还认为,从测试的角度来看,更多的论点会变得更加困难。但是,在这里,我主要想强调上面的示例以及他对零函数论点的依据。)


-2

理想情况下为零。一两个可以,在某些情况下三个。
四个或更多通常是一个不好的做法。

除了其他人指出的单一责任原则之外,您还可以从测试和调试的角度来考虑它。

如果只有一个参数,则知道它的值,对其进行测试并使用它们查找错误相对“容易,因为只有一个因素。随着因素的增加,总的复杂性会迅速增加。对于一个抽象的例子:

考虑“在这种天气下穿什么”计划。考虑一下如何处理一个输入-温度。您可以想象,根据这一因素,穿什么衣服的结果非常简单。现在考虑如果程序实际上通过了温度,湿度,露点,降水等因素,程序可能/将/应该做什么。现在想象一下,如果程序给出了“错误”的答案,调试将有多么困难。


12
如果一个函数的参数为​​零,则它要么返回一个常数(在某些情况下有用,但有限制),要么使用某种隐藏状态,最好将其显式显示。(在OO方法调用中,上下文对象足够明确,不会引起问题。)
Donal Fellows

4
-1不引用源
约书亚·德雷克

您是否在认真地说,理想情况下所有函数都不要带参数?还是这夸张?
GreenAsJade 2014年

1
请参阅Bob叔叔的论点,网址为:notifyit.com/articles/article.aspx?p=1375308,请注意,他在底部说:“函数应包含少量论点。最好没有论点,其次是一,二和三。超过三个是非常有问题的,应避免产生偏见。”
2014年

我已经给出了来源。从那时起没有评论。我还尝试回答“指南”部分,因为许多人现在将Bob叔叔和Clean Code视为指南。有趣的是,(目前)巨大支持的最高答案表示没有任何指导方针。鲍勃叔叔无意成为权威,但实际上是这样,这个答案至少试图成为对问题细节的回答。
Michael Durrant
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.