第2条:最小化变量的作用域
当我们定义一个状态时,我们倾向于通过以下方式来收紧变量和属性的范围:
使用局部变量,代替属性
在尽可能小的范围使用变量,例如,如果一个变量仅仅在一个循环中使用到,那么就在这个循环里定义它
元素的作用域是计算机程序中元素可见的区域。在 Kotlin 中,作用域几乎总是由花括号创建的,我们通常可以从外部作用域去访问元素。来看下面这个例子:
在上面的例子中,在 fizz
和 buzz
函数的作用域中,我们可以访问外部作用域的变量(a),然而,在外部作用域中,我们无法访问这些在函数里定义的变量。下面是一个限制变量作用域的示例:
在第一个示例中,user
变量不仅可以在 for 循环的范围内访问,也可以在 for 循环之外访问。在第二个和第三个示例中,我们将 user
变量的作用域具体限制在 for 循环的作用域内。
类似的,作用域内可能有许多作用域(比如 Lambda 表达式里面又有一层 Lambda 表达式),我们最好在尽可能窄的作用域内定义变量。
我们喜欢使用这种技巧的原因有很多,但最主要的原因是:我们收紧变量的作用域时,我们的程序将更加易于追踪和管理。当我们分析代码时,我们需要考虑此时存在哪些元素。要处理的元素越多,编程就越难进行。应用程序越简单,崩溃的可能性就越小。这与我们为什么更喜欢不可变属性而不是可变属性的原因类似。
如果考虑可变属性,当它们只能在较小的范围内修改时,跟踪它们的变化更容易。对它们进行推断并改变它们的行为更容易。
另一个问题是,范围较广的变量可能会被另一个开发者过度使用。举个例子,如果使用一个变量被用于指定迭代中的下一个元素,那么在循环完成后,列表中的最后一个元素应该保留在该变量中。这样的推断可能会导致严重的滥用,比如在迭代之后使用这个变量来处理最后一个元素,这是非常糟糕的,因为另一个试图理解程序的开发人员需要理解整个推断过程,这将是一个不必要的让程序复杂化的行为。
无论一个变量是只读的还是可读写的,我们总是希望在定义变量时就对其进行初始化。不要强迫其开发人员寻找它定义之处。这可以通过流程控制语句来支持,比如 if
,when
,try-catch
,或者作为表达式使用的 Elvis 操作符:
如果我们需要设置多个属性值,可以使用解构函数:
最后,过于广泛的变量范围可能是危险的,让我来描述其中一个常见的危险。
提取
当我在教授 Kotlin 协程时,我会布置的一个练习是:使用_埃拉托斯特尼筛法_来寻找质数,该算法其实原理很简单:
我们从2开始构建一个数字列表;
我们取出第一个,它是一个质数;
从剩下的数中,我们移除掉第一个,然后过滤掉所有能被这个质数整除的数。
下面是这个算法的一个简单实现:
尽管在几乎每个团队中都有人试图去“优化”它,例如不会在每一个循环中创建变量,而是提取质数作为可变变量:
现在停下来,来尝试解释下这个错误的结果。
之所以会有这样的结果,是因为我们提取了这个 prime
作为可变变量存储每次计算的质数。因为使用的是一个 sequence
,所以过滤是惰性计算的的。在每一步中,我们会添加越来越多的过滤器。在 “优化” 代码中,我们会去为 可变属性prime
添加过滤器,因此我们总是过滤 prime
的最后一个值。这就是这个 filter()
不能正常工作的原因,只有 drop()
方法正常工作,所以我们得到了连续的数字(除了4,当 prime
设置为2时,4就被过滤掉了)
我们应该意识到日常开发中无意识提取变量的问题,因为这种情况会时常发生。为了防止这种情况,我们应该避免元素可变性,并选择使用更窄范围的变量作用域。
总结
出于人性化的考虑,我们应该尽可能接近作用域的去定义变量。此外,对于局部变量,我们更喜欢使用 val
而不是 var
,我们应该始终注意到这样一个事实:变量是在 lambda 表达式中提取的,这样一个简单的规则可以为我们省去诸多麻烦
Last updated