捕获和重新抛出异常的最佳做法是什么?

Rah*_*sad 143 php exception

应该直接重新抛出捕获的异常,还是应该绕过新的异常?

也就是说,我应该这样做:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}
Run Code Online (Sandbox Code Playgroud)

或这个:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}
Run Code Online (Sandbox Code Playgroud)

如果您的答案是直接抛出,请建议使用异常链接,我无法理解我们使用异常链接的真实场景.

Jon*_*Jon 270

除非你打算做一些有意义的事情,否则你不应该捕获异常.

"有意义的东西"可能是其中之一:

处理异常

最明显的有意义的操作是处理异常,例如通过显示错误消息并中止操作:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}
Run Code Online (Sandbox Code Playgroud)

记录或部分清理

有时您不知道如何在特定上下文中正确处理异常; 也许你缺乏关于"大图"的信息,但你确实希望将故障记录在尽可能接近发生点的位置.在这种情况下,您可能想要捕获,记录和重新抛出:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}
Run Code Online (Sandbox Code Playgroud)

一个相关的场景是您在正确的位置为失败的操作执行一些清理,而不是决定如何在顶层处理失败.在早期的PHP版本中,这将实现为

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}
Run Code Online (Sandbox Code Playgroud)

PHP 5.5引入了finally关键字,因此对于清理方案,现在有另一种方法来解决这个问题.如果清理代码无论发生什么都需要运行(即错误和成功),现在可以在透明地允许任何抛出的异常传播时执行此操作:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}
Run Code Online (Sandbox Code Playgroud)

错误抽象(异常链接)

第三种情况是,您希望在更大的保护伞下对许多可能的故障进行逻辑分组.逻辑分组的示例:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您不希望用户Component知道它是使用数据库连接实现的(可能您希望保持选项打开并在将来使用基于文件的存储).因此,您的规范Component会说"在初始化失败的情况下,ComponentInitException将被抛出".这允许消费者Component捕获预期类型的​​异常,同时还允许调试代码访问所有(依赖于实现的)细节.

提供更丰富的上下文(链接除外)

最后,在某些情况下,您可能希望为异常提供更多上下文.在这种情况下,将异常包装在另一个异常中是有意义的,该异常包含有关错误发生时您尝试执行的操作的更多信息.例如:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这种情况类似于上面的情况(可能不是最好的例子),但它说明了提供更多上下文的重点:如果抛出异常,它会告诉我们文件复制失败.但为什么会失败?此信息在包装的异常中提供(如果示例更复杂,则可能有多个级别).

如果您考虑一个场景,例如创建一个UserProfile对象导致文件被复制,因为用户配置文件存储在文件中并且它支持事务语义:您可以"撤消"更改,因为它们仅在您提交之前的个人资料副本.

在这种情况下,如果你这样做了

try {
    $profile = UserProfile::getInstance();
}
Run Code Online (Sandbox Code Playgroud)

并且因此捕获了"无法创建目标目录"异常错误,您将有权被混淆.在提供上下文的其他异常层中包含此"核心"异常将使错误更容易处理("创建配置文件复制失败" - >"文件复制操作失败" - >"无法创建目标目录").

  • 我认为你错过了这里的名单是有原因的 - 你可能无法判断你是否可以处理异常,直到你抓住它并有机会检查它.例如,使用错误代码(并且有数以万计)的低级API的包装器可能有一个异常类,它会抛出任何错误的实例,并且可以检查`error_code`属性底层错误代码.如果你只能有意义地处理其中的一些错误,那么你可能想要捕获,检查,如果你不能处理错误 - 重新抛出. (9认同)
  • 最后,PHP 5.5现在终于实现了. (3认同)

irc*_*ell 34

嗯,这都是关于维护抽象的.所以我建议使用异常链接直接抛出.至于原因,让我解释泄漏抽象的概念

假设您正在构建模型.该模型应该从应用程序的其余部分抽象出所有数据持久性和验证.那么现在当你收到数据库错误时会发生什么?如果你重新抛出DatabaseQueryException,你就会泄露抽象.要理解为什么,请考虑一下抽象.您不关心模型如何存储数据,只关心它.同样,你并不关心模型底层系统出了什么问题,只是你知道出了什么问题,大概出了什么问题.

因此,通过重新抛出DatabaseQueryException,您将泄露抽象并要求调用代码理解模型下正在发生的事情的语义.相反,创建一个泛型ModelStorageException,并将捕获的DatabaseQueryException内部包装起来.这样,您的调用代码仍然可以尝试在语义上处理错误,但是模型的基础技术并不重要,因为您只是暴露了该抽象层的错误.更好的是,因为你包装了异常,如果它一直冒泡并且需要记录,你可以追踪抛出的根异常(走链),这样你仍然可以获得所需的所有调试信息!

除非您需要进行一些后处理,否则不要简单地捕获并重新抛出相同的异常.但是像一块一样} catch (Exception $e) { throw $e; }没有意义.但是你可以重新包装异常以获得一些重要的抽象增益.

  • 很好的答案.看起来很多人围绕Stack Overflow(基于答案等)有点使用它们是错误的. (2认同)

Cle*_*man 9

恕我直言,抓住一个例外只是重新抛出它是没用的.在这种情况下,只是不要捕获它,让之前调用的方法处理它(也就是调用堆栈中'upper'的方法).

如果你重新抛出它,将捕获的异常链接到你将抛出的新异常绝对是一个好习惯,因为它将保留捕获的异常包含的信息.但是,重新抛出它只有在添加一些信息或处理捕获的异常时才有用,可能是某些上下文,值,日志记录,释放资源等等.

添加一些信息的方式是扩展Exception类,有像例外NullParameterException,DatabaseException更结束了,等等,这让developper只赶上一些例外,他可以处理.例如,一个人只能捕获DatabaseException并尝试解决导致该问题的原因Exception,例如重新连接到数据库.

  • 它不是没有用的,有时您需要在抛出异常的函数中执行某些操作,然后将其重新抛出以使更高级别的捕获执行其他操作。在我正在从事的一个项目中,有时我们会在操作方法中捕获异常,向用户显示友好通知,然后将其重新抛出,因此代码中更远的try catch块可以再次捕获该错误以将错误记录到一个日志。 (2认同)
  • 好吧,如果您需要关闭资源,可以重新抛出它,但不需要添加其他信息.我同意这不是世界上最干净的东西,但它不是*可怕的* (2认同)
  • @ircmaxell同意,已进行编辑以反映出仅当您**除了将其重新扔掉之外不做任何事情**时它才是无用的 (2认同)