八、异常:如果出现问题

对于非常简单的程序来说,确保程序的所有部分都准确地做它们应该做的事情可能很容易。对于复杂程度更高的程序,那些由许多开发人员构建的程序,或者那些使用外部程序(库)的程序,情况就不那么清楚了。例如,如果列表或数组的地址越界,对文件或网络数据流的一些 I/O 访问失败,或者对象以意外或损坏的状态结束,就会出现问题。

这就是异常的用途。异常是,如果发生了意想不到的、可能是恶意的事情,对象会被创建或被抛出。然后,特殊的程序部分可以接收这样的异常对象并适当地动作。

Kotlin 和异常

Kotlin 对待异常状态的方式相当自由,但 Android 没有。如果你不关心你的应用中的异常,并且任何程序部分碰巧抛出了异常,Android 会清醒地告诉你应用崩溃了。您可以通过将可疑的程序部分放入 try-catch 块来防止这种情况:

try {
    // ... statements
} catch(e:Exception) {
    // do something
}

or

try {
    // ... statements
} catch(e:Exception) {
    // do something...
} finally {
    // do this by any means: ...
}

在这两种情况下,该构造都被称为捕获异常。可选的 finally 块在构造结束时被执行,不管异常是否被捕获。你通常用它来清理try { }中的代码可能造成的混乱,包括关闭任何打开的文件或网络资源以及类似的操作。

注意

根据经验,在代码中使用许多 try-catch 子句很难提高代码质量。别这样。不过,在应用的中心位置放几个这样的图标通常是个好主意。

一旦在try{ }块中出现异常——这包括来自那里的任何方法调用——程序流立即分支到catch{ }块。这是一个很难回答的问题,尤其是在 Android 环境下。当你开发一个应用时,写日志条目肯定是一个好主意。这不是 Kotlin 标准库的一部分,但是 Android 提供了一个单例对象android.util.Log,你可以用它来写日志:

import android.util.Log
...
try {
    // ... statements
} catch(e:Exception) {
    Log.e("LOG", "Some exception occurred", e)
}

当然,您可以写一些更具体的信息,而不是这里显示的日志文本。

注意

如果你看一下android.util.Log类,你会发现这是一个 Java 类,函数e()是一个不需要实例的静态函数。因此,严格来说,它不是一个单例对象,但是从 Kotlin 的角度来看,它就像是一个单例对象。

开发应用时,您可以在 Logcat 选项卡上看到日志语句,前提是您使用的是仿真器或连接的硬件设备,并且打开了调试功能。使用来自Log类的e()函数提供了一个优势,你可以得到一个堆栈跟踪,这意味着行号被指出,导致错误程序部分的函数调用被列出。图 8-1 给出了一个例子。

img/476388_1_En_8_Fig1_HTML.jpg

图 8-1。

Android Studio 中的异常记录

对于您的最终用户来说,以这种方式提供日志记录是不可取的,因为在大多数情况下,您的用户不知道如何检查日志文件。您可以做的是以Toast的形式显示一条简短的错误消息,如下所示:

import android.util.Log
...
try {
    // ... statements
} catch(e:Exception) {
    Log.e("LOG", "Some exception occurred", e)
    Toast.makeText(this,"Error Code 36A",
          Toast.LENGTH_LONG).show()
}

当然,您向用户呈现的具体内容取决于异常的严重程度。也许您可以以某种方式清除错误状态,并继续正常的程序流程。在非常严重的情况下,您可以显示一个错误消息对话框或分支到一个错误处理活动。

更多异常类型

到目前为止,我们看到的Exception类只是一种异常。如果我们在一个catch语句中使用Exception,我们正式表达了一种非常一般的异常。根据具体情况,您的应用可能与只使用Exception的 try-catch 子句共存得很好。然而,你也可以使用Exception的许多子类。例如,有一个ArrayIndexOutOfBounds异常,一个IllegalArgumentException,一个IllegalStateException,等等。通过添加更多的catch{ }子句,您甚至可以同时使用多个:

try {
    // ... statements
} catch(e:ExceptionType1) {
    // do something...
} catch(e:ExceptionType2) {
    // do something...
... possibly more catch statements
} finally {
    // do this by any means: ...
}

如果在try{ }中抛出一个异常,那么所有的catch子句都会被一个接一个地检查,如果其中一个声明的异常匹配,那么相应的catch子句就会被执行。如果你想捕捉几个异常,你通常做的是把更具体的捕捉放在列表的开始,把最一般的放在最后。例如,假设您有一些访问文件、处理数组的代码,此外还可能抛出未知的异常。你可以在这里写

try {
    // ... file access
    // ... array access
} catch(e:IOException) {
    // do something...
} catch(e:ArrayIndexOutOfBoundsException) {
    // do something...
} catch(e:Exception) {
    // do something...
} finally {
    // do this by any means: ...
}

这里的finally子句是可选的,和往常一样。

自己抛出异常

从您编写的代码中引发异常

throw exceptionInstance

其中exceptionInstance是异常类的实例,例如

val exc = Exception("The exception message")
throw exc

或者

throw Exception("The exception message")

因为异常是普通类,除了在catch子句中的可用性,还可以定义自己的异常。只需扩展Exception类或它的任何子类:

class MyException(msg:String) : Exception(msg)
...
try {
    ...
    throw MyException("an error occurred")
    ...
} catch(e:MyException) {
    ...
}

练习 1

NumberGuess游戏 app 中,定义一个新的类GameException作为Exception的扩展。检查用户输入的数字,如果超过最小或最大的可猜测数字,抛出一个GameException。在guess()函数中捕捉新的异常,并可能显示一条Toast消息。提示:使用if (num.text.toString().toInt() < Constants.LOWER_BOUND) throw ...if (num.text.toString().toInt() > Constants.UPPER_BOUND) throw ...进行检查。

表达式中的异常

Kotlin 的一个有趣特性是,可以在表达式中使用 try-catch 块和 throw 语句。try-catch 块的结果是try{ }catch(...){ }块中最后一行的值,这取决于异常是否被捕获。例如,如果出现问题,您可以将它用作默认值。在…里

val x = try{ arr[ind] }
      catch(e:ArrayIndexOutOfBoundsException) { -1 }

对于一些名为arrIntArray,如果违反了数组边界限制,变量x将获得默认值1

警告

注意不要滥用 try-catch 块来处理一些异常的但却是预期的程序流路径。你真的应该只对意料之外的问题使用异常。

一个throw someException也有一个值。它属于Nothing类型,在 Kotlin 类型层次结构中是所有事物的子类。因此有可能写

val v = map[someKey] ?: throw Exception("no such key in the map")

注意算子?:(有时被称为埃尔维斯算子)只有在左侧产生null时才对右侧求值;否则它走左边。这意味着如果map[someKey]的值为null,相当于地图没有这个键,那么就会抛出异常。