当前位置:网站首页 > 更多 > 玩电脑 > 正文

[玩转系统] 您想了解的有关异常的所有信息

作者:精品下载站 日期:2024-12-14 03:04:11 浏览:15 分类:玩电脑

您想了解的有关异常的所有信息


当涉及到编写代码时,错误处理只是生活的一部分。我们经常可以检查和验证预期行为的条件。当意外发生时,我们转向异常处理。您可以轻松处理其他人的代码生成的异常,也可以生成自己的异常供其他人处理。

笔记

本文的原始版本出现在@KevinMarquette 撰写的博客上。 PowerShell 团队感谢 Kevin 与我们分享这些内容。请查看他的博客:PowerShellExplained.com。

基本术语

在我们开始讨论这个问题之前,我们需要先了解一些基本术语。

例外

异常就像正常错误处理无法处理问题时创建的事件。尝试将数字除以零或内存不足都是导致异常的示例。有时,您所使用的代码的作者会在某些问题发生时创建异常。

投掷和接住

当异常发生时,我们说抛出异常。要处理抛出的异常,您需要捕获它。如果抛出异常并且没有被某些东西捕获,则脚本将停止执行。

调用堆栈

调用堆栈是相互调用的函数的列表。当一个函数被调用时,它会被添加到堆栈或列表的顶部。当函数退出或返回时,它将从堆栈中删除。

当引发异常时,将检查该调用堆栈,以便异常处理程序捕获它。

终止和非终止错误

异常通常是终止错误。抛出的异常要么被捕获,要么终止当前执行。默认情况下,Write-Error 会生成非终止错误,并将错误添加到输出流而不引发异常。

我指出这一点是因为 Write-Error 和其他非终止错误不会触发 catch

吞掉异常

这是当您捕获错误只是为了抑制它时。请谨慎执行此操作,因为这会使故障排除变得非常困难。

基本命令语法

以下是 PowerShell 中使用的基本异常处理语法的快速概述。

要创建我们自己的异常事件,我们使用 throw 关键字抛出异常。

function Start-Something
{
    throw "Bad thing happened"
}

这会创建一个运行时异常,即终止错误。它由调用函数中的 catch 处理,或者使用类似这样的消息退出脚本。

PS> Start-Something

Bad thing happened
At line:1 char:1
+ throw "Bad thing happened"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Bad thing happened:String) [], RuntimeException
    + FullyQualifiedErrorId : Bad thing happened

写入错误-ErrorAction 停止

我提到过,Write-Error 默认情况下不会引发终止错误。如果您指定 -ErrorAction StopWrite-Error 会生成一个终止错误,可以使用 catch 来处理该错误。

Write-Error -Message "Houston, we have a problem." -ErrorAction Stop

感谢 Lee Dailey 提醒您以这种方式使用 -ErrorAction Stop

Cmdlet -ErrorAction 停止

如果您在任何高级函数或 cmdlet 上指定 -ErrorAction Stop,它会将所有 Write-Error 语句转换为停止执行或可由 处理的终止错误捕获

Start-Something -ErrorAction Stop

有关 ErrorAction 参数的详细信息,请参阅 about_CommonParameters。有关 $ErrorActionPreference 变量的详细信息,请参阅 about_Preference_Variables。

尝试/捕捉

PowerShell(以及许多其他语言)中异常处理的工作方式是,您首先尝试一段代码,如果它抛出错误,您可以捕获它。这是一个快速示例。

try
{
    Start-Something
}
catch
{
    Write-Output "Something threw an exception"
    Write-Output $_
}

try
{
    Start-Something -ErrorAction Stop
}
catch
{
    Write-Output "Something threw an exception or used Write-Error"
    Write-Output $_
}

catch 脚本仅在出现终止错误时运行。如果 try 正确执行,则会跳过 catch。您可以使用 $_ 变量访问 catch 块中的异常信息。

尝试/最后

有时,您不需要处理错误,但仍然需要在异常发生与否时执行一些代码。 finally 脚本正是这样做的。

看一下这个例子:

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()

每当您打开或连接到资源时,都应该将其关闭。如果 ExecuteNonQuery() 引发异常,则连接不会关闭。这是 try/finally 块内的相同代码。

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
    $command.Connection.Open()
    $command.ExecuteNonQuery()
}
finally
{
    $command.Connection.Close()
}

在此示例中,如果出现错误,连接将关闭。如果没有错误,它也会关闭。 finally 脚本每次都会运行。

因为您没有捕获异常,所以它仍然会在调用堆栈中传播。

尝试/抓住/最后

catchfinally 一起使用是完全有效的。大多数时候您会使用其中之一,但您可能会发现同时使用两者的场景。

$PS项目

现在我们已经了解了基础知识,我们可以更深入地挖掘。

catch 块内,有一个 ErrorRecord 类型的自动变量($PSItem$_),其中包含有关异常的详细信息。以下是一些关键属性的快速概述。

对于这些示例,我在 ReadAllText 中使用了无效路径来生成此异常。

[System.IO.File]::ReadAllText( '\test\no\filefound.log')

PSItem.ToString()

这为您提供了在日志记录和一般输出中使用的最干净的消息。如果将 $PSItem 放置在字符串内,则会自动调用 ToString()

catch
{
    Write-Output "Ran into an issue: $($PSItem.ToString())"
}

catch
{
    Write-Output "Ran into an issue: $PSItem"
}

$PSItem.InspirationInfo

此属性包含 PowerShell 收集的有关引发异常的函数或脚本的附加信息。以下是我创建的示例异常中的 InitationInfo

PS> $PSItem.InvocationInfo | Format-List *

MyCommand             : Get-Resource
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 5
OffsetInLine          : 5
ScriptName            : C:\blog\throwerror.ps1
Line                  :     Get-Resource
PositionMessage       : At C:\blog\throwerror.ps1:5 char:5
                        +     Get-Resource
                        +     ~~~~~~~~~~~~
PSScriptRoot          : C:\blog
PSCommandPath         : C:\blog\throwerror.ps1
InvocationName        : Get-Resource

这里的重要细节显示了 ScriptName、代码的 Line 以及调用开始处的 ScriptLineNumber

$PSItem.ScriptStackTrace

此属性显示使您到达生成异常的代码的函数调用顺序。

PS> $PSItem.ScriptStackTrace
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Start-Something, C:\blog\throwerror.ps1: line 5
at <ScriptBlock>, C:\blog\throwerror.ps1: line 18

我只在同一脚本中调用函数,但是如果涉及多个脚本,这将跟踪调用。

$PSItem.Exception

这是实际引发的异常。

$PSItem.Exception.Message

这是描述异常的一般消息,是故障排除时的良好起点。大多数异常都有默认消息,但也可以在引发异常时设置为自定义消息。

PS> $PSItem.Exception.Message

Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."

如果 ErrorRecord 上没有设置,这也是调用 $PSItem.ToString() 时返回的消息。

$PSItem.Exception.InnerException

异常可以包含内部异常。当您调用的代码捕获异常并引发不同的异常时,通常会出现这种情况。原始异常被放置在新异常中。

PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.

稍后当我谈论重新抛出异常时我会再次讨论这一点。

$PSItem.Exception.StackTrace

这是异常的StackTrace。我在上面展示了一个 ScriptStackTrace,但这个是用于调用托管代码的。

at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean
 useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs,
 String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32
 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean
 checkHost)
at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks,
 Int32 bufferSize, Boolean checkHost)
at System.IO.File.InternalReadAllText(String path, Encoding encoding, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )

仅当从托管代码引发事件时,您才会获得此堆栈跟踪。我直接调用 .NET 框架函数,因此这就是我们在此示例中看到的全部内容。通常,当您查看堆栈跟踪时,您正在寻找代码停止的位置和系统调用开始的位置。

处理异常

异常除了基本语法和异常属性之外还有更多内容。

捕获类型异常

您可以选择性地排除您发现的例外情况。异常有一个类型,您可以指定要捕获的异常类型。

try
{
    Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
    Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
        Write-Output "IO error with the file: $path"
}

检查每个 catch 块的异常类型,直到找到与您的异常匹配的异常类型。重要的是要认识到异常可以从其他异常继承。在上面的示例中,FileNotFoundException 继承自IOException。因此,如果 IOException 是第一个,那么它将被调用。即使存在多个匹配项,也只会调用一个 catch 块。

如果我们有一个 System.IO.PathTooLongException ,则 IOException 会匹配,但如果我们有一个 InsufficientMemoryException ,那么什么都不会捕获它并且它会传播向上堆栈。

一次捕获多种类型

可以使用相同的 catch 语句捕获多种异常类型。

try
{
    Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException],[System.IO.FileNotFoundException]
{
    Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
    Write-Output "IO error with the file: [$path]"
}

感谢 Reddit 用户 u/Sheppard_Ra 建议添加此内容。

抛出类型异常

您可以在 PowerShell 中引发类型化异常。而不是使用字符串调用 throw

throw "Could not find: $path"

使用这样的异常加速器:

throw [System.IO.FileNotFoundException] "Could not find: $path"

但是当您这样做时,您必须指定一条消息。

您还可以创建要抛出的异常的新实例。当您执行此操作时,该消息是可选的,因为系统对所有内置异常都有默认消息。

throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")

如果您不使用 PowerShell 5.0 或更高版本,则必须使用旧的 New-Object 方法。

throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")

通过使用类型化异常,您(或其他人)可以按上一节中提到的类型捕获异常。

写入错误异常

我们可以将这些类型异常添加到Write-Error中,并且我们仍然可以通过异常类型来捕获错误。使用Write-Error,如以下示例所示:

# with normal message
Write-Error -Message "Could not find path: $path" -Exception ([System.IO.FileNotFoundException]::new()) -ErrorAction Stop

# With message inside new exception
Write-Error -Exception ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop

# Pre PS 5.0
Write-Error -Exception ([System.IO.FileNotFoundException]"Could not find path: $path") -ErrorAction Stop

Write-Error -Message "Could not find path: $path" -Exception (New-Object -TypeName System.IO.FileNotFoundException) -ErrorAction Stop

然后我们可以这样捕获它:

catch [System.IO.FileNotFoundException]
{
    Write-Log $PSItem.ToString()
}

.NET 异常的大列表

我在 Reddit r/PowerShell 社区的帮助下编译了一个主列表,其中包含数百个 .NET 异常,以补充本文。

  • .NET 异常的大列表

我首先在该列表中搜索那些感觉非常适合我的情况的例外情况。您应该尝试在基本 System 命名空间中使用异常。

异常是对象

如果您开始使用大量类型化异常,请记住它们是对象。不同的异常有不同的构造函数和属性。如果我们查看 System.IO.FileNotFoundException 的 FileNotFoundException 文档,我们会发现我们可以传入消息和文件路径。

[System.IO.FileNotFoundException]::new("Could not find file", $path)

它有一个 FileName 属性来公开该文件路径。

catch [System.IO.FileNotFoundException]
{
    Write-Output $PSItem.Exception.FileName
}

您应该查阅 .NET 文档以了解其他构造函数和对象属性。

重新抛出异常

如果您在 catch 块中要做的只是抛出相同的异常,那么就不要catch它。您应该只捕获您计划在发生时处理或执行某些操作的异常。

有时您想要对异常执行操作,但要重新抛出异常,以便下游可以处理它。我们可以在发现问题的地方附近写一条消息或记录问题,但在堆栈中进一步处理问题。

catch
{
    Write-Log $PSItem.ToString()
    throw $PSItem
}

有趣的是,我们可以从 catch 中调用 throw,它会重新抛出当前异常。

catch
{
    Write-Log $PSItem.ToString()
    throw
}

我们希望重新抛出异常以保留原始执行信息,例如源脚本和行号。如果我们此时抛出一个新的异常,它会隐藏异常开始的位置。

重新抛出新的异常

如果您捕获一个异常但想抛出另一个异常,那么您应该将原始异常嵌套在新异常中。这允许堆栈下方的人员将其作为 $PSItem.Exception.InnerException 进行访问。

catch
{
    throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}

$PSCmdlet.ThrowTerminateError()

我不喜欢对原始异常使用 throw 的一件事是错误消息指向 throw 语句并指示该行是问题所在。

Unable to find the specified file.
At line:31 char:9
+         throw [System.IO.FileNotFoundException]::new()
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], FileNotFoundException
    + FullyQualifiedErrorId : Unable to find the specified file.

如果错误消息告诉我我的脚本已损坏,因为我在第 31 行调用了 throw,那么对于脚本的用户来说,这是一条不好的消息。它没有告诉他们任何有用的东西。

Dexter Dhami 指出我可以使用 ThrowTerminateError() 来纠正这个问题。

$PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
        ([System.IO.FileNotFoundException]"Could not find $Path"),
        'My.ID',
        [System.Management.Automation.ErrorCategory]::OpenError,
        $MyObject
    )
)

如果我们假设在名为 Get-Resource 的函数内调用了 ThrowTerminateError(),那么这就是我们将看到的错误。

Get-Resource : Could not find C:\Program Files (x86)\Reference
Assemblies\Microsoft\Framework\.NETPortable\v4.6\System.IO.xml
At line:6 char:5
+     Get-Resource -Path $Path
+     ~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (:) [Get-Resource], FileNotFoundException
    + FullyQualifiedErrorId : My.ID,Get-Resource

您是否看到它如何指出 Get-Resource 函数是问题的根源?这告诉用户一些有用的信息。

因为$PSItem是一个ErrorRecord,所以我们也可以使用ThrowTerminateError这种方式来重新抛出。

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

这会将错误源更改为 Cmdlet,并向 Cmdlet 用户隐藏函数的内部结构。

尝试可能会产生终止错误

Kirk Munro 指出,某些异常仅在 try/catch 块内执行时才终止错误。这是他给我的示例,该示例生成除以零的运行时异常。

function Start-Something { 1/(1-1) }

然后像这样调用它以查看它生成错误并仍然输出消息。

&{ Start-Something; Write-Output "We did it. Send Email" }

但是,通过将相同的代码放入 try/catch 中,我们会看到其他情况发生。

try
{
    &{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
    Write-Output "Notify Admin to fix error and send email"
}

我们看到错误变成了终止错误并且不输出第一条消息。我不喜欢这个的地方是,您可以在函数中包含此代码,但如果有人使用 try/catch,它的行为会有所不同。

我自己没有遇到过这个问题,但这是需要注意的极端情况。

try/catch 中的 $PSCmdlet.ThrowTerminateError()

$PSCmdlet.ThrowTerminateError() 的一个细微差别是,它会在 Cmdlet 内创建终止错误,但在离开 Cmdlet 后会变成非终止错误。这给函数的调用者留下了决定如何处理错误的负担。他们可以使用 -ErrorAction Stop 或从 try{...}catch{...} 中调用它,将其转回终止错误。

公共函数模板

我与 Kirk Munro 交谈时的最后一个方法是,他在每个 begin 周围放置一个 try{...}catch{...} >processend 块在他的所有高级功能中。在这些通用的 catch 块中,他使用 $PSCmdlet.ThrowTerminateError($PSItem) 来处理离开其函数的所有异常。

function Start-Something
{
    [CmdletBinding()]
    param()

    process
    {
        try
        {
            ...
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

因为所有内容都在其函数内的 try 语句中,所以所有内容的行为都是一致的。这也为最终用户提供了干净的错误,从而隐藏了生成错误的内部代码。

陷阱

我重点关注异常的 try/catch 方面。但在结束之前我需要提及一个遗留功能。

陷阱被放置在脚本或函数中以捕获该范围内发生的所有异常。当异常发生时,会执行trap中的代码,然后继续正常的代码。如果发生多个异常,则陷阱会被一遍又一遍地调用。

trap
{
    Write-Log $PSItem.ToString()
}

throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')

我个人从未采用过这种方法,但我可以在记录任何和所有异常的管理或控制器脚本中看到值,然后仍然继续执行。

结束语

向脚本添加适当的异常处理不仅可以使它们更加稳定,还可以使您更轻松地排除这些异常。

我花了很多时间谈论抛出,因为它是谈论异常处理时的核心概念。 PowerShell 还为我们提供了 Write-Error 来处理所有需要使用 throw 的情况。因此,阅读本文后,不要认为您需要使用 throw

现在我已经花时间详细介绍了异常处理,我将转而使用 Write-Error -Stop 在我的代码中生成错误。我还将采纳 Kirk 的建议,将 ThrowTerminateError 设置为每个函数的 goto 异常处理程序。

您需要 登录账户 后才能发表评论

取消回复欢迎 发表评论:

关灯