Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

核对指令

**核对指令(Check Instructions)**用于确保满足约束条件。它们通常用于验证函数参数和结果。

核对指令通过表达非明显的要求来发现编程错误。它们还可以为以后阅读代码的读者提供文档。通常在函数的开头,用于确保参数合法性,以及在函数的末尾,用于检查函数的计算结果。

核对指令通常在失败时抛出异常。通常情况下,您可以使用核对指令代替显式地抛出异常。核对指令更容易编写和思考,并产生更易理解的代码。尽可能使用它们来测试和阐明您的程序。

require()

**设计契约(Design By Contract)**中的*前置条件(preconditions)*保证了初始化约束。Kotlin中的require()通常用于验证函数参数,因此通常出现在函数体的开头。这些测试不能在编译时检查。前置条件相对容易包含在代码中,但有时可以转换为单元测试

考虑一个表示儒略历月份的数值字段。您知道该值必须始终在1..12的范围内。前置条件在值超出该范围时报告错误:

// CheckInstructions/JulianMonth.kt
package checkinstructions
import atomictest.*

data class Month(val monthNumber: Int) {
  init {
    require(monthNumber in 1..12) {
      "Month out of range: $monthNumber"
    }
  }
}

fun main() {
  Month(1) eq "Month(monthNumber=1)"
  capture { Month(13) } eq
    "IllegalArgumentException: " +
    "Month out of range: 13"
}

我们在构造函数内执行require()。如果其条件不满足,require()会抛出IllegalArgumentException。您总是可以使用require()来代替抛出IllegalArgumentException

require()的第二个参数是一个生成String的lambda表达式。如果String需要构造,那么除非require()失败,否则不会发生该开销。

Summary 2中,Quadratic.kt的参数不当时,它会抛出IllegalArgumentException。我们可以使用require()简化代码:

// CheckInstructions/QuadraticRequire.kt
package checkinstructions
import kotlin.math.sqrt
import atomictest.*

class Roots(
  val root1: Double,
  val root2: Double
)

fun quadraticZeroes(
  a: Double,
  b: Double,
  c: Double
): Roots {
  require(a != 0.0) { "a is zero" }
  val underRadical = b * b - 4 * a * c
  require(underRadical >= 0) {
    "Negative underRadical: $underRadical"
  }
  val squareRoot = sqrt(underRadical)
  val root1 = (-b - squareRoot) / 2 * a
  val root2 = (-b + squareRoot) / 2 * a
  return Roots(root1, root2)
}

fun main() {
  capture {
    quadraticZeroes(0.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "a is zero"
  capture {
    quadraticZeroes(3.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "Negative underRadical: -44.0"
  val roots = quadraticZeroes(3.0, 8.0, 5.0)
  roots.root1 eq -15.0
  roots.root2 eq -9.0
}

这段代码比原始的Quadratic.kt代码要更清晰和简洁。

以下DataFile类允许我们在IDE通过AtomicKotlin课程运行示例,或在独立的书籍构建中运行时,使用文件。所有的DataFile对象都将文件存储在targetDir子目录中:

// CheckInstructions/DataFile.kt
package checkinstructions
import atomictest.eq
import java.io.File
import java.nio.file.Paths

val targetDir = File("DataFiles")

class DataFile(val fileName: String) :
  File(targetDir, fileName) {
  init {
    if (!targetDir.exists())
      targetDir.mkdir()
  }
  fun erase() { if (exists()) delete() }
  fun reset(): File {
    erase()
    createNewFile()
    return this
  }
}

fun main() {
  DataFile("Test.txt").reset() eq
    Paths.get("DataFiles", "Test.txt")
      .toString()
}

DataFile可以操作底层的操作系统文件,以便读写该文件。DataFile的基类是java.io.File,它是Java库中最古老的类之一;它出现在语言的第一个版本中,当时他们认为使用相同的类(File)来表示文件和目录是个很好的主意。尽管File古老,但Kotlin可以轻松地继承它。

在构造过程中,如果targetDir不存在,则创建它。erase()函数删除文件,而reset()函数会删除文件并创建一个新的空文件。

Java标准库的Paths类只包含一个重载的get()函数。我们想要的get()版本接受任意数量的String,并构建一个Path对象,表示与操作系统无关的目录路径。

打开文件通常有许多前置条件,通常涉及文件路径、命名和内容。考虑一个函数,该函数打开并读取以“file_”开头的文件名的文件。使用require(),我们验证文件名是否正确,并且文件存在且不为空:

// CheckInstructions/GetTrace.kt
package checkinstructions
import atomictest.*

fun getTrace(fileName: String): List<String> {
  require(fileName.startsWith("file_")) {
    "$fileName must start with 'file_'"
  }
  val file = DataFile(fileName)
  require(file.exists()) {
    "$fileName

 doesn't exist"
  }
  val lines = file.readLines()
  require(lines.isNotEmpty()) {
    "$fileName is empty"
  }
  return lines
}

fun main() {
  DataFile("file_empty.txt").writeText("")
  DataFile("file_wubba.txt").writeText(
    "wubba lubba dub dub")
  capture {
    getTrace("wrong_name.txt")
  } eq "IllegalArgumentException: " +
    "wrong_name.txt must start with 'file_'"
  capture {
    getTrace("file_nonexistent.txt")
  } eq "IllegalArgumentException: " +
    "file_nonexistent.txt doesn't exist"
  capture {
    getTrace("file_empty.txt")
  } eq "IllegalArgumentException: " +
    "file_empty.txt is empty"
  getTrace("file_wubba.txt") eq
    "[wubba lubba dub dub]"
}

我们一直使用的是两个参数版本的require(),但也有一个只有一个参数的版本,它会生成一个默认消息:

// CheckInstructions/SingleArgRequire.kt
package checkinstructions
import atomictest.*

fun singleArgRequire(arg: Int): Int {
  require(arg > 5)
  return arg
}

fun main() {
  capture {
    singleArgRequire(5)
  } eq "IllegalArgumentException: " +
    "Failed requirement."
  singleArgRequire(6) eq 6
}

失败消息没有两个参数版本那么明确,但在某些情况下足够使用。

requireNotNull()

requireNotNull()测试其第一个参数,并在该参数不为null时返回该参数。否则,它会抛出IllegalArgumentException

在成功时,requireNotNull()的参数会自动智能转换为非空类型。因此,您通常不需要requireNotNull()的返回值:

// CheckInstructions/RequireNotNull.kt
package checkinstructions
import atomictest.*

fun notNull(n: Int?): Int {
  requireNotNull(n) {             // [1]
    "notNull() argument cannot be null"
  }
  return n * 9                    // [2]
}

fun main() {
  val n: Int? = null
  capture {
    notNull(n)
  } eq "IllegalArgumentException: " +
    "notNull() argument cannot be null"
  capture {
    requireNotNull(n)             // [3]
  } eq "IllegalArgumentException: " +
    "Required value was null."
  notNull(11) eq 99
}
  • [2] 注意,由于调用了requireNotNull()n不再需要空检查,因为它变成了非空类型。

require()一样,有一个两个参数的版本,可以自己构造消息([1]),还有一个带有默认消息的单参数版本([3])。由于requireNotNull()测试的是特定问题(是否为null),因此单参数版本比在require()中更有用。

check()

设计契约的*后置条件(postcondition)*测试函数的结果。对于长而复杂的函数,后置条件对于验证结果的可靠性非常重要。每当您可以描述函数结果的约束时,最好将其表达为后置条件。

check()require()完全相同,不同之处在于它会抛出IllegalStateException,而不是IllegalArgumentException。通常它在函数的末尾使用,以验证结果(或函数对象中的字段)是否有效,即事物是否变得不好。

假设一个复杂的函数写入一个文件,您不确定所有执行路径是否会创建该文件。在函数的末尾添加一个后置条件有助于确保正确性:

// CheckInstructions/Postconditions.kt
package checkinstructions
import atomictest.*

val resultFile = DataFile("Results.txt")

fun createResultFile(create: Boolean) {
  if (create)
    resultFile.writeText("Results\n# ok")
  // ... other execution paths
  check(resultFile.exists()) {
    "${resultFile.name} doesn't exist!"
  }
}

fun main() {
  resultFile.erase()
  capture {
    createResultFile(false)
  } eq "IllegalStateException: " +
    "Results.txt doesn't exist!"
  createResultFile(true)
}

假设您的前置条件确保了有效的参数,后置条件失败几乎总是表示编程错误。出于这个原因,您可能会更少地看到后置条件,因为一旦程序员确信代码是正确的,后置条件就可以被注释掉或删除,如果它影响性能的话。当然,最好保留这些测试,以便立即检测到由未来的代码更改引起的问题。一种做法是将后置条件移到单元测试中。

assert()

为了避免对check()语句进行注释和取消注释,assert()允许您启用和禁用assert()检查。

assert()来自Java。默认情况下,断言是禁用的,只有在您使用命令行标志明确启用它们时才会启用。在Kotlin中,该标志是-ea

我们建议始终使用require()check(),它们在没有特殊配置的情况下始终可用。

练习和解决方案可以在www.AtomicKotlin.com找到。