“变量应尽可能地处于最小范围内”是否包括“变量应尽可能不存在”的情况?


32

根据有关“ 原理变量比实例变量更喜欢局部变量? ” 的公认答案,变量应处于尽可能小的范围内。
将问题简化为我的解释,这意味着我们应该重构这种代码:

public class Main {
    private A a;
    private B b;

    public ABResult getResult() {
        getA();
        getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      a = SomeFactory.getA();
    }

    private getB() {
      b = SomeFactory.getB();
    }
}

变成这样的东西:

public class Main {
    public ABResult getResult() {
        A a = getA();
        B b = getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

但是,根据“变量应该尽可能地处于最小范围内”的“精神”,难道“没有变量”的范围是否比“拥有变量”的范围小?所以我认为上面的版本应该重构:

public class Main {
    public ABResult getResult() {
        return ABFactory.mix(getA(), getB());
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

因此getResult()根本没有任何局部变量。真的吗?


72
创建显式变量的好处是必须命名它们。引入一些变量可以快速将一种不透明的方法变成一种可读的方法。
Jared Goguen

11
老实说,一个关于变量名a和b的例子太过虚构,无法说明局部变量的值。幸运的是,您仍然得到了很好的答案。
布朗

3
在第二个片段中,变量不是真正的变量。它们实际上是局部常量,因为您无需修改​​它们。如果最终定义了Java编译器,它们将被视为相同,因为它们在本地范围内,并且编译器知道它们将要发生什么。无论您是否应实际使用final关键字,这都是风格和意见的问题。
海德

2
@JaredGoguen创建显式变量会带来命名负担以及能够命名它们的好处。
贝吉

2
诸如“变量应在尽可能小的范围内生存”之类的代码样式规则的绝对是普遍适用的,无需考虑。因此,在此问题中,您永远不会希望基于代码的形式决定代码风格,在这里,您可以采用简单的准则并将其扩展到不太明显的区域(“如果可能,变量应具有较小的范围”-> “如果可能,则变量不应该存在”)。有充分的理由专门支持您的结论,或者您的结论是不适当的扩展;或者 无论哪种方式,您都不需要原始指南。

Answers:


110

否。有以下几个原因:

  1. 具有有意义名称的变量可以使代码更易于理解。
  2. 将复杂的公式分解为较小的步骤可以使代码更易于阅读。
  3. 正在缓存。
  4. 保留对对象的引用,以便可以多次使用它们。

等等。


22
还值得一提:无论如何,该值都将存储在内存中,因此无论如何它实际上都具有相同的作用域。也可以命名(由于罗伯特上文提到的原因)!
Maybe_Factor

5
@Maybe_Factor出于性能方面的考虑,该指南并不存在。这与维护代码有多容易有关。但这仍然意味着有意义的局部语言比一个大型表达式要好,除非您只是组成名称明确且设计良好的函数(例如,没有理由这样做var taxIndex = getTaxIndex();)。
a安

16
都是重要因素。方式我看它的:简洁是一个目标; 清晰度是另一回事;健壮性是另一个。表演是另一回事;等等。这些都不是绝对目标,有时会发生冲突。因此,我们的工作是找出如何最好地平衡这些目标。
gidds

8
编译器将采取3&4的护理
停止汉宁波莫妮卡

9
数字4对于正确性可能很重要。如果函数调用的值可以更改(例如now(),),则删除变量并多次调用方法可能会导致错误。这会造成一种非常微妙且难以调试的情况。这看起来似乎很明显,但是如果您盲目地执行重构任务以删除变量,那么最终容易引入缺陷。
JimmyJames

16

同意,应避免使用不必要的变量,这些变量不会提高代码的可读性。在代码给定点范围内的变量越多,代码应理解的越复杂。

我实在不明白变量的利益ab在你的榜样,所以我会写的版本不变量。另一方面,功能首先是如此简单,我认为它并不重要。

函数获取的时间越长,作用域中的变量越多,就成为一个问题。

例如,如果您有

    a=getA();
    b=getB();
    m = ABFactory.mix(a,b);

在较大功能的顶部,通过引入三个变量而不是一个变量,增加了理解代码其余部分的精神负担。您必须通读其余的代码以查看是否再次使用ab。作用域比所需时间长的本地语言对整体可读性不利。

当然,在必须使用变量(例如,存储临时结果)或变量确实提高了代码可读性的情况下,应保留该变量。


2
当然,从某种意义上说,一种方法太长了,以至于本地人的数量太大而难以维护,您可能还是想将其分解。
Luaan

4
“外部”变量的好处var result = getResult(...); return result;是,例如,您可以设置一个断点return并了解确切的含义result
Joker_vD

5
@Joker_vD:名符其实的调试器应该能够使您完成所有这些工作(查看函数调用的结果,而无需将其存储在本地变量中+在函数出口处设置断点)。
Christian Hackl

2
@ChristianHackl很少有debuggesr可以让您做到这一点,而无需设置可能需要花费时间才能插入的复杂手表(如果函数有副作用,则可能会破坏所有功能)。我将使用with变量版本在一周的任何一天进行调试。
Gabe Sechan

1
@ChristianHackl并在此基础上进一步扩展,即使调试器默认情况下由于调试器的设计糟糕而不允许您执行此操作,也可以修改计算机上的本地代码以存储结果,然后在调试器中将其检出。有还是没有理由在一个变量存储值只是这一目的。
大鸭

11

除了其他答案,我还要指出其他问题。保持变量作用域较小的好处不仅在于减少语法上可以访问该变量的代码量,而且还减少了可能潜在地修改变量的控制流路径的数量(通过为它分配新值或调用变量中保存的现有对象的变异方法)。

与局部作用域变量相比,类作用域变量(实例或静态)比本地作用域变量具有更多的可能的控制流路径,因为它们可以通过方法进行突变,方法可以按任意顺序,任意次数调用,并且常常由类外的代码调用。

让我们看一下您的初始getResult方法:

public ABResult getResult() {
    getA();
    getB();
    return ABFactory.mix(this.a, this.b);
}

现在,名称getAgetB可能暗示,他们将分配给this.athis.b,我们无法知道肯定从只盯着getResult。因此,传递给方法的this.athis.b值可能来自调用之前对象mix的状态,这是无法预测的,因为客户端控制方法的调用方式和时间。thisgetResult

在带有局部变量ab变量的修改后的代码中,很明显,从分配每个变量到使用为止,只有一个(无异常)控制流,因为变量在使用之前就已声明。

因此,将变量从类范围移动到局部范围(以及将变量从循环外部移动到内部)具有显着的好处,因为它简化了控制流推理。

另一方面,像上一个示例中那样消除变量的好处较小,因为它并不会真正影响控制流推理。您还会丢失赋予值的名称,将变量移至内部作用域时不会发生。您必须权衡这点,因此消除变量在某些情况下可能会更好,而在其他情况下则更糟。

如果您不想丢失变量名,但仍想减小变量的范围(在较大的函数中使用它们的情况下),则可以考虑将变量及其用法包装在一个语句块中(或将其移至自己的功能)。


2

这在某种程度上取决于语言,但我想说的是,函数式编程的较不明显的好处之一是,它鼓励程序员和代码阅读者不需要这些。考虑:

(reduce (fn [map string] (assoc map string (inc (map string 0))) 

或一些LINQ:

var query2 = mydb.MyEntity.Select(x => x.SomeProp).AsEnumerable().Where(x => x == "Prop");

Node.js:

 return visionFetchLabels(storageUri)
    .then(any(isCat))
    .then(notifySlack(secrets.slackWebhook, `A cat was posted: ${storageUri}`))
    .then(logSuccess, logError)
    .then(callback)

最后一个是在没有任何中间变量的情况下,根据前一个函数的结果调用函数的链。引入它们会使它变得不清楚。

但是,第一个示例与其他两个示例之间的区别在于操作隐含顺序。这可能与实际计算的顺序不同,但这是读者应该考虑的顺序。对于后两个,这从左到右。对于Lisp / Clojure示例,它更像是从右到左。您应该警惕编写不是针对您的语言的“默认方向”的代码,并且绝对应避免将两者混为一谈的“中间”表达式。

F#的管道运算符|>之所以有用,部分是因为它允许您编写从左到右的东西,否则这些东西就必须是从右到左的。


2
我习惯在简单的LINQ表达式或lambda中使用下划线作为变量名。myCollection.Select(_ => _.SomeProp).Where(_ => _.Size > 4);
格雷厄姆

不确定这丝毫不会回答OP的问题...
AC

1
为什么要下票?似乎对我来说是一个很好的答案,即使它没有直接回答问题,它仍然是有用的信息
reggaeguitar

1

我会说不,因为您应该将“可能的最小范围”读为“在现有范围或合理添加的范围中”。否则,这意味着您应该创建人为的作用域(例如{},类似C的语言的免费块),以确保变量的作用域不超出最后的预期用途,并且通常会被混淆/混淆,除非已经存在范围独立存在的充分理由。


2
在许多语言中,范围界定的一个问题是,某些变量的最后用途是计算其他变量的初始值,这是非常普遍的。如果一种语言具有明确的构造作用域,从而允许将使用内部作用域变量计算的值用于外部作用域变量的初始化,则这些作用域可能比许多语言所要求的严格嵌套的作用域更有用。
supercat

1

考虑函数方法)。既没有将代码拆分为最小的子任务,也没有拆分最大的单例代码。

这是将逻辑任务划分为多个易耗件的限制。

这同样适用于变量。将逻辑数据结构指向易于理解的部分。或者也可以简单地命名(声明)参数:

boolean automatic = true;
importFile(file, automatic);

但是,当然,在顶部有一个声明,再往后两百行,第一次使用已被认为是不好的样式。这显然是“变量应该在尽可能小的范围内生存”的意图。就像一个非常接近的“不要重用变量”。


1

由于NO的原因是调试/可读性,因此缺少一些东西。为此,应该对代码进行优化,清楚简洁的名称会有所帮助,例如,设想一种3

if (frobnicate(x) && (get_age(x) > 2000 || calculate_duration(x) < 100 )

这行很短,但是已经很难读懂了。再添加一些参数,如果参数跨越多行。

can_be_frobnicated = frobnicate(x)
is_short_lived_or_ancient = get_age(x) > 2000 || calculate_duration(x) < 100
if (can_be_frobnicated || is_short_lived_or_ancient )

我发现这种方式更易于阅读和传达含义-因此我对中间变量没有任何疑问。

另一个示例是R之类的语言,其中最后一行自动是返回值:

some_func <- function(x) {
    compute(x)
}

这是危险的,退货是扩大还是需要?这更清楚:

some_func <- function(x) {
   rv <- compute(x)
   return(rv)
}

与往常一样,这是一个判断性呼吁-如果中间变量不能改善阅读,则应消除它们,否则请保留或引入它们。

另一点可以是可调试性:如果对中介结果感兴趣,最好像上面的R示例中那样简单地引入中介。很难想象需要多长时间调用一次,这很难想象,并且要注意检查的内容-太多的调试变量令人困惑-再次,是一个判断调用。


0

仅指您的标题:绝对,如果不需要变量,则应将其删除。

但是“不必要”并不意味着可以在不使用变量的情况下编写等效程序,否则我们将被告知应该以二进制形式编写所有内容。

最常见的不必要变量是未使用的变量,变量的范围越小,就越容易确定它是不必要的。中间变量是否是不必要的更难确定,因为它不是二进制情​​况,而是上下文相关的。实际上,根据过去解决周围代码问题的经验,两种不同方法中的相同源代码可能会由同一用户产生不同的答案。

如果您的示例代码完全像所表示的那样,我建议您摆脱这两个私有方法,但是对于您将工厂调用的结果保存到局部变量还是只是将它们用作混合的参数,则几乎不用担心方法。

代码的可读性胜过一切,除了能正常工作(正确地包含可接受的性能标准,很少“尽可能快”)。

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.