定义变量来命名方法参数是否是一种好习惯?


73

为了便于阅读,我经常发现自己在调用函数时定义了临时变量,例如以下代码

var preventUndo = true;
doSomething(preventUndo);

这个的简短版本是

doSomething(true);

但是,当我回到代码时,我常常想知道true所指的是什么。这种难题是否有约定?


5
您是否使用某种IDE?大多数将以某种方式表明所调用函数的参数是什么,从而使执行此操作的必要性无效。
马修·沙利

4
我不使用IDE,但是即使如此,我仍暗示该提示将需要某种操作(鼠标悬停或移至该行)。我只想看一下代码就知道代码在做什么。
methodofaction 2012年

11
如果语言支持,则可以使用例如doSomething( Undo.PREVENT )
James P.

4
但是您可以定义Undo = { PREVENT = true, DONT_PREVENT = false }。但是在JavaScript中,约定是那样做的:function myFunction( mandatoryArg1, mandatoryArg2, otherArgs ) { /*...*/ }然后myFunction( 1, 2, { option1: true, option2: false } )
xavierm02 2012年

6
绝对取决于语言。例如,如果这是python,我建议仅使用关键字参数,例如doSomething(preventUndo=True)
Joel Cornett

Answers:


117

解释变量

您的案例是介绍变量重构的介绍示例。简而言之,解释变量不是严格必需的变量,但为了提高可读性,可以使您对某些事物起清晰的名字。

高质量的代码可以将意图传达给读者。作为专业开发人员,可读性和可维护性是您的第一目标。

因此,我建议的经验法则是这样的: 如果您的参数的用途不是立即显而易见的,请随时使用变量来为其命名。 我认为这通常是个好习惯(除非被滥用)。这是一个快速的人为示例-考虑:

editButton.Enabled = (_grid.SelectedRow != null && ((Person)_grid.SelectedRow).Status == PersonStatus.Active);

相对更长一点,但可以说更清晰:

bool personIsSelected = (_grid.SelectedRow != null);
bool selectedPersonIsEditable = (personIsSelected && ((Person)_grid.SelectedRow).Status == PersonStatus.Active)
editButton.Enabled = (personIsSelected && selectedPersonIsEditable);

布尔参数

您的示例实际上突显了API中的布尔值通常不是一个好主意的原因 -在调用方,它们不解释发生的事情。考虑:

ParseFolder(true, false);

您必须查看这些参数的含义;如果它们是枚举,那就更清楚了:

ParseFolder(ParseBehaviour.Recursive, CompatibilityOption.Strict);

编辑:

添加了标题,并交换了两个主要段落的顺序,因为太多的人专注于布尔参数部分(公平地说,它最初是第一段)。在第一部分中还添加了一个示例。


28
除非您使用命名参数(.NET Framework中的命名参数),否则这种情况doSomething(preventUndo: true);就足够清楚了。另外,为您的API中的每个布尔值创建一个枚举吗?认真吗
阿森尼·穆尔坚科

12
@MainMa:如果您使用的语言不够酷,无法支持命名参数,则使用枚举是一个不错的选择。这并不意味着您应该在任何地方系统地引入枚举。
乔治(Giorgio)

18
@MainMa-要回答这个问题,重点实际上是您不应该在API中使用布尔值(至少是公共的)。您需要更改为命名参数语法,在编写本文时,它可能不是惯用语言,在任何情况下,布尔参数几乎总是表示您的方法完成了两件事,并且允许调用者执行以下操作:选择。但是我的答案是,在代码中使用解释变量没有什么错;这实际上是一个很好的做法。
丹尼尔·B

3
它是C#语言的永久组成部分,还需要什么“主流接受”?即使对于尚不了解该构造的人(这意味着您的C#开发人员甚至都没有阅读过C#文档),它的作用也很明显。
Nate CK 2012年

3
这就是为什么MS会发布它们在每个版本中添加的所有新内容的摘要,我认为每两年阅读其中的一篇文章并不花很多精力:msdn.microsoft.com/zh-我们/图书馆/bb383815%28v=vs.100%29.aspx
Nate CK

43

不要编写不需要的代码。

如果您觉得doSomething(true)难以理解,则应该添加评论:

// Do something and prevent the undo.
doSomething(true);

或者,如果语言支持,则添加参数名称:

doSomething(preventUndo: true);

否则,请依靠您的IDE为您提供被调用方法的签名:

在此处输入图片说明

有用的情况

放置其他变量可能很有用:

  1. 出于调试目的:

    var productId = this.Data.GetLastProductId();
    this.Data.AddToCart(productId);
    

    可以在同一行中编写相同的代码,但是如果您想在将产品添加到购物车之前放置一个断点以查看产品ID是否正确,则最好编写两行而不是一行。

  2. 如果您有一个包含大量参数的方法,并且每个参数都来自求值。在一句话中,这可能变得完全不可读。

    // Even with indentation, this is unreadable.
    var doSomething(
        isSomethingElse ? 0 : this.getAValue(),
        this.getAnotherOne() ?? this.default,
        (a + b + c + d + e) * f,
        this.hello ? this.world : (this.hello2 ? this.world2 : -1));
    
  3. 参数的评估是否过于复杂。我看过的一个代码示例:

    // Wouldn't it be easier to have several if/else's (maybe even in a separate method)?
    do(something ? (hello ? world : -1) : (programmers ? stackexchange : (com ? -1 : 0)));
    

为什么在其他情况下不呢?

为什么在简单情况下不应该创建其他变量?

  1. 不是因为性能影响。对于初学者开发人员正在微优化其应用程序的情况,这将是一个非常错误的假设。在大多数语言中,性能不会受到影响,因为编译器将内联变量。在那些编译器不执行的语言中,通过手动内联它可能会花费几微秒的时间,这是不值得的。不要那样做

  2. 但是由于存在将名称分离的风险,因此您可以将变量赋予参数名称。

    例:

    假设原始代码是:

    void doSomething(bool preventUndo)
    {
        // Does something very interesting.
        undoHistory.removeLast();
    }
    
    // Later in code:
    var preventUndo = true;
    doSomething(preventUndo);
    

    后来,开发人员doSomething注意到最近撤消历史记录引入了两种新方法:

    undoHistory.clearAll() { ... }
    undoHistory.disable() { ... }
    

    现在,preventUndo似乎不是很清楚。这是否意味着只能采取最后的行动?也许这意味着用户将无法再使用撤消功能?还是撤消历史记录会被清除?更清晰的名称是:

    doSomething(bool hideLastUndo) { ... }
    doSomething(bool removeAllUndo) { ... }
    doSomething(bool disableUndoFeature) { ... }
    

    现在,您有了:

    void doSomething(bool hideLastUndo)
    {
        // Does something very interesting.
        undoHistory.removeLast();
    }
    
    // Later in code, very error prone, while the signature of the method is clear:
    var preventUndo = true;
    doSomething(preventUndo);
    

枚举呢?

其他一些人建议使用枚举。它解决了眼前的问题,却创造了更大的难题。让我们来看看:

enum undoPrevention
{
    keepInHistory,
    prevent,
}

void doSomething(undoPrevention preventUndo)
{
    doTheJob();
    if (preventUndo == undoPrevention.prevent)
    {
        this.undoHistory.discardLastEntry();
    }
}

doSomething(undoPrevention.prevent);

问题在于:

  1. 代码太多了。if (preventUndo == undoPrevention.prevent)?认真吗?我不想if每次都写这样的。

  2. 如果我在其他地方使用相同的枚举,那么在枚举中添加元素非常诱人。如果我这样修改它,该怎么办:

    enum undoPrevention
    {
        keepInHistory,
        prevent,
        keepButDisable, // Keeps the entry in the history, but makes it disabled.
    }
    

    现在会发生什么?doSomething方法会按预期工作吗?为了防止这种情况,从一开始就需要以这种方式编写此方法:

    void doSomething(undoPrevention preventUndo)
    {
        if (![undoPrevention.keepInHistory, undoPrevention.prevent].contains(preventUndo))
        {
            throw new ArgumentException('preventUndo');
        }
    
        doTheJob();
        if (preventUndo == undoPrevention.prevent)
        {
            this.undoHistory.discardLastEntry();
        }
    }
    

    使用布尔值的变体看起来非常漂亮!

    void doSomething(bool preventUndo)
    {
        doTheJob();
        if (preventUndo)
        {
            this.undoHistory.discardLastEntry();
        }
    }
    

3
“代码太多。if(preventUndo == undoPrevention.prevent)?认真吗?!我不想每次都写这样的ifs。”:使用一个好的老式switch语句怎么样?另外,如果您使用布尔值,则无论如何都需要一个if语句。老实说,您对这一点的批评对我来说似乎有点人为。
乔治

1
我在这里缺少替代解决方案。您是在说他应该坚持一无是处的对与错,并依靠IDE(他不使用它)吗?注释可能会像变量名一样消失。
确保

2
@superM:我发现更改仅用于调用的临时变量的名称并不困难。如果枚举名称更改了,那就更好了,因为调用代码将不再运行,并且会向您的脸上抛出错误。如果该函数的功能由枚举扩展进行了根本性的更改,那么无论如何,您都必须重新访问所有调用以获取进一步的正确性,否则您将根据希望而不是根据逻辑进行编程。甚至没有考虑改变值,例如否定preventUndoallowUndo在签名...
固定

4
确实,@ superM使注释与代码保持同步非常容易出现问题,因为编译器在搜索不一致时没有帮助。
2012年

7
“ ...依靠您的IDE为您提供被调用方法的签名”-这在您编写代码时很有用,但在阅读时却没有那么多。当我阅读一个类的代码时,我只想看一下就知道它在做什么。如果必须依靠IDE来提高可读性,则您的代码不是自记录文件。
Phil

20

都不是,您不应该编写带有布尔标志的方法。

用罗伯特·C·马丁(Robert C. Martin)的话说,“干净的代码(ISBN-13 978-0-13-235088-4)”中说:“将布尔值传递给函数是一种非常糟糕的做法。”

用他的话来解释,理由是您有一个正确/错误的开关这一事实意味着您的方法很可能在做两种不同的事情(即“用撤消做某事”和“用撤消做某事”),因此应该分为两个不同的方法(可能在内部调用同一件事)。

DoSomething()
{...

DoSomethingUndoable()
{....

7
按照这种逻辑,我的单个HotDog构造函数将需要大约16种不同的方法:HotDogWithMustardRelishKrautOnion(),HotDogWithMustardNorelishKrautOnion(),等等。或者,它可能是一个简单的HotDogWithOptions(int options),在其中我将这些选项解释为一个位字段,但随后我们回到一种可以执行一些据称不同的操作的方法。因此,如果我选择第一个选项,是否必须编写一个大的条件以根据原本应该是布尔参数的情况来选择要调用的构造函数?而如果将q使用一个int,这方面的一个未接Q.
迦勒

4
阅读所有答案并查看我的代码后,我得出的结论是这是正确的方法。doSomethingUndoable()可以捕获更改,然后致电doSomething()
methodofaction 2012年

1
因此,如果我具有执行大量处理的功能并且可以选择在完成后发送电子邮件,那么我应该没有布尔型参数可以阻止电子邮件的发送,我应该创建一个单独的函数吗?
yakatz

2
@Caleb在这种情况下,我要么做一个热狗,然后一一添加配料,要么以强类型语言传递一个对象HotDogSettings,在其中设置所有布尔标志。我不会保留15个不同的true / false标志,这将是一场噩梦。请记住,代码是关于维护而不是编写的,因为那是应用程序成本的80%。VAR HD = MakeHotDog(); hd.Add(洋葱); ...
尼克B.

2
@Caleb我认为区别在于您的示例使用布尔值作为实际数据字段,而大多数使用布尔值来驱动执行流程;因此鲍勃叔叔的栏杆对此表示反对。另外,他的建议有些偏激-本书的后几行建议不要使用两个以上的参数,这可能是一个更极端的立场。虽然有疯狂的方法。您完全正确,尽管它不能回答问题,但我在自己的回答中犯了一个错误。
丹尼尔·B

5

当您查看两个类以了解它们之间的耦合程度时,其中一数据耦合,它指的是一个类中的代码,该类调用另一类的方法并仅传递数据,例如您想要报告的月份,另一个类别是控制耦合,调用方法以及传入一些控制方法行为的信息。我在课堂上使用的示例是一个verbose标志或一个reportType标志,但这preventUndo也是一个很好的示例。正如您刚刚演示的那样,控制耦合使读取调用代码和了解正在发生的事情变得困难。它还使调用代码容易受到更改的影响,doSomething()因为使用同一标志来控制撤消和存档,或者向doSomething() 控制归档,从而破坏您的代码,依此类推。

问题是代码耦合太紧密。在我看来,将布尔值传递给控制行为是API错误的征兆。如果您拥有API,请对其进行更改。两种方法,doSomething()并且doSomethingWithUndo(),效果会更好。如果您不拥有它,请在自己拥有的代码中自己编写两个包装器方法,并让其中一个调用doSomething(true),另一个调用doSomething(false)


4

定义参数的角色不是调用者的角色。我总是选择内联版本,如果有疑问,请检查被调用函数的签名。

变量应以它们在当前作用域中的作用来命名。否将它们发送到的范围。


1
变量在当前作用域中的作用是说明其用途。我不认为这与OP对临时变量的使用有何抵触。
superM 2012年

4

我在JavaScript中要做的是让函数将对象作为唯一的参数,如下所示:

function doSomething(settings) {
    var preventUndo = settings.hasOwnProperty('preventUndo') ? settings.preventUndo : false;
    // Deal with preventUndo in the normal way.
}

然后调用:

doSomething({preventUndo: true});

哈!!!我们俩都制定了doSomething(x)方法!大声笑...直到现在我都没有注意到您的帖子。
blesh 2012年

如果我传递字符串“ noUndo”会发生什么,使用签名的人在不考虑实现的情况下,应该清楚应该包含哪些设置?
尼克B.

1
记录您的代码。那就是其他人所做的。它使阅读代码变得更加容易,编写只需执行一次。
ridecar2 2012年

1
@NickB。约定很强大。如果您的大多数代码都接受带有参数的对象,那么这很清楚。
orip 2012年

4

我喜欢利用语言功能来帮助自己清晰化。例如,在C#中,您可以按名称指定参数:

 CallSomething(name: "So and so", age: 12, description: "A random person.");

在JavaScript中,由于这个原因,我通常更喜欢使用options对象作为参数:

function doSomething(args) { /*...*/ }

doSomething({ name: 'So and so', age: 12, description: 'A random person.' });

不过那只是我的偏爱。我想这将很大程度上取决于所调用的方法,以及IDE如何帮助开发人员理解签名。


1
这是松散类型语言的“缺点”。如果不进行某些验证,就无法真正执行签名。我不知道您可以在JS中添加类似的名称。
James P.

1
@JamesPoulson:您可能确实知道您可以用JS做到这一点,但没有意识到。这一切都结束了在JQuery的:$.ajax({url:'', data:''})等等等等
blesh

真正。乍一看似乎很不寻常,但它与某些语言中可能存在的内联伪对象相似。
James P.

3

我发现这是最简单易读的内容:

enum Undo { ALLOW, PREVENT }

doSomething(Undo u) {
    if (Undo.ALLOW == u) {
        // save stuff to undo later.
    }
    // do your thing
}

doSomething(Undo.ALLOW);

MainMa不喜欢那样。我们可能不得不同意不同意,或者我们可以使用Joshua Bloch提出的解决方案:

enum Undo {
    ALLOW {
        @Override
        public void createUndoBuffer() {
            // put undo code here
        }
    },
    PREVENT {
        @Override
        public void createUndoBuffer() {
            // do nothing
        }
    };

    public abstract void createUndoBuffer();
}

doSomething(Undo u) {
    u.createUndoBuffer();
    // do your thing
}

现在,如果您添加Undo.LIMITED_UNDO,则除非实现createUndoBuffer()方法,否则代码不会编译。并且在doSomething()中没有if(Undo.ALLOW == u)。我已经完成了这两种方法,第二种方法非常笨重,并且当Undo枚举扩展到页面和代码页时有点难以理解,但这确实让您思考。我通常坚持使用更简单的方法,用一个2值枚举替换一个简单的布尔值,直到我有理由要更改为止。当我添加第三个值时,我将使用IDE来“查找用法”并修复所有问题。


2

我认为您的解决方案使您的代码更具可读性,但我会避免定义一个额外的变量只是为了使您的函数调用更清晰。另外,如果您确实想使用额外的变量,则在您的编程语言支持的情况下,我会将其标记为常量。

我可以想到两个不涉及额外变量的替代方案:

1.使用额外的评论

doSomething(/* preventUndo */ true);

2.使用二值枚举而不是布尔值

enum Options
{
    PreventUndo,
    ...
}

...

doSomething(PreventUndo);

如果您的语言支持枚举,则可以使用替代项2。

编辑

当然,如果您的语言支持,也可以使用命名参数。

关于枚举所需的额外编码,对我而言,这似乎可以忽略不计。代替

if (preventUndo)
{
    ...
}

你有

if (undoOption == PreventUndo)
{
    ...
}

要么

switch (undoOption)
{
  case PreventUndo:
  ...
}

即使输入更多一些,也请记住该代码只编写一次并且可以读取多次,所以现在值得再次输入一些代码,以便在六个月后找到更具可读性的代码。


2

我同意@GlenPeterson所说的话:

doSomething(Undo.ALLOW); // call using a self evident enum

但也会令人困惑,因为在我看来只有两种可能性(true或false)

//Two methods    

doSomething()  // this method does something but doesn't prevent undo

doSomethingPreventUndo() // this method does something and prevents undo

2
好主意-我没想到。当您变得更复杂时,如doSomething(String,String,String,boolean,boolean,boolean),则命名常量是唯一合理的解决方案。好吧,乔什·布洛赫(Josh Bloch)提出了一个工厂和构造函数对象,如下所示:MyThingMaker ThingMaker = MyThingMaker.new(); somethingMaker.setFirstString(“ Hi”); somethingMaker.setSecondString(“ There”); 等等...事物t = ThingMaker.makeIt(); t.doSomething(); 那只是更多的工作,所以最好值得。
GlenPeterson

后一种方法的一个问题是编写一个使用doSomething但允许调用者选择是否允许撤消选择的方法变得很尴尬。补救措施可能是使重载采用Boolean选项,但也有包装器版本,该包装器版本必须使用参数true或false来调用以前的版本。
2012年

@GlenPeterson工厂是可能的,并且可以在Javascript中实现(由OP提及)。
James P.

2

由于您主要是在询问javascript,因此通过使用coffeescript,您可以拥有一个命名参数,例如语法,超级简单:

# declare method, ={} declares a default value to prevent null reference errors
doSomething = ({preventUndo} = {}) ->
  if preventUndo
    undo = no
  else
    undo = yes

#call method
doSomething preventUndo: yes
#or 
doSomething(preventUndo: yes)

编译为

var doSomething;

doSomething = function(_arg) {
  var preventUndo, undo;
  preventUndo = (_arg != null ? _arg : {}).preventUndo;
  if (preventUndo) {
    return undo = false;
  } else {
    return undo = true;
  }
};

doSomething({
  preventUndo: true
});

doSomething({
  preventUndo: true
});

http://js2coffee.org/上找到一些乐趣,尝试一下可能性


仅在需要避免javascript的OOP疯狂时才使用coffeeScript。这是一个很好的解决方案,我可能会在单独编码时使用,很难为同事证明我正在传递一个对象而不是布尔值,以提高可读性!
methodofaction 2012年

1

只是对于不太传统的解决方案,并且由于OP提到了Javascript,因此可能使用关联数组来映射值。与单个布尔值相比,优点是您可以根据需要选择任意数量的参数,而不必担心它们的顺序。

var doSomethingOptions = new Array();

doSomethingOptions["PREVENTUNDO"] = true;
doSomethingOptions["TESTMODE"] = false;

doSomething( doSomethingOptions );

// ...

function doSomething( doSomethingOptions ){
    // Check for null here
    if( doSomethingOptions["PREVENTUNDO"] ) // ...
}

我意识到这是具有强烈类型背景的人的反映,可能不那么实际,但出于独创性考虑。

另一种可能性是拥有一个对象,并且在Javascript中也是可能的。我不是100%确信这在语法上是正确的。查看诸如Factory之类的模式以获取更多灵感。

function SomethingDoer( preventUndo ) {
    this.preventUndo = preventUndo;
    this.doSomething = function() {
            if( this.preventUndo ){
                    // ...
            }
    };
}

mySomethingDoer = new SomethingDoer(true).doSomething();

1
我认为最好用点符号(doSomethingOptions.PREVENTUNDO)代替数组访问符号。
圣保罗Ebermann

1

您的问题和其他答案的重点是提高在调用函数时的可读性,这是我同意的重点。任何特定的准则,例如“没有布尔参数”事件,都应始终以此为目的,而不应成为自己的目标。

我认为有必要指出,当编程语言支持命名参数/关键字参数时(例如在C#4和Python中),或者在方法名称中插入了方法参数的地方(例如在Smalltalk或Objective-C中),这个问题通常可以避免。

例子:

// C# 4
foo.doSomething(preventUndo: true);
# Python
foo.doSomething(preventUndo=True)
// Objective-C
[foo doSomethingWith:bar shouldPreventUndo:YES];

0

应该避免将布尔变量作为参数传递给函数。因为函数应该一次做一件事。通过传递布尔变量,该函数具有两种行为。这也为以后的阶段或其他倾向于查看您的代码的程序员带来了可读性问题。这也会在测试功能时产生问题。在这种情况下,可能必须创建两个测试用例。想象一下,如果您有一个具有switch语句并根据switch类型更改其行为的函数,那么您必须为其定义许多不同的测试用例。

每当遇到函数的布尔参数时,他们都必须对代码进行调整,以使它们根本不编写布尔参数。


因此,您将要在两个函数中转换一个带有布尔参数的函数,而这两个函数仅在其代码的一小部分上有所不同(原始代码会带有if(param) {...} else {...})?
圣保罗Ebermann

-1
int preventUndo = true;
doSomething(preventUndo);

一个适用于低纬度语言(例如C ++)的良好编译器将优化上面两行的机器代码,如下所示:

doSomething(true); 

因此,性能无关紧要,但会提高可读性。

使用变量也很有帮助,以防以后会变成变量并且也可以接受其他值。

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.