我在高中的第一个编程课上。我们正在完成第一学期的项目。
本项目只涉及一个类,但方法很多。我的问题是关于实例变量和局部变量的最佳实践。对我来说,几乎只使用实例变量来编写代码似乎要容易得多。但我不确定这是我应该这样做还是应该更多地使用局部变量(我只需要让方法更多地接受局部变量的值)。
我这样做的原因也是因为很多时候我想让一个方法返回两个或三个值,但这当然是不可能的。因此,简单地使用实例变量似乎更容易,而且不必担心,因为它们在类中是通用的。
我在高中的第一个编程课上。我们正在完成第一学期的项目。
本项目只涉及一个类,但方法很多。我的问题是关于实例变量和局部变量的最佳实践。对我来说,几乎只使用实例变量来编写代码似乎要容易得多。但我不确定这是我应该这样做还是应该更多地使用局部变量(我只需要让方法更多地接受局部变量的值)。
我这样做的原因也是因为很多时候我想让一个方法返回两个或三个值,但这当然是不可能的。因此,简单地使用实例变量似乎更容易,而且不必担心,因为它们在类中是通用的。
我还没有看到任何人讨论这个,所以我会投入更多的思考。简短的回答/建议是不要仅仅因为您认为它们更容易返回值而在局部变量上使用实例变量。如果您不适当地使用局部变量和实例变量,您将很难使用您的代码。您将产生一些非常难以追踪的严重错误。如果您想了解我所说的严重错误是什么意思,以及可能看起来像什么,请继续阅读。
让我们尝试仅使用您建议写入函数的实例变量。我将创建一个非常简单的类:
public class BadIdea {
public Enum Color { GREEN, RED, BLUE, PURPLE };
public Color[] map = new Colors[] {
Color.GREEN,
Color.GREEN,
Color.RED,
Color.BLUE,
Color.PURPLE,
Color.RED,
Color.PURPLE };
List<Integer> indexes = new ArrayList<Integer>();
public int counter = 0;
public int index = 0;
public void findColor( Color value ) {
indexes.clear();
for( index = 0; index < map.length; index++ ) {
if( map[index] == value ) {
indexes.add( index );
counter++;
}
}
}
public void findOppositeColors( Color value ) {
indexes.clear();
for( index = 0; i < index < map.length; index++ ) {
if( map[index] != value ) {
indexes.add( index );
counter++;
}
}
}
}
我知道这是一个愚蠢的程序,但我们可以用它来说明这样一个概念:使用实例变量来处理这样的事情是一个非常糟糕的主意。您会发现最重要的是这些方法使用了我们拥有的所有实例变量。并且每次调用时都会修改索引、计数器和索引。您会发现的第一个问题是,一个接一个地调用这些方法可以修改之前运行的答案。例如,如果您编写了以下代码:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
idea.findColor( Color.GREEN ); // whoops we just lost the results from finding all Color.RED
由于 findColor 使用实例变量来跟踪返回值,我们一次只能返回一个结果。在再次调用之前,让我们尝试保存对这些结果的引用:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findColor( Color.GREEN ); // this causes red positions to be lost! (i.e. idea.indexes.clear()
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
在第二个示例中,我们保存了第 3 行的红色位置,但同样的事情发生了!?为什么我们失去了它们?!因为 idea.indexes 被清除而不是分配,所以一次只能使用一个答案。在再次调用它之前,您必须完全使用该结果。一旦你再次调用一个方法,结果就会被清除,你会失去一切。为了解决这个问题,您每次都必须分配一个新结果,因此红色和绿色的答案是分开的。因此,让我们克隆我们的答案以创建新的事物副本:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes.clone();
int redCount = idea.counter;
idea.findColor( Color.GREEN );
List<Integer> greenPositions = idea.indexes.clone();
int greenCount = idea.counter;
好的,最后我们有两个单独的结果。红色和绿色的结果现在是分开的。但是,在程序运行之前,我们必须对 BadIdea 内部的运作方式有很多了解,不是吗?我们需要记住每次调用它时都克隆返回值,以安全地确保我们的结果没有被破坏。为什么来电者被迫记住这些细节?如果我们不必这样做会不会更容易?
另请注意,调用者必须使用局部变量来记住结果,所以当您在 BadIdea 的方法中没有使用局部变量时,调用者必须使用它们来记住结果。那么你真正完成了什么?您实际上只是将问题转移给呼叫者,迫使他们做更多事情。而且你推给调用者的工作不是一个容易遵循的规则,因为该规则有很多例外。
现在让我们尝试用两种不同的方法来做到这一点。请注意我是如何“聪明”的,并且我重用了这些相同的实例变量来“节省内存”并保持代码紧凑。;-)
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findOppositeColors( Color.RED ); // this causes red positions to be lost again!!
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
同样的事情发生了!该死,但我是如此“聪明”并节省内存,代码使用更少的资源!!!这是使用实例变量的真正危险,因为现在调用方法是顺序依赖的。如果我更改方法调用的顺序,即使我没有真正更改 BadIdea 的底层状态,结果也会有所不同。我没有更改地图的内容。当我以不同的顺序调用方法时,为什么程序会产生不同的结果?
idea.findColor( Color.RED )
idea.findOppositeColors( Color.RED )
产生的结果与我交换这两种方法不同:
idea.findOppositeColors( Color.RED )
idea.findColor( Color.RED )
这些类型的错误真的很难追踪,尤其是当这些线彼此不相邻时。您可以通过在这两行之间的任何位置添加一个新调用来完全破坏您的程序,并获得截然不同的结果。当然,当我们处理少量行时,很容易发现错误。但是,在一个更大的程序中,即使程序中的数据没有改变,你也可能会浪费几天的时间来尝试重现它们。
这仅关注单线程问题。如果在多线程情况下使用 BadIdea,错误会变得非常奇怪。如果同时调用 findColors() 和 findOppositeColors() 会发生什么?崩溃,你的头发都掉光了,死亡,时空坍塌成一个奇点,宇宙被吞没?可能至少其中两个。线程现在可能在你的头上,但希望我们现在可以引导你远离做坏事,所以当你真正接触线程时,这些不良做法不会让你真正心痛。
您是否注意到在调用方法时必须非常小心?它们相互覆盖,它们可能随机共享内存,你必须记住它如何在内部工作以使其在外部工作的细节,改变事物的调用顺序会在接下来的几行中产生非常大的变化,它只能在单线程情况下工作。做这样的事情会产生非常脆弱的代码,只要你触摸它就会崩溃。我展示的这些做法直接导致代码变得脆弱。
虽然这可能看起来像封装,但恰恰相反,因为调用者必须知道您如何编写它的技术细节。调用者必须以一种非常特殊的方式编写他们的代码以使他们的代码工作,并且他们不能在不了解代码的技术细节的情况下这样做。这通常被称为泄漏抽象,因为该类被假设隐藏在抽象/接口后面的技术细节,但技术细节泄漏出来,迫使调用者改变他们的行为。每个解决方案都有一定程度的泄漏,但是使用任何上述技术(如这些保证),无论您尝试解决什么问题,如果您应用它们,它将非常容易泄漏。现在让我们看看GoodIdea。
让我们使用局部变量重写:
public class GoodIdea {
...
public List<Integer> findColor( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] == value ) {
results.add( i );
}
}
return results;
}
public List<Integer> findOppositeColors( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] != value ) {
results.add( i );
}
}
return results;
}
}
这解决了我们上面讨论的所有问题。我知道我没有跟踪计数器或返回它,但如果我这样做了,我可以创建一个新类并返回它而不是 List。有时我使用以下对象快速返回多个结果:
public class Pair<K,T> {
public K first;
public T second;
public Pair( K first, T second ) {
this.first = first;
this.second = second;
}
}
很长的答案,但一个非常重要的话题。
当它是类的核心概念时使用实例变量。如果您正在迭代、递归或进行一些处理,请使用局部变量。
当您需要在同一个地方使用两个(或更多)变量时,是时候创建一个具有这些属性的新类(以及设置它们的适当方法)。这将使您的代码更清晰并帮助您思考问题(每个类都是您词汇表中的一个新术语)。
当一个变量是一个核心概念时,它可以成为一个类。例如真实世界的标识符:这些可以表示为字符串,但通常,如果您将它们封装到它们自己的对象中,它们会突然开始“吸引”功能(验证、与其他对象的关联等)
此外(不完全相关)是对象一致性 - 对象能够确保其状态是有意义的。设置一个属性可能会改变另一个。它还使以后更容易将程序更改为线程安全(如果需要)。
小故事:当且仅当一个变量需要被多个方法(或类外部)访问时,将其创建为实例变量。如果您仅在本地需要它,在单个方法中,它必须是一个局部变量。
实例变量比局部变量更昂贵。
请记住:实例变量被初始化为默认值,而局部变量则不是。
始终首选方法内部的局部变量,因为您希望使每个变量的范围尽可能小。但是如果不止一种方法需要访问一个变量,那么它就必须是一个实例变量。
局部变量更像是用于获得结果或即时计算某些东西的中间值。实例变量更像是一个类的属性,比如你的年龄或名字。
简单的方法:如果变量必须由多个方法共享,则使用实例变量,否则使用局部变量。
但是,好的做法是尽可能多地使用局部变量。为什么?对于只有一个类的简单项目,没有区别。对于一个包含很多类的项目,有很大的不同。实例变量指示您的类的状态。类中的实例变量越多,该类可以具有的状态越多,然后,该类越复杂,该类的维护就越困难,或者您的项目可能越容易出错。所以好的做法是使用尽可能多的局部变量来保持类的状态尽可能简单。
声明变量的范围尽可能小。先声明局部变量。如果这还不够,请使用实例变量。如果这还不够,请使用类(静态)变量。
我需要返回多个值返回复合结构,如数组或对象。
首先尽量不要从您的方法中返回多个值。如果你不能,并且在某些情况下你真的不能,那么我建议将它封装在一个类中。在最后一种情况下,我建议您更改类中的另一个变量(实例变量)。实例变量方法的问题在于它增加了副作用 - 例如,您在程序中调用方法 A 并且它修改了一些实例变量。随着时间的推移,这会导致代码的复杂性增加,并且维护变得越来越困难。
当我必须使用实例变量时,我尝试在类构造函数中设置 then final 并初始化 then,这样可以最大限度地减少副作用。这种编程风格(最小化应用程序中的状态变化)应该会带来更好的代码,更容易维护。
试着从对象的角度来思考你的问题。每个类代表不同类型的对象。实例变量是类需要记住的数据片段,以便与自身或其他对象一起工作。局部变量应该只用于中间计算,离开方法后不需要保存的数据。
使用实例变量时
同样,当这些条件都不匹配时使用局部变量,特别是如果变量的角色将在堆栈弹出后结束。例如:Comparator.compare(o1, o2);
通常变量应该有最小的范围。
不幸的是,为了构建具有最小变量范围的类,通常需要做很多方法参数传递。
但是如果你一直遵循这个建议,完美地最小化变量范围,你最终可能会因为所有需要的对象传入和传出方法而导致大量冗余和方法不灵活。
想象一个包含数千种方法的代码库,如下所示:
private ClassThatHoldsReturnInfo foo(OneReallyBigClassThatHoldsCertainThings big,
AnotherClassThatDoesLittle little) {
LocalClassObjectJustUsedHere here;
...
}
private ClassThatHoldsReturnInfo bar(OneMediumSizedClassThatHoldsCertainThings medium,
AnotherClassThatDoesLittle little) {
...
}
另一方面,想象一个包含大量实例变量的代码库,如下所示:
private OneReallyBigClassThatHoldsCertainThings big;
private OneMediumSizedClassThatHoldsCertainThings medium;
private AnotherClassThatDoesLittle little;
private ClassThatHoldsReturnInfo ret;
private void foo() {
LocalClassObjectJustUsedHere here;
....
}
private void bar() {
....
}
随着代码的增加,第一种方法可能最好地最小化变量范围,但很容易导致大量方法参数被传递。代码通常会更冗长,这可能会导致复杂性,因为需要重构所有这些方法。
使用更多实例变量可以降低传递的大量方法参数的复杂性,并且可以在您为了清晰起见而频繁重组方法时为方法提供灵活性。但它会创建更多您必须维护的对象状态。一般来说,建议是做前者,避免后者。
然而,与第一种情况的数千个额外对象引用相比,通常情况下(可能取决于个人)更容易管理状态复杂性。当方法中的业务逻辑增加并且组织需要更改以保持秩序和清晰时,人们可能会注意到这一点。
不仅。当您重新组织方法以保持清晰并在过程中进行大量方法参数更改时,最终会产生大量版本控制差异,这对于稳定的生产质量代码来说并不是那么好。有一个平衡。一种方式导致一种复杂性。另一种方式会导致另一种复杂性。
使用最适合您的方式。随着时间的推移,你会发现这种平衡。
我认为这位年轻的程序员对低维护代码有一些深刻的第一印象。