如果可以,是否应该消除局部变量?


95

例如,要在Android中保持CPU开启,我可以使用以下代码:

PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "abc");
wakeLock.acquire();

但我认为局部变量powerManagerwakeLock可以消除:

((PowerManager)getSystemService(POWER_SERVICE))
    .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakelockTag")
    .acquire();

类似的场景出现在iOS警报视图中,例如:from

UIAlertView *alert = [[UIAlertView alloc]
    initWithTitle:@"my title"
    message:@"my message"
    delegate:nil
    cancelButtonTitle:@"ok"
    otherButtonTitles:nil];
[alert show];

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    [alertView release];
}

至:

[[[UIAlertView alloc]
    initWithTitle:@"my title"
    message:@"my message"
    delegate:nil
    cancelButtonTitle:@"ok"
    otherButtonTitles:nil] show];

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    [alertView release];
}

如果仅在范围中使用一次,则消除局部变量是否是一个好习惯?


95
不必要。有时,它使使用一次性变量的代码更加清晰,并且在大多数体面的编程语言中,这些附加变量的运行时成本很少(如果有的话)。
罗伯特·哈维

75
这也使单步执行带有调试器的代码更加困难。而且您还必须确保(取决于语言)第一个表达式不是NULL还是错误。
GrandmasterB

78
不,不是。实际上,优良作法是引入局部变量来中断方法调用的链。生成的机器代码可能是相同的,并且几乎可以保证源代码更具可读性,因此更好。
Kilian Foth

48
试试看 现在,插入调试器,并尝试查看PowerManager或WakeLock的状态。意识到你的错误。以前我一直都这么想(“所有这些本地人怎么了?”),直到我不得不花大部分时间研究代码。
Faerindel

42
即使在您的示例中,“消除”版本也具有滚动条,并且无法完全显示在我的屏幕上,这使得阅读起来非常困难。
Erik

Answers:


235

阅读代码的频率要比编写代码的频率高得多,因此您应该怜悯这个可怜的人,因为这个可怜的人将从现在起六个月后必须阅读代码(可能是您自己),并努力争取最清晰,最容易理解的代码。我认为,具有局部变量的第一种形式更容易理解。我看到三行有三个动作,而不是一行有三个动作。

而且,如果您认为自己正在通过摆脱局部变量来优化任何东西,那么事实并非如此。无论是否使用局部变量,现代编译器都会将powerManager寄存器1放入该newWakeLock方法中。的情况也是如此wakeLock。因此,无论哪种情况,您最终都得到相同的编译代码。

1如果在声明和使用局部变量之间有很多插入代码,则这些代码可能会放在堆栈上,但这只是次要的细节。


2
您不知道是否有很多代码,优化器可能内联了一些功能...否则,您同意,首先努力提高可读性是正确的做法。
Matthieu M.

2
好吧,我想相反,如果我看到一个局部变量在使用它的每个函数上都被初始化了多次,而不是在可能的时候成为一个属性,我会寻找原因,基本上我会仔细阅读这些行并将它们进行比较。确保它们是相同的。所以我会浪费时间。
Walfrat

15
@Walfrat局部变量被多次初始化?哎哟。没有人建议再用本地人:)
a安

32
@Walfrat成员会产生副作用,仅当您特别希望在调用公共成员之间保持状态时,才应使用@Walfrat成员。这个问题似乎是关于使用local临时存储中间计算。
古斯多

4
问:如果可以,是否应该消除局部变量?答:否
simon's

94

一些高度评价的评论说明了这一点,但是我没有看到任何答案,因此,我将其添加为答案。决定此问题的主要因素是:

可调试性

通常,与编写代码相比,开发人员花费更多的时间和精力进行调试。

使用局部变量,您可以:

  • 分配给它的行上的断点(以及所有其他断点修饰,例如条件断点等)

  • 检查/监视/打印/更改局部变量的值

  • 捕获由于强制转换而出现的问题。

  • 具有清晰的堆栈轨迹(XYZ行仅执行一次操作,而不是10次操作)

如果没有局部变量,则上述所有任务要么更加艰巨,极其艰巨,要么完全不可能,这取决于您的调试器。

这样,遵循臭名昭著的格言(以一种编写代码的方式,就好像您自己的下一个开发人员在知道自己的住所的疯子疯子之后会维护它一样),并且在更容易调试的方面犯了错误,这意味着使用局部变量


20
我要补充一点,当一行上有许多调用时,堆栈跟踪的用处要小得多。如果您在第50行有一个空指针异常,并且该行有10个调用,则不会使事情变窄。在生产应用程序中,这通常是从缺陷报告中获得的大部分信息。
JimmyJames

3
单步执行代码也要困难得多。如果存在call()-> call()-> call()-> call(),则很难进入第三个方法调用。如果有四个局部变量,则容易
得多

3
@FrankPuffer-我们必须处理现实世界。调试器不执行您的建议的地方。
DVK

2
@DVK:实际上,调试器的数量超出了我的想象,可以让您检查或监视任何表达式。MS Visual Studio(自2013版)具有针对C ++和C#的此功能。Eclipse为Java提供了它。在另一个评论中,Faerindel提到了JS的IE11。
Frank Puffer

4
@DVK:是的,许多现实世界中的调试器没有按照我的建议去做。但这与我的评论的第一部分无关。我只是觉得疯狂,以为要维护我的代码的人将主要通过调试器与之交互。调试器对于某些特定目的非常有用,但如果它们获得了用于分析代码的主要工具,我会说有些严重错误,我将尝试对其进行修复,而不是使代码更具可调试性。
Frank Puffer

47

仅当它使代码更易于理解时。在您的示例中,我认为它很难阅读。

对于任何受人尊敬的编译器而言,消除已编译代码中的变量都是微不足道的操作。您可以自己检查输出以进行验证。


1
这与样式的使用和变量的使用一样重要。现在,该问题已被编辑。
Jodrell

29

您的问题“如果仅在范围中使用一次,则消除局部变量是一种好习惯吗?” 测试错误的标准。局部变量的实用程序不取决于它的使用次数,而是取决于它是否使代码更清晰。在有意义的名称中标记中间值可以提高在某些情况下(例如您所提出的名称)的清晰度,因为它会将代码分解为更小,更易消化的块。

在我看来,您提供的未修改代码比修改后的代码更清晰,因此应保持不变。实际上,我会考虑从修改后的代码中提取局部变量,以提高清晰度。

我不希望局部变量对性能有任何影响,即使存在局部变量,也可能太小而不值得考虑,除非代码在程序中对速度至关重要的部分处于非常紧密的循环中。


我认为这与样式和将空格用作任何东西有很大关系。问题已被编辑。
Jodrell

15

也许。

如果涉及类型转换,我个人会犹豫消除局部变量。由于方括号的数量开始接近我的心理极限,我发现您的精简版不易读。它甚至引入了一套新的括号,在使用局部变量的稍微冗长的版本中不需要。

代码编辑器提供的语法高亮显示可能会在某种程度上减轻此类担忧。

在我看来,这是更好的折衷方案:

PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE);
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "abc").acquire();

因此保留一个局部变量而放弃另一个局部变量。在C#中,我会在第一行使用var关键字,因为类型转换已经提供了类型信息。


13

尽管我接受支持局部变量的答案的有效性,但我将扮演魔鬼的拥护者,并提出相反的观点。

我个人反对纯粹将局部变量用于构成文档目的,尽管该原因本身表明该做法确实有价值:局部变量有效地对代码进行了伪文档编制。

在您的情况下,主要问题是缩进而不是缺少变量。您可以(并且应该)将代码的格式设置如下:

((PowerManager)getSystemService(POWER_SERVICE))
        .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakelockTag")
        .acquire();

将其与具有局部变量的版本进行比较:

PowerManager powerManager=(PowerManager)getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"abc");
wakeLock.acquire();

使用局部变量:名称为阅读器添加了一些信息,并且明确指定了类型,但实际的方法调用不太明显,并且(以我的观点)阅读起来不太清晰。

不使用局部变量:它更简洁,方法调用更可见,但是您可能需要有关返回内容的更多信息。但是,某些IDE可以向您显示。

另外三点评论:

  1. 我认为,添加没有功能用途的代码有时会造成混淆,因为读者必须意识到它的确没有功能用途,并且只是用于“文档编制”。例如,如果此方法的长度为100行,那么除非稍后阅读了整个方法,否则显然在以后的某个时候不需要局部变量(以及方法调用的结果)。

  2. 添加局部变量意味着您必须指定它们的类型,这会在代码中引入依赖关系,否则该依赖关系将不存在。如果方法的返回类型发生了微小变化(例如,已重命名),则必须更新代码,而没有局部变量,则不会。

  3. 如果调试器不显示方法返回的值,则使用局部变量进行调试可能会更容易。但是,解决方法是修复调试器中的缺陷,而不是更改代码。


关于您的进一步评论:1.如果遵循以下约定:局部变量的声明应尽可能靠近使用它们的地方,并且您的方法保持合理的长度,那么这应该不是问题。2.不能使用具有类型推断的编程语言。3.编写良好的代码通常根本不需要调试器。
罗伯特·哈维

2
您的第一个代码示例仅在使用流利的界面或“构建器”来制作对象时才有效,我不会在代码中的任何地方都使用模式,而只是选择性地使用这些模式。
罗伯特·哈维

1
我认为问题是假设局部变量在功能上是冗余的,因此这种情况需要接口类型。如果不是通过各种扭曲的话,我们可能可以实现移除本地人的功能,但是我想到的是,我们正在研究一个简单的案例。
rghome

1
我发现您的变体的可读性甚至更差。我可能对VB.net的了解太多了,但是尽管看到您的示例,我的第一个消息是“ WITH语句去了哪里?我是否意外删除了它?” 我需要看一下发生了什么事,而几个月后我需要重新审视代码时,那就不好了。
Tonny

1
@Tonny对我来说,更具可读性:当地人不会添加任何从上下文中看不出来的东西。我有我的Java头,所以没有with声明。这将是Java中的常规格式约定。
rghome

6

这里的动机很紧张。临时变量的引入可以使代码更易于阅读。但是,它们还可以防止其他可能的重构,并且使它更难查看,例如Extract Method以及使用Query替换Temp。适当时,这些后面的重构类型通常提供比temp变量更大的好处。

关于这些后期重构的动机,Fowler写道

“临时性的问题在于它们是临时的和局部的。因为只能在使用它们的方法的上下文中才能看到它们,所以临时性倾向于鼓励使用更长的方法,因为这是达到临时性的唯一途径。用查询方法代替temp,该类中的任何方法都可以获取该信息。这有助于为该类提供更简洁的代码。”

因此,是的,如果临时文件使代码更具可读性,请使用临时文件,尤其是当这是您和您的团队的本地规范时。但请注意,这样做有时会使发现较大的替代改进变得更加困难。如果您可以增强能力去感知何时没有这种临时性值得,并且在这样做时变得更加舒适,那么这可能是一件好事。

FWIW我个人十年来一直避免阅读Fowler的书《 Refactoring》,因为我认为在这样相对简单的话题上没什么可说的。我完全错了。当我最终阅读它时,它改变了我的日常编码习惯,这使情况变得更好。


1
好答案。我编写的最好的代码(从其他人那里看到的)通常有很多小的(2行,3行)方法可以代替此类临时变量。与此相关的是,这种有争议的方法使私有方法发臭了 ……。经过深思熟虑,我经常发现它是正确的。
Stijn de Witt

该问题中的临时工已经涉及功能,因此该答案与OPs问题无关。
user949300

@ user949300我认为这无关紧要。是的,OP的特定示例中的临时值是函数或方法的返回值。但是OP非常清楚,这只是示例场景。实际的问题是更为笼统的“我们应该在可能的时候消除临时变量吗?”
乔纳森·哈特利

1
好的,我被卖了,**试图**改变我的投票。但是,通常当我超出OP问题的有限范围时,就会感到沮丧。所以可能是善变的... :-)。呃,直到您编辑,我才能更改我的投票,等等……
user949300

@ user949300圣牛!经过深思熟虑后才改变主意的人!先生,您是罕见的,我向您请了我的帽子。
乔纳森·哈特利

3

好吧,如果可以使代码更具可读性,则消除局部变量是一个好主意。那种长的单行代码是不可读的,但是遵循了非常冗长的OO风格。如果您可以将其减少到类似

acquireWakeLock(POWER_SERVICE, PARTIAL, "abc");

那我说那可能是个好主意。也许您可以引入辅助方法将其简化为类似的内容;如果这样的代码多次出现,那么它很值得。


2

让我们在这里考虑Demeter定律。在LoD上的Wikipedia文章中指出以下内容:

函数的德米特定律要求对象O的方法m只能调用以下类型的对象的方法:[2]

  1. O本身
  2. m的参数
  3. 在m内创建/实例化的任何对象
  4. O的直接组成对象
  5. 全局变量,在m范围内可由O访问

遵循该法则的后果之一是,应避免在长的点缀在一起的字符串中调用其他方法返回的对象的方法,如上面的第二个示例所示:

((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakelockTag").acquire();

真的很难弄清楚它在做什么。要了解它,您必须了解每个过程的作用,然后解密在哪个对象上调用哪个函数。第一个代码块

PowerManager powerManager=(PowerManager)getSystemService(POWER_SERVICE);
WakeLock wakeLock=powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"abc");
wakeLock.acquire();

清楚地显示了您要执行的操作-您正在获取PowerManager对象,从PowerManager中获取WakeLock对象,然后获取akeLock。上面的代码遵循LoD的规则#3-您在代码中实例化了这些对象,因此可以使用它们来完成所需的工作。

也许可以考虑的另一种方法是记住,在创建软件时,您应该写清楚,而不是为了简洁。所有软件开发中的90%是维护。切勿编写不愿维护的代码。

祝你好运。


3
我会对这种语法如何适应流畅的语法感兴趣。使用正确的格式,流畅的语法具有很高的可读性。
古斯多

12
那不是“得墨meter耳法则”的意思。得墨meter耳定律不是点数练习;从本质上讲,它意味着“只与您的直系朋友交谈”。
罗伯特·哈维

5
通过中间变量访问相同的方法和成员仍然严重违反Demeter定律。您只是模糊了它,没有解决它。
乔纳森·哈特利

2
就“得墨meter耳法律”而言,两个版本都是“坏”的。
绿巨人

@Gusdor同意,这更多地是对问题示例中样式的评论。现在,该问题已被编辑。
Jodrell

2

需要注意的一件事是,代码经常被读取到不同的“深度”。这段代码:

PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "abc");
wakeLock.acquire();

容易“浏览”。这是3条陈述。首先我们想出一个PowerManager。然后我们想出一个WakeLock。然后我们acquirewakeLock。只需查看每一行的开始,我就能很轻松地看到这一点。简单的变量赋值确实很容易被部分识别为“ Type varName = ...”,而在精神上略过“ ...”。同样,最后一个语句显然不是赋值的形式,而是仅包含两个名称,因此“主要要旨”立即显而易见。如果我只是想回答“此代码的作用是什么?”,这通常就是我所需要的。在较高的水平。

如果我正在追寻一个我认为在这里的细微错误,那么显然我将需要更详细地介绍这一点,并且实际上会记住“ ...”。但是单独的语句结构仍然可以帮助我一次执行一个语句(特别是在我需要更深入地执行每个语句所调用的内容的情况下特别有用;当我回来时,我已经完全理解“一个单元”然后可以转到下一条语句)。

((PowerManager)getSystemService(POWER_SERVICE))
    .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakelockTag")
    .acquire();

现在,这只是一个陈述。顶层结构不太容易阅读。在OP的原始版本中,没有换行符和缩进以直观地传达任何结构,我不得不计算括号将其解码为3个步骤。如果某些多部分表达式相互嵌套而不是按方法调用链排列,那么它看起来仍然可能与此类似,因此我必须谨慎对待,不要计算括号。如果我确实相信缩进,只是略过最后一件事,将其作为所有这些假设的要点,那么,.acquire()告诉我的是什么呢?

但是有时候,这可能就是您想要的。如果我中途应用您的转换并写道:

WakeLock wakeLock =
     ((PowerManeger)getSystemService(POWER_SERVICE))
    .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakeLockTage");
wakeLock.acquire();

现在,这传达给快速浏览“先获得WakeLock,然后获得acquire”。比第一个版本更简单。显而易见,获得的东西是WakeLock。如果获取PowerManager只是一个子细节,对本代码的意义而言并不wakeLock重要,但是至关重要,那么它实际上可以帮助掩埋这些PowerManager内容,因此,如果您只是想快速获取一个代码,则自然可以略过它该代码的作用的想法。而不是将其命名为通信,它只使用一次,有时候这是 重要的是什么(如果范围的其余部分很长,我必须阅读所有内容以判断它是否再次使用;不过,如果您的语言支持,使用显式子范围可能是解决该问题的另一种方式)。

由此可见,一切都取决于上下文和您要交流的内容。就像用自然语言编写散文一样,总是有很多方法可以编写给定的代码,这些信息在信息内容上基本上是等效的。与使用自然语言编写散文一样,在它们之间进行选择通常不应采用诸如“消除仅出现一次的任何局部变量”之类的机械规则。而是如何您选择写下您的代码将强调某些内容而不再强调其他内容。您应该根据您实际要强调的内容,有意识地做出这些选择(包括出于技术原因有时编写不太可读的代码的选择)。尤其要考虑为那些只需要“了解要点”(在各个级别上)的代码的读者提供服务的原因,因为这种情况比非常接近的逐表达式阅读要频繁得多。


对我来说,即使我不了解API,即使没有变量,代码的作用也很明显。getSystemService(POWER_SERVICE)获取电源管理器。.newWakeLock获得唤醒锁。.acquire获得它。好的,我不知道所有的类型,但是如果需要,我可以在IDE中找到。
rghome

对您来说,代码应该执行的操作可能很明显,但是您不知道它实际执行的操作。如果我正在寻找错误,那么我所知道的是,某处的某些代码无法实现其应有的功能,因此我必须找到哪个代码。
gnasher729

@ gnasher729是的。当您需要仔细阅读所有细节时,我就举了一个寻找错误的例子。真正有助于查找未执行应做的代码的事情之一是能够轻松查看原始作者的意图。

当我退出时,我已经完全理解“一个单元”,然后可以继续进行下一个声明。其实不行 实际上,局部变量妨碍了对此的全面理解。因为他们仍然挥之不去......占据心理空间......会有什么事情发生在自己身上,在未来20个语句... 击鼓声 ......如果没有当地人你会确保物品都不见了,也不可能做任何事情与他们在下一行。无需记住它们。return出于相同的原因,我喜欢尽可能快地使用方法。
Stijn de Witt

2

这甚至是具有自己名称的反模式:Train Wreck。已经阐明了避免更换的几个原因:

  • 难以阅读
  • 难以调试(同时监视变量和检测异常位置)
  • 违反Demeter(LoD)

考虑此方法是否对其他对象了解太多。方法链接是一种替代方法,可以帮助您减少耦合。

还请记住,对对象的引用确实很便宜。


嗯,修改后的代码不是在进行方法链接吗?编辑阅读链接的文章,所以我现在看到了区别。相当不错的文章,谢谢!
Stijn de Witt

感谢您的审查。无论如何,方法链接可能在某些情况下可行,而仅保留变量可能是适当的决定。知道为什么人们不同意您的回答总是很高兴的。
Borjab

我想“火车残骸”的反面是“本地线”。它是在两个主要城市之间的火车,停在之间的每个乡村中,以防万一有人想要上下车,即使大多数日子没有人这样做。
rghome

1

提出的问题是“如果可以,是否应该消除局部变量”?

不,您不应该仅仅因为可以就消除它。

您应该在商店中遵循编码准则。

在大多数情况下,局部变量使代码更易于阅读和调试。

我喜欢你的所作所为PowerManager powerManager。对我而言,该班级的小写形式只是一次使用。

如果不该使用变量,它将不占用昂贵的资源。许多语言都有需要清除/释放的局部变量语法。在C#中正在使用。

using(SQLconnection conn = new SQLconnnection())
{
    using(SQLcommand cmd = SQLconnnection.CreateCommand())
    {
    }
}

这不是真正与局部变量有关,而是与资源清理有关...我不太精通C#,但是如果它using(new SQLConnection()) { /* ... */ }也不合法(如果它有用,则是另一回事),我会感到惊讶。
Stijn de Witt

1

在其他答案中未提及一个重要方面:每当添加变量时,都会引入可变状态。这通常是一件坏事,因为它会使您的代码更复杂,从而更难以理解和测试。当然,变量的范围越小,问题就越小。

您实际上想要的不是一个可以修改其值的变量,而是一个临时常量。因此,如果您的语言允许,请考虑在您的代码中表达这一点。在Java中,您可以使用final;在C ++中,您可以使用const。在大多数功能语言中,这是默认行为。

的确,局部变量是可变状态的危害最小的类型。成员变量可能会带来更多麻烦,而静态变量甚至更糟。我仍然发现,在您的代码中尽可能准确地表达应该执行的操作仍然很重要。而且,以后可以修改的变量与中间结果的唯一名称之间存在巨大差异。因此,如果您的语言允许您表达这种差异,那就去做。


2
我没有低估您的答案,但我想这与以下事实有关:除非您正在谈论某种长期运行的方法,否则在命令式语言中通常不会将局部变量视为“状态”。我同意将final / const用作最佳实践,但引入变量以简单地中断链式调用几乎不会导致可变状态导致问题。实际上,使用局部变量有助于处理可变状态。如果一个方法可以在不同的时间返回不同的值,那么您可能会遇到一些非常有趣的错误。
JimmyJames

@JimmyJames:在我的回答中添加了一些解释。但是您的评论中有一部分是我不理解的:“使用局部变量有助于处理可变状态”。你能解释一下吗?
Frank Puffer

1
例如,假设我需要检查一个值,如果该值超过10,则会发出警报。假设此值来自method getFoo()。如果我避免使用局部声明,则最终会得到,if(getFoo() > 10) alert(getFoo());但是getFoo()在两个不同的调用上可能返回不同的值。我可以发送一个小于或等于10的警报,这充其量是令人困惑的,并且会作为缺陷再次出现。并发使得这种事情变得更加重要。本地分配是原子的。
JimmyJames

很好的一点。也许我不喜欢这些本地人原因。...您将在调用此方法后对该实例执行操作PowerManager吗?让我检查一下其余的方法。而WakeLock您得到的这 件事...您正在做什么(再次扫描其余方法)... mmm又什么也没有...确定,为什么它们在那里?哦,是的,应该更具可读性...但是如果没有它们,我相信您不会再使用它们了,因此我不必阅读本方法的其余部分。
Stijn de Witt

发言者在最近的一次演讲中认为,表明变量是最终变量的最佳方法是编写显而易见的变量不变的简短方法。如果final需要方法中的局部变量,则您的方法太长。
user949300
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.