Effective Kotlin Translation Series - Chapter 1 - Item 2 - Minimizing Variable Scope

Get into the habit of writing together! This is the second day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event

Translation: jiajianchen Proofreading: zhiyueli

Item 2: Minimize variable scope

TLDR:

In any case, we recommend that you narrow down the scope of the variable or member property as much as possible.

introduce

When we define a state, we tend to narrow down the scope of variables and properties by referring to the following guidelines :

  • Prefer local variables over member properties;
  • Use variables in the smallest possible scope, for example, if a variable is only used in a loop, define the variable in the loop;

The range of elements we mention here refers to the visible range of elements in the program. In Kotlin, the visible scope of an element is generally determined by the outer curly braces, and members outside the current scope can usually be accessed, such as the following example:

val a = 1
fun fizz() {
  val b = 2
  print(a + b) // 当前位置可以访问 a
}
val buzz = {
  val c = 3
  print(a + c)
}
// 当前位置可以访问 a,但是不能访问 b 和 c
复制代码

In the above example, within methods fizzand methods buzz, variables of outer scope can be accessed. However, the members inside the method cannot be accessed from the outside.

The following example shows how to limit the scope of a variable:

// 坏的
var user : User
for (i in users.indices) {
   user = users[i]
   print("User at $i is $user")
}
​
// 好的
for (i in users.indices) {
  val user = users[i]
  print("User at $i is $user")
}
​
// 同样的变量可见范围,更佳的语法实现
for ((i, user) in users.withIndex()) {
  print("User at $i is $user")
}
复制代码

In the first example, the variable can be accessed usernot only inside the loop, but also outside; while in the second and third examples, we limit the scope of the variable to the loop.for

Similarly, there may be cases where visible scopes are nested , such as lambdaanother expression nested in an lambdaexpression. We recommend that the best practice is to define variables in the smallest possible scope.

why

As for why we do this, there are several reasons:

首先最重要的是:当我们收紧了变量的范围,能更容易地去跟踪和管理我们的程序。当我们分析代码的时候,我们需要去考虑目前都有哪些元素。如果需要处理的元素越多,那么进行下一步编程的难度就会越大;而如果程序越简单,那么它就越不容易被破坏。其次,这跟我们更喜欢用不可变的属性或对象的原因是类似的。结合可变的属性来考虑,如果它仅能在一个更小的范围中修改,那么我们能更容易跟踪它是如何被修改的。我们也能更容易地对其进行进一步的推理和修改。

另一个问题是,具备更宽泛范围的变量,可能会被另一个开发者滥用。举个例子:

  • 我们使用一个变量来记录列表的最后一个元素。做法是对列表进行遍历,不断通过列表对应值修改当前变量,在循环结束时当前变量即可获取到列表的最后一个元素。

但这可能会引发严重的问题,比如说在循环结束之后去修改这最后的元素。这就非常糟糕了,因为另一个开发者会努力去理解整套逻辑,分析出当前元素代表的数值究竟是什么。而带来的这些副作用很明显是不必要的。

译者注:作者举这个例子的意图其实是推荐使用一个不可变的属性来记录上述提到的“列表末尾值”。

除此之外,无论一个变量是只读的还是可读写的,我们会更推荐在变量定义时对其进行初始化。别让开发者被迫要去找变量定义的位置。实现上述观点,我们可以在表达式中使用一些控制结构(如ifwhentry-catch和 Kotlin 的多目运算符)。

// 坏的
val user : User
if (hasValue) {
  user = getValue()
} else {
  user = User()
}
​
// 好的
val user : User = if (hasValue) {
  getValue()
} else {
  User()
}
复制代码

如果我们需要同时定义多个属性,可以使用 Kotlin 的解构声明语法:

// 坏的
fun updateWeather(degrees: Int) {
  val description: String
  val color: Int
  if (degrees < 5) {
    description = "cold"
    color = Color.BLUE
  } else if (degrees < 23) {
    description = "mild"
    color = Color.YELLOW
  } else {
    description = "hot"
    color = Color.RED
  }
}
​
// 好的
fun updateWeather(degrees: Int) {
  val (description, color) = when {
    degrees < 5 -> "cold" to Color.BLUE
    degrees < 23 -> "mild" to Color.YELLOW
    else -> "hot" to Color.RED
  }
}
复制代码

隐患

最后,太宽泛的可见范围是很危险的。接下来讲一种一个常见的危险做法:变量捕获「Capturing」

当我在传授 Kotlin 协程知识的时候,我布置的其中一个练习题是:通过 Sequence Builder 来过滤出某个列表中的素数。解题思路如下:

  1. 创建一个从 2 开始的列表;
  2. 取列表中的第一个数,同时它也是一个素数;
  3. 接下来从列表中过滤掉所有能被这个素数整除的数字。

下面是这个算法的简单实现:

var numbers = (2..100).toList()
val primes = mutableListOf<Int>()
while (numbers.isNotEmpty()) {
  val prime = numbers.first()
  primes.add(prime)
  numbers = numbers.filter { it % prime != 0 }
}
print(primes) // [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]
​
复制代码

现在增加一点难度,我需要一个能返回无限个数的素数序列「sequence」。(如果你想要挑战这道题,你可以在这里停下来,并且尝试自己去实现它)

解决的答案如下:

val primes: Sequence<Int> = sequence {
  var numbers = generateSequence(2) { it + 1 }
  while (true) {
    val prime = numbers.first()
    yield(prime)
    numbers = numbers.drop(1).filter { it % prime != 0 }
  }
}
​
print(primes.take(10).toList()) // [2,3,5,7,11,13,17,19,23,29]
复制代码

然后,几乎每个组里面都有一个人想要试图去“优化”它,认为不应该在每次循环中都创建变量来提取素数,于是改成了以下代码:

val primes: Sequence<Int> = sequence {
  var numbers = generateSequence(2) { it + 1 }
  var prime: Int
  while (true) {
    prime = numbers.first()
    yield(prime)
    numbers = numbers.drop(1).filter { it % prime != 0 }
  }
}
复制代码

问题是,“优化”后的实现并不能正确工作,得出的答案是错误的。

print(primes.take(10).toList())
// [2,3,5,6,7,8,9,10,11,12]
复制代码

(你可以在这停下来,思考为什么会是这个结果)

为什么会出现这样的结果,是因为我们访问的是变量prime。当我们使用Sequence时候,执行过滤操作是惰性的。在每一次循环中,我们添加越来越多的循环操作。在“优化后”的代码中,我们添加的过滤器引用的prime是可变的,因此,每次执行过滤逻辑时使用的必然是最后一次变量 prime 的值(而并非预期的值),因此导致我们得出的结果是错误的。

了解到上面的情况之后,我们应该多注意获取数值过程中可能引发的意想不到的问题。要规避这些问题,我们推荐的做法是给变量设置更小的可见范围。

总结

For many reasons, we prefer to define variables in as small a scope as possible. And for local variables var, we recommend using them instead val. These simple rules can save us a lot of trouble.

Guess you like

Origin juejin.im/post/7085637121725169700
Recommended