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

[玩转系统] 关于类方法

作者:精品下载站 日期:2024-12-14 02:24:08 浏览:10 分类:玩电脑

关于类方法


简短描述

描述如何定义 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 - 最小方法定义

ExampleCube1 类的 GetVolume() 方法返回立方体的体积。它将输出类型定义为浮点数,并返回实例的 HeightLengthWidth 属性相乘的结果。

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 - 无输出的方法

此示例定义输出类型为 System.VoidValidate() 方法。此方法不返回任何输出。相反,如果验证失败,则会抛出错误。 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 以外的显式输出类型,则该方法必须返回该类型的对象。除非 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 - 正常访问。
  • $MyInspiration - 正常访问。
  • $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 关键字声明类的方法来隐藏它们。隐藏类方法有:

  • 不包含在 Get-Member cmdlet 返回的类成员列表中。要使用 Get-Member 显示隐藏方法,请使用 Force 参数。
  • 除非完成发生在定义隐藏方法的类中,否则不会在选项卡完成或 IntelliSense 中显示。
  • 班级的公共成员。它们可以被调用和继承。隐藏方法并不意味着它是私有的。它仅隐藏前面几点中描述的方法。

笔记

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

有关 hidden 关键字的更多信息,请参阅 about_Hidden。

静态方法

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

  • 始终可用,独立于类实例化。
  • 在类的所有实例之间共享。
  • 随时可用。
  • 无法访问类的实例属性。他们只能访问静态属性。
  • 在整个会话期间保持活跃。

派生类方法

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

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

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

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

基类定义:

  • 静态方法 Now() 用于返回当前时间,DaysAgo() 用于返回过去的日期。
  • 实例属性 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(),不带任何参数。输出显示 DerivedClassC 实例的值设置为比其他实例早 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 类方法具有以下限制:

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

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

  • 方法参数不能定义默认值。参数始终是强制性的。

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

  • 方法始终是公开的,即使它们是隐藏的。当类被继承时,它们可以被覆盖。

    解决方法:无。

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

    解决方法:无。

参见

  • about_Automatic_Variables
  • 关于课程
  • about_类_构造函数
  • about_Classes_Inheritance
  • about_Classes_Properties
  • 关于_使用

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

取消回复欢迎 发表评论:

关灯