about_Classes_Methods

简短说明

介绍如何定义 PowerShell 类的方法。

长说明

方法定义类可以执行的操作。 方法可以采用指定输入数据的参数。 方法始终定义输出类型。 如果方法未返回任何输出,则它必须具有 Void 输出类型。 如果方法未显式定义输出类型,则该方法的输出类型为 Void

在类方法中,除语句中指定的 return 对象外,不会向管道发送任何对象。 代码不会意外输出到管道。

注意

这与 PowerShell 函数处理输出的方式有根本的不同,在 PowerShell 中,所有内容都进入管道。

从类方法内部写入错误流的非确定性错误不会通过。 必须使用 throw 来显示终止错误。 使用 Write-* cmdlet,仍可以从类方法中写入 PowerShell 的输出流。 cmdlet 遵循 调用范围中的首选项变量 。 但是,应避免使用 Write-* cmdlet,以便该方法仅使用 return 语句输出对象。

类方法可以使用自动变量访问当前类中定义的属性和其他方法来引用类对象的 $this 当前实例。 自动 $this 变量在静态方法中不可用。

类方法可以具有任意数量的属性,包括 隐藏 属性和 静态 属性。

语法

类方法使用以下语法:

单行语法

[[<attribute>]...] [hidden] [static] [<output-type>] <method-name> ([<method-parameters>]) { <body> }

多行语法

[[<attribute>]...]
[hidden]
[static]
[<output-type>] <method-name> ([<method-parameters>]) {
  <body>
}

示例

示例 1 - 最小方法定义

GetVolume() ExampleCube1 类的方法返回多维数据集的卷。 它将输出类型定义为浮点数,并返回将实例的高度长度Width 属性相乘的结果。

class ExampleCube1 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    [float] GetVolume() { return $this.Height * $this.Length * $this.Width }
}

$box = [ExampleCube1]@{
    Height = 2
    Length = 2
    Width  = 3
}

$box.GetVolume()
12

示例 2 - 具有参数的方法

该方法 GeWeight() 采用多维数据集密度的浮点数输入,并返回多维数据集的权重,计算为卷乘以密度。

class ExampleCube2 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    [float] GetVolume() { return $this.Height * $this.Length * $this.Width }
    [float] GetWeight([float]$Density) {
        return $this.GetVolume() * $Density
    }
}

$cube = [ExampleCube2]@{
    Height = 2
    Length = 2
    Width  = 3
}

$cube.GetWeight(2.5)
30

示例 3 - 不带输出的方法

此示例将 Validate() 输出类型定义为 System.Void 的方法。 此方法不返回任何输出。 相反,如果验证失败,则会引发错误。 该方法 GetVolume() 在计算多维数据集的卷之前调用 Validate() 。 如果验证失败,则该方法在计算之前终止。

class ExampleCube3 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    [float] GetVolume() {
        $this.Validate()

        return $this.Height * $this.Length * $this.Width
    }

    [void] Validate() {
        $InvalidProperties = @()
        foreach ($Property in @('Height', 'Length', 'Width')) {
            if ($this.$Property -le 0) {
                $InvalidProperties += $Property
            }
        }

        if ($InvalidProperties.Count -gt 0) {
            $Message = @(
                'Invalid cube properties'
                "('$($InvalidProperties -join "', '")'):"
                "Cube dimensions must all be positive numbers."
            ) -join ' '
            throw $Message
        }
    }
}

$Cube = [ExampleCube3]@{ Length = 1 ; Width = -1 }
$Cube

$Cube.GetVolume()
Height Length Width
------ ------ -----
  0.00   1.00 -1.00

Exception:
Line |
  20 |              throw $Message
     |              ~~~~~~~~~~~~~~
     | Invalid cube properties ('Height', 'Width'): Cube dimensions must
     | all be positive numbers.

该方法会引发异常,因为 HeightWidth 属性无效,从而阻止类计算当前卷。

示例 4 - 具有重载的静态方法

ExampleCube4 类使用两个重载定义静态方法GetVolume()。 第一个重载具有多维数据集维度的参数和一个标志,用于指示该方法是否应验证输入。

第二个重载仅包括数值输入。 它将第一个重载 $Static 调用为 $true. 第二个重载使用户能够调用该方法,而无需始终定义是否严格验证输入。

该类还定义为 GetVolume() 实例(非静态)方法。 此方法调用第二个静态重载,确保在返回输出值之前,实例 GetVolume() 方法始终验证多维数据集的维度。

class ExampleCube4 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    static [float] GetVolume(
        [float]$Height,
        [float]$Length,
        [float]$Width,
        [boolean]$Strict
    ) {
        $Signature = "[ExampleCube4]::GetVolume({0}, {1}, {2}, {3})"
        $Signature = $Signature -f $Height, $Length, $Width, $Strict
        Write-Verbose "Called $Signature"

        if ($Strict) {
            [ValidateScript({$_ -gt 0 })]$Height = $Height
            [ValidateScript({$_ -gt 0 })]$Length = $Length
            [ValidateScript({$_ -gt 0 })]$Width  = $Width
        }

        return $Height * $Length * $Width
    }

    static [float] GetVolume([float]$Height, [float]$Length, [float]$Width) {
        $Signature = "[ExampleCube4]::GetVolume($Height, $Length, $Width)"
        Write-Verbose "Called $Signature"

        return [ExampleCube4]::GetVolume($Height, $Length, $Width, $true)
    }

    [float] GetVolume() {
        Write-Verbose "Called `$this.GetVolume()"
        return [ExampleCube4]::GetVolume(
            $this.Height,
            $this.Length,
            $this.Width
        )
    }
}

$VerbosePreference = 'Continue'
$Cube = [ExampleCube4]@{ Height = 2 ; Length = 2 }
$Cube.GetVolume()
VERBOSE: Called $this.GetVolume()
VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0)
VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0, True)

MetadataError:
Line |
  19 |              [ValidateScript({$_ -gt 0 })]$Width  = $Width
     |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The variable cannot be validated because the value 0 is not a valid
     | value for the Width variable.

方法定义中的详细消息显示调用静态方法的初始调用 $this.GetVolume() 方式。

使用 Strict 参数直接调用静态方法作为 $false 卷的 0 返回。

[ExampleCube4]::GetVolume($Cube.Height, $Cube.Length, $Cube.Width, $false)
VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0, False)
0

方法签名和重载

每个类方法都有一个唯一的签名,用于定义如何调用该方法。 方法的输出类型、名称和参数定义方法签名。

当类定义多个具有相同名称的方法时,该方法的定义是 重载。 方法的重载必须具有不同的参数。 即使输出类型不同,方法也不能使用相同的参数定义两个实现。

以下类定义两种方法, Shuffle() 以及 Deal(). 该方法 Deal() 定义两个重载,一个没有任何参数,另一个使用 Count 参数。

class CardDeck {
    [string[]]$Cards  = @()
    hidden [string[]]$Dealt  = @()
    hidden [string[]]$Suits  = @('Clubs', 'Diamonds', 'Hearts', 'Spades')
    hidden [string[]]$Values = 2..10 + @('Jack', 'Queen', 'King', 'Ace')

    CardDeck() {
        foreach($Suit in $this.Suits) {
            foreach($Value in $this.Values) {
                $this.Cards += "$Value of $Suit"
            }
        }
        $this.Shuffle()
    }

    [void] Shuffle() {
        $this.Cards = $this.Cards + $this.Dealt | Where-Object -FilterScript {
             -not [string]::IsNullOrEmpty($_)
        } | Get-Random -Count $this.Cards.Count
    }

    [string] Deal() {
        if ($this.Cards.Count -eq 0) { throw "There are no cards left." }

        $Card        = $this.Cards[0]
        $this.Cards  = $this.Cards[1..$this.Cards.Count]
        $this.Dealt += $Card

        return $Card
    }

    [string[]] Deal([int]$Count) {
        if ($Count -gt $this.Cards.Count) {
            throw "There are only $($this.Cards.Count) cards left."
        } elseif ($Count -lt 1) {
            throw "You must deal at least 1 card."
        }

        return (1..$Count | ForEach-Object { $this.Deal() })
    }
}

方法输出

默认情况下,方法没有任何输出。 如果方法签名包含 Void 以外的显式输出类型,该方法必须返回该类型的对象。 除非关键字 (keyword)显式返回对象,return否则方法不会发出任何输出。

方法参数

类方法可以定义要在方法正文中使用的输入参数。 方法参数在括号内,并且用逗号分隔。 空括号指示方法不需要任何参数。

可以在单行或多行上定义参数。 以下块显示了方法参数的语法。

([[<parameter-type>]]$<parameter-name>[, [[<parameter-type>]]$<parameter-name>])
(
    [[<parameter-type>]]$<parameter-name>[,
    [[<parameter-type>]]$<parameter-name>]
)

方法参数可以强类型化。 如果未键入参数,该方法接受该参数的任何对象。 如果键入参数,该方法将尝试将该参数的值转换为正确的类型,如果无法转换输入,则会引发异常。

方法参数不能定义默认值。 所有方法参数都是必需的。

方法参数不能具有任何其他属性。 这可以防止方法将参数与属性一起使用 Validate* 。 有关验证属性的详细信息,请参阅 about_Functions_Advanced_Parameters

可以使用以下模式之一将验证添加到方法参数:

  1. 使用所需的验证属性将参数重新分配给同一变量。 这适用于静态和实例方法。 有关此模式的示例,请参阅 示例 4
  2. 用于 Update-TypeData 定义 ScriptMethod 直接使用参数上的验证属性。 这仅适用于实例方法。 有关详细信息,请参阅 “使用 Update-TypeData 定义实例方法”部分。

方法中的自动变量

并非所有自动变量都可用于方法。 以下列表包括自动变量和建议,这些变量以及如何在 PowerShell 类方法中使用它们。 列表中未包含的自动变量对类方法不可用。

  • $_ - 正常访问。
  • $args - 请改用显式参数变量。
  • $ConsoleFileName - 按原样 $Script:ConsoleFileName 访问。
  • $Error - 正常访问。
  • $EnabledExperimentalFeatures - 按原样 $Script:EnabledExperimentalFeatures 访问。
  • $Event - 正常访问。
  • $EventArgs - 正常访问。
  • $EventSubscriber - 正常访问。
  • $ExecutionContext - 按原样 $Script:ExecutionContext 访问。
  • $false - 正常访问。
  • $foreach - 正常访问。
  • $HOME - 按原样 $Script:HOME 访问。
  • $Host - 按原样 $Script:Host 访问。
  • $input - 请改用显式参数变量。
  • $IsCoreCLR - 按原样 $Script:IsCoreCLR 访问。
  • $IsLinux - 按原样 $Script:IsLinux 访问。
  • $IsMacOS - 按原样 $Script:IsMacOS 访问。
  • $IsWindows - 按原样 $Script:IsWindows 访问。
  • $LASTEXITCODE - 正常访问。
  • $Matches - 正常访问。
  • $MyInvocation - 正常访问。
  • $NestedPromptLevel - 正常访问。
  • $null - 正常访问。
  • $PID - 按原样 $Script:PID 访问。
  • $PROFILE - 按原样 $Script:PROFILE 访问。
  • $PSBoundParameters - 不要使用此变量。 它适用于 cmdlet 和函数。 在类中使用它可能会产生意外的副作用。
  • $PSCmdlet - 不要使用此变量。 它适用于 cmdlet 和函数。 在类中使用它可能会产生意外的副作用。
  • $PSCommandPath - 正常访问。
  • $PSCulture - 按原样 $Script:PSCulture 访问。
  • $PSEdition - 按原样 $Script:PSEdition 访问。
  • $PSHOME - 按原样 $Script:PSHOME 访问。
  • $PSItem - 正常访问。
  • $PSScriptRoot - 正常访问。
  • $PSSenderInfo - 按原样 $Script:PSSenderInfo 访问。
  • $PSUICulture - 按原样 $Script:PSUICulture 访问。
  • $PSVersionTable - 按原样 $Script:PSVersionTable 访问。
  • $PWD - 正常访问。
  • $Sender - 正常访问。
  • $ShellId - 按原样 $Script:ShellId 访问。
  • $StackTrace - 正常访问。
  • $switch - 正常访问。
  • $this - 正常访问。 在类方法中, $this 始终是类的当前实例。 可以使用它访问类属性和方法。 它在静态方法中不可用。
  • $true - 正常访问。

有关自动变量的详细信息,请参阅 about_Automatic_Variables

隐藏的方法

可以通过使用hidden关键字 (keyword)声明类的方法来隐藏这些方法。 隐藏类方法包括:

  • 不包含在 cmdlet 返回 Get-Member 的类成员列表中。 若要显示隐藏的方法, Get-Member请使用 Force 参数。
  • 除非完成发生在定义隐藏方法的类中,否则不会显示在 Tab 补全或 IntelliSense 中。
  • 类的公共成员。 可以调用和继承它们。 隐藏方法不会使其成为私有方法。 它仅隐藏上述点中所述的方法。

注意

隐藏方法的任何重载时,将从 IntelliSense、完成结果和默认输出 Get-Member中删除该方法。

有关关键字 (keyword)的详细信息hidden,请参阅about_Hidden

静态方法

可以通过使用关键字 (keyword)声明方法static,将方法定义为属于类本身,而不是类的实例。 静态类方法:

  • 始终可用,独立于类实例化。
  • 在类的所有实例之间共享。
  • 始终可用。
  • 无法访问类的实例属性。 它们只能访问静态属性。
  • 整个会话范围的实时直播。

派生类方法

当类派生自基类时,它将继承基类的方法及其重载。 在基类上定义的任何方法重载(包括隐藏的方法)都可用于派生类。

派生类可以通过在类定义中重新定义继承的方法重载来替代继承的方法重载。 若要重写重载,参数类型必须与基类的类型相同。 重载的输出类型可能不同。

与构造函数不同,方法不能使用 : base(<parameters>) 语法为该方法调用基类重载。 派生类上重新定义的重载完全替换基类定义的重载。

以下示例显示了派生类上的静态和实例方法的行为。

基类定义:

  • 用于返回当前时间和DaysAgo()返回过去日期的静态方法Now()
  • 实例属性 TimeStamp 和返回 ToString() 该属性的字符串表示形式的实例方法。 这可确保在字符串中使用实例时,它会转换为日期/时间字符串而不是类名。
  • 具有两个重载的实例方法 SetTimeStamp() 。 在没有参数的情况下调用该方法时,它将 TimeStamp 设置为当前时间。 使用 DateTime 调用该方法时,它将 TimeStamp 设置为该值。
class BaseClass {
    static [datetime] Now() {
        return Get-Date
    }
    static [datetime] DaysAgo([int]$Count) {
        return [BaseClass]::Now().AddDays(-$Count)
    }

    [datetime] $TimeStamp = [BaseClass]::Now()

    [string] ToString() {
        return $this.TimeStamp.ToString()
    }

    [void] SetTimeStamp([datetime]$TimeStamp) {
        $this.TimeStamp = $TimeStamp
    }
    [void] SetTimeStamp() {
        $this.TimeStamp = [BaseClass]::Now()
    }
}

下一个块定义派生自 BaseClass 的类:

  • DerivedClassA 继承自 BaseClass ,没有任何替代。
  • DerivedClassB 重写 DaysAgo() 静态方法以返回字符串表示形式,而不是 DateTime 对象。 它还会重写 ToString() 实例方法,以将时间戳作为ISO8601日期字符串返回。
  • DerivedClassC 重写方法的 SetTimeStamp() 无参数重载,以便将时间戳设置为当前日期之前的 10 天。
class DerivedClassA : BaseClass     {}
class DerivedClassB : BaseClass     {
    static [string] DaysAgo([int]$Count) {
        return [BaseClass]::DaysAgo($Count).ToString('yyyy-MM-dd')
    }
    [string] ToString() {
        return $this.TimeStamp.ToString('yyyy-MM-dd')
    }
}
class DerivedClassC : BaseClass {
    [void] SetTimeStamp() {
        $this.SetTimeStamp([BaseClass]::Now().AddDays(-10))
    }
}

以下块显示了定义的类的静态 Now() 方法的输出。 每个类的输出都是相同的,因为派生类不会重写方法的基类实现。

"[BaseClass]::Now()     => $([BaseClass]::Now())"
"[DerivedClassA]::Now() => $([DerivedClassA]::Now())"
"[DerivedClassB]::Now() => $([DerivedClassB]::Now())"
"[DerivedClassC]::Now() => $([DerivedClassC]::Now())"
[BaseClass]::Now()     => 11/06/2023 09:41:23
[DerivedClassA]::Now() => 11/06/2023 09:41:23
[DerivedClassB]::Now() => 11/06/2023 09:41:23
[DerivedClassC]::Now() => 11/06/2023 09:41:23

下一个块调用 DaysAgo() 每个类的静态方法。 只有 DerivedClassB输出不同,因为它重写了基本实现。

"[BaseClass]::DaysAgo(3)     => $([BaseClass]::DaysAgo(3))"
"[DerivedClassA]::DaysAgo(3) => $([DerivedClassA]::DaysAgo(3))"
"[DerivedClassB]::DaysAgo(3) => $([DerivedClassB]::DaysAgo(3))"
"[DerivedClassC]::DaysAgo(3) => $([DerivedClassC]::DaysAgo(3))"
[BaseClass]::DaysAgo(3)     => 11/03/2023 09:41:38
[DerivedClassA]::DaysAgo(3) => 11/03/2023 09:41:38
[DerivedClassB]::DaysAgo(3) => 2023-11-03
[DerivedClassC]::DaysAgo(3) => 11/03/2023 09:41:38

以下块显示了每个类的新实例的字符串表示形式。 DerivedClassB表示形式不同,因为它重写了ToString()实例方法。

"`$base = [BaseClass]::New()     => $($base = [BaseClass]::New(); $base)"
"`$a    = [DerivedClassA]::New() => $($a = [DerivedClassA]::New(); $a)"
"`$b    = [DerivedClassB]::New() => $($b = [DerivedClassB]::New(); $b)"
"`$c    = [DerivedClassC]::New() => $($c = [DerivedClassC]::New(); $c)"
$base = [BaseClass]::New()     => 11/6/2023 9:44:57 AM
$a    = [DerivedClassA]::New() => 11/6/2023 9:44:57 AM
$b    = [DerivedClassB]::New() => 2023-11-06
$c    = [DerivedClassC]::New() => 11/6/2023 9:44:57 AM

下一个块调用每个实例的 SetTimeStamp() 实例方法,将 TimeStamp 属性设置为特定日期。 每个实例具有相同的日期,因为派生类都无法替代该方法的参数化重载。

[datetime]$Stamp = '2024-10-31'
"`$base.SetTimeStamp(`$Stamp) => $($base.SetTimeStamp($Stamp) ; $base)"
"`$a.SetTimeStamp(`$Stamp)    => $($a.SetTimeStamp($Stamp); $a)"
"`$b.SetTimeStamp(`$Stamp)    => $($b.SetTimeStamp($Stamp); $b)"
"`$c.SetTimeStamp(`$Stamp)    => $($c.SetTimeStamp($Stamp); $c)"
$base.SetTimeStamp($Stamp) => 10/31/2024 12:00:00 AM
$a.SetTimeStamp($Stamp)    => 10/31/2024 12:00:00 AM
$b.SetTimeStamp($Stamp)    => 2024-10-31
$c.SetTimeStamp($Stamp)    => 10/31/2024 12:00:00 AM

不带任何参数的最后一个块调用 SetTimeStamp() 。 输出显示派生类C 实例的值在其他人之前设置为 10 天。

"`$base.SetTimeStamp() => $($base.SetTimeStamp() ; $base)"
"`$a.SetTimeStamp()    => $($a.SetTimeStamp(); $a)"
"`$b.SetTimeStamp()    => $($b.SetTimeStamp(); $b)"
"`$c.SetTimeStamp()    => $($c.SetTimeStamp(); $c)"
$base.SetTimeStamp() => 11/6/2023 9:53:58 AM
$a.SetTimeStamp()    => 11/6/2023 9:53:58 AM
$b.SetTimeStamp()    => 2023-11-06
$c.SetTimeStamp()    => 10/27/2023 9:53:58 AM

使用 Update-TypeData 定义实例方法

除了直接在类定义中声明方法外,还可以使用 Update-TypeData cmdlet 为静态构造函数中的类实例定义方法。

将此代码片段用作模式的起点。 根据需要替换尖括号中的占位符文本。

class <ClassName> {
    static [hashtable[]] $MemberDefinitions = @(
        @{
            MemberName = '<MethodName>'
            MemberType = 'ScriptMethod'
            Value      = {
              param(<method-parameters>)

              <method-body>
            }
        }
    )

    static <ClassName>() {
        $TypeName = [<ClassName>].Name
        foreach ($Definition in [<ClassName>]::MemberDefinitions) {
            Update-TypeData -TypeName $TypeName @Definition
        }
    }
}

提示

Add-Member cmdlet 可以将属性和方法添加到非静态构造函数中的类,但每次调用构造函数时都会运行该 cmdlet。 在 Update-TypeData 静态构造函数中使用可确保将成员添加到类的代码只需在会话中运行一次。

使用默认参数值和验证属性定义方法

直接在类声明中定义的方法不能定义方法参数的默认值或验证属性。 若要定义具有默认值或验证属性的类方法,必须将其定义为 ScriptMethod 成员。

在此示例中,CardDeck 类定义一个Draw()方法,该方法同时使用验证属性和 Count 参数的默认值。

class CookieJar {
    [int] $Cookies = 12

    static [hashtable[]] $MemberDefinitions = @(
        @{
            MemberName = 'Eat'
            MemberType = 'ScriptMethod'
            Value      = {
                param(
                    [ValidateScript({ $_ -ge 1 -and $_ -le $this.Cookies })]
                    [int] $Count = 1
                )

                $this.Cookies -= $Count
                if ($Count -eq 1) {
                    "You ate 1 cookie. There are $($this.Cookies) left."
                } else {
                    "You ate $Count cookies. There are $($this.Cookies) left."
                }
            }
        }
    )

    static CookieJar() {
        $TypeName = [CookieJar].Name
        foreach ($Definition in [CookieJar]::MemberDefinitions) {
            Update-TypeData -TypeName $TypeName @Definition
        }
    }
}

$Jar = [CookieJar]::new()
$Jar.Eat(1)
$Jar.Eat()
$Jar.Eat(20)
$Jar.Eat(6)
You ate 1 cookie. There are 11 left.

You ate 1 cookie. There are 10 left.

MethodInvocationException:
Line |
  36 |  $Jar.Eat(20)
     |  ~~~~~~~~~~~~
     | Exception calling "Eat" with "1" argument(s): "The attribute
     | cannot be added because variable Count with value 20 would no
     | longer be valid."

You ate 6 cookies. There are 4 left.

注意

虽然此模式适用于验证属性,但请注意异常具有误导性,引用无法添加属性。 显式检查参数的值并改为引发有意义的错误可能是更好的用户体验。 这样,用户就可以理解为什么他们看到错误以及该错误该怎么做。

限制

PowerShell 类方法具有以下限制:

  • 方法参数不能使用任何属性,包括验证属性。

    解决方法:使用验证属性重新分配方法正文中的参数,或使用 cmdlet 在静态构造函数 Update-TypeData 中定义该方法。

  • 方法参数不能定义默认值。 参数始终是必需的。

    解决方法:使用 Update-TypeData cmdlet 在静态构造函数中定义方法。

  • 方法始终为公共方法,即使它们被隐藏也是如此。 当继承类时,可以重写它们。

    解决方法:无。

  • 如果隐藏了方法的任何重载,则该方法的每个重载也被视为隐藏。

    解决方法:无。

另请参阅