about_Classes_Inheritance

简短说明

介绍如何定义扩展其他类型的类。

长说明

PowerShell 类支持 继承,这使你可以定义可重用(继承)、扩展或修改父类行为的子类。 成员被继承的类称为基类。 继承基类成员的类称为派生类

PowerShell 仅支持单一继承。 类只能继承自单个类。 不过,继承是可传递的。这样一来,就可以为一组类型定义继承层次结构。 换句话说,类型 D 可以继承自类型 C,该类型继承自类型 B,后者继承自基类类型 A。由于继承是可传递的,A 类型的成员可用于类型 D

派生类不会继承基类的所有成员。 不会继承以下成员:

  • 静态构造函数:用于初始化类的静态数据。
  • 实例构造函数:在创建类的新实例时调用。 每个类都必须定义自己的构造函数。

可以通过创建派生自现有类的新类来扩展类。 派生类继承基类的属性和方法。 可以根据需要添加或替代基类成员。

类也可以继承自定义协定的接口。 继承自接口的类必须实现该协定。 这样做时,该类与实现该接口的任何其他类一样可用。 如果类继承自接口但未实现接口,则 PowerShell 会引发类分析错误。

某些 PowerShell 运算符依赖于实现特定接口的类。 例如, -eq 运算符仅检查引用相等性,除非类实现 System.IEquatable 接口。 -le-lt-gt -ge运算符仅适用于实现 System.IComparable 接口的类。

派生类使用 : 语法扩展基类或实现接口。 派生类应始终位于类声明的最左侧。

此示例显示基本的 PowerShell 类继承语法。

Class Derived : Base {...}

此示例显示了基类之后带有接口声明的继承。

Class Derived : Base, Interface {...}

语法

类继承使用以下语法:

一行语法

class <derived-class-name> : <base-class-or-interface-name>[, <interface-name>...] {
    <derived-class-body>
}

例如:

# Base class only
class Derived : Base {...}
# Interface only
class Derived : System.IComparable {...}
# Base class and interface
class Derived : Base, System.IComparable {...}

多行语法

class <derived-class-name> : <base-class-or-interface-name>[,
    <interface-name>...] {
    <derived-class-body>
}

例如:

class Derived : Base,
                System.IComparable,
                System.IFormattable,
                System.IConvertible {
    # Derived class definition
}

示例

示例 1 - 从基类继承和重写

以下示例演示了继承属性的行为,且不重写。 读取代码块的说明后按顺序运行代码块。

定义基类

第一个代码块将 PublishedWork 定义为基类。 它具有两个静态属性: ListArtists。 接下来,它定义将作品添加到静态 List 属性和艺术家到 Artists 属性的静态RegisterWork()方法,并为列表中的每个新条目编写一条消息。

该类定义描述已发布工作的三个实例属性。 最后,它定义 Register()ToString() 实例方法。

class PublishedWork {
    static [PublishedWork[]] $List    = @()
    static [string[]]        $Artists = @()

    static [void] RegisterWork([PublishedWork]$Work) {
        $wName   = $Work.Name
        $wArtist = $Work.Artist
        if ($Work -notin [PublishedWork]::List) {
            Write-Verbose "Adding work '$wName' to works list"
            [PublishedWork]::List += $Work
        } else {
            Write-Verbose "Work '$wName' already registered."
        }
        if ($wArtist -notin [PublishedWork]::Artists) {
            Write-Verbose "Adding artist '$wArtist' to artists list"
            [PublishedWork]::Artists += $wArtist
        } else {
            Write-Verbose "Artist '$wArtist' already registered."
        }
    }

    static [void] ClearRegistry() {
        Write-Verbose "Clearing PublishedWork registry"
        [PublishedWork]::List    = @()
        [PublishedWork]::Artists = @()
    }

    [string] $Name
    [string] $Artist
    [string] $Category

    [void] Init([string]$WorkType) {
        if ([string]::IsNullOrEmpty($this.Category)) {
            $this.Category = "${WorkType}s"
        }
    }

    PublishedWork() {
        $WorkType = $this.GetType().FullName
        $this.Init($WorkType)
        Write-Verbose "Defined a published work of type [$WorkType]"
    }

    PublishedWork([string]$Name, [string]$Artist) {
        $WorkType    = $this.GetType().FullName
        $this.Name   = $Name
        $this.Artist = $Artist
        $this.Init($WorkType)

        Write-Verbose "Defined '$Name' by $Artist as a published work of type [$WorkType]"
    }

    PublishedWork([string]$Name, [string]$Artist, [string]$Category) {
        $WorkType    = $this.GetType().FullName
        $this.Name   = $Name
        $this.Artist = $Artist
        $this.Init($WorkType)

        Write-Verbose "Defined '$Name' by $Artist ($Category) as a published work of type [$WorkType]"
    }

    [void]   Register() { [PublishedWork]::RegisterWork($this) }
    [string] ToString() { return "$($this.Name) by $($this.Artist)" }
}

定义不带重写的派生类

第一个派生类是 Album。 它不会重写任何属性或方法。 它添加了基类上不存在的新实例属性 流派

class Album : PublishedWork {
    [string[]] $Genres   = @()
}

以下代码块显示派生 的 Album 类的行为。 首先,它设置 $VerbosePreference 类方法发出的消息发送到控制台。 它创建类的三个实例,在表中显示它们,然后使用继承的静态 RegisterWork() 方法注册它们。 然后,它直接在基类上调用相同的静态方法。

$VerbosePreference = 'Continue'
$Albums = @(
    [Album]@{
        Name   = 'The Dark Side of the Moon'
        Artist = 'Pink Floyd'
        Genres = 'Progressive rock', 'Psychedelic rock'
    }
    [Album]@{
        Name   = 'The Wall'
        Artist = 'Pink Floyd'
        Genres = 'Progressive rock', 'Art rock'
    }
    [Album]@{
        Name   = '36 Chambers'
        Artist = 'Wu-Tang Clan'
        Genres = 'Hip hop'
    }
)

$Albums | Format-Table
$Albums | ForEach-Object { [Album]::RegisterWork($_) }
$Albums | ForEach-Object { [PublishedWork]::RegisterWork($_) }
VERBOSE: Defined a published work of type [Album]
VERBOSE: Defined a published work of type [Album]
VERBOSE: Defined a published work of type [Album]

Genres                               Name                      Artist       Category
------                               ----                      ------       --------
{Progressive rock, Psychedelic rock} The Dark Side of the Moon Pink Floyd   Albums
{Progressive rock, Art rock}         The Wall                  Pink Floyd   Albums
{Hip hop}                            36 Chambers               Wu-Tang Clan Albums

VERBOSE: Adding work 'The Dark Side of the Moon' to works list
VERBOSE: Adding artist 'Pink Floyd' to artists list
VERBOSE: Adding work 'The Wall' to works list
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Adding work '36 Chambers' to works list
VERBOSE: Adding artist 'Wu-Tang Clan' to artists list

VERBOSE: Work 'The Dark Side of the Moon' already registered.
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Work 'The Wall' already registered.
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Work '36 Chambers' already registered.
VERBOSE: Artist 'Wu-Tang Clan' already registered.

请注意,即使 Album 类未定义 Category 或任何构造函数的值,该属性也由基类的默认构造函数定义。

在详细消息传送中,对方法的第二次调用 RegisterWork() 报告作品和艺术家已经注册。 尽管第一次调用RegisterWork()是派生的 Album 类,但它还是使用基 PublishedWork 类继承的静态方法。 该方法更新了基类上的静态 ListArtist 属性,该基类未重写该基类。

下一个代码块清除注册表并调用 Register() Album 对象上的实例方法。

[PublishedWork]::ClearRegistry()
$Albums.Register()
VERBOSE: Clearing PublishedWork registry

VERBOSE: Adding work 'The Dark Side of the Moon' to works list
VERBOSE: Adding artist 'Pink Floyd' to artists list
VERBOSE: Adding work 'The Wall' to works list
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Adding work '36 Chambers' to works list
VERBOSE: Adding artist 'Wu-Tang Clan' to artists list

Album 对象上的实例方法与对派生类或基类调用静态方法的效果相同。

以下代码块比较基类和派生类的静态属性,显示它们相同。

[pscustomobject]@{
    '[PublishedWork]::List'    = [PublishedWork]::List -join ",`n"
    '[Album]::List'            = [Album]::List -join ",`n"
    '[PublishedWork]::Artists' = [PublishedWork]::Artists -join ",`n"
    '[Album]::Artists'         = [Album]::Artists -join ",`n"
    'IsSame::List'             = (
        [PublishedWork]::List.Count -eq [Album]::List.Count -and
        [PublishedWork]::List.ToString() -eq [Album]::List.ToString()
    )
    'IsSame::Artists'          = (
        [PublishedWork]::Artists.Count -eq [Album]::Artists.Count -and
        [PublishedWork]::Artists.ToString() -eq [Album]::Artists.ToString()
    )
} | Format-List
[PublishedWork]::List    : The Dark Side of the Moon by Pink Floyd,
                           The Wall by Pink Floyd,
                           36 Chambers by Wu-Tang Clan
[Album]::List            : The Dark Side of the Moon by Pink Floyd,
                           The Wall by Pink Floyd,
                           36 Chambers by Wu-Tang Clan
[PublishedWork]::Artists : Pink Floyd,
                           Wu-Tang Clan
[Album]::Artists         : Pink Floyd,
                           Wu-Tang Clan
IsSame::List             : True
IsSame::Artists          : True

使用重写定义派生类

下一个代码块定义从基 PublishedWork 类继承的 Illustration 类。 新类通过使用默认值Unknown定义 Medium 实例属性来扩展基类。

与派生 的 Album 类不同, 插图 将重写以下属性和方法:

  • 它重写静态 Artists 属性。 定义相同,但 Illustration 类直接声明它。
  • 它重写 Category 实例属性,将默认值设置为 Illustrations.
  • 它重写 ToString() 实例方法,因此插图的字符串表示形式包括它创建的介质。

该类还定义了静态 RegisterIllustration() 方法以先调用基类 RegisterWork() 方法,然后将艺术家添加到派生类上重写 的 Artists 静态属性。

最后,类将重写所有三个构造函数:

  1. 默认构造函数为空,但指示它已创建插图的详细消息除外。
  2. 下一个构造函数为创建该插图的名称和艺术家采用两个字符串值。 构造函数不实现设置 NameArtist 属性的逻辑,而是从基类调用相应的构造函数。
  3. 最后一个构造函数采用三个字符串值作为插图的名称、艺术家和介质。 这两个构造函数都编写了一条详细消息,指示它们创建了一个插图。
class Illustration : PublishedWork {
    static [string[]] $Artists = @()

    static [void] RegisterIllustration([Illustration]$Work) {
        $wArtist = $Work.Artist

        [PublishedWork]::RegisterWork($Work)

        if ($wArtist -notin [Illustration]::Artists) {
            Write-Verbose "Adding illustrator '$wArtist' to artists list"
            [Illustration]::Artists += $wArtist
        } else {
            Write-Verbose "Illustrator '$wArtist' already registered."
        }
    }

    [string] $Category = 'Illustrations'
    [string] $Medium   = 'Unknown'

    [string] ToString() {
        return "$($this.Name) by $($this.Artist) ($($this.Medium))"
    }

    Illustration() {
        Write-Verbose 'Defined an illustration'
    }

    Illustration([string]$Name, [string]$Artist) : base($Name, $Artist) {
        Write-Verbose "Defined '$Name' by $Artist ($($this.Medium)) as an illustration"
    }

    Illustration([string]$Name, [string]$Artist, [string]$Medium) {
        $this.Name = $Name
        $this.Artist = $Artist
        $this.Medium = $Medium

        Write-Verbose "Defined '$Name' by $Artist ($Medium) as an illustration"
    }
}

以下代码块显示派生 的 Illustration 类的行为。 它创建类的三个实例,在表中显示它们,然后使用继承的静态 RegisterWork() 方法注册它们。 然后,它直接在基类上调用相同的静态方法。 最后,它会编写消息,其中显示了基类和派生类的已注册艺术家列表。

$Illustrations = @(
    [Illustration]@{
        Name   = 'The Funny Thing'
        Artist = 'Wanda Gág'
        Medium = 'Lithography'
    }
    [Illustration]::new('Millions of Cats', 'Wanda Gág')
    [Illustration]::new(
      'The Lion and the Mouse',
      'Jerry Pinkney',
      'Watercolor'
    )
)

$Illustrations | Format-Table
$Illustrations | ForEach-Object { [Illustration]::RegisterIllustration($_) }
$Illustrations | ForEach-Object { [PublishedWork]::RegisterWork($_) }
"Published work artists: $([PublishedWork]::Artists -join ', ')"
"Illustration artists: $([Illustration]::Artists -join ', ')"
VERBOSE: Defined a published work of type [Illustration]
VERBOSE: Defined an illustration
VERBOSE: Defined 'Millions of Cats' by Wanda Gág as a published work of type [Illustration]
VERBOSE: Defined 'Millions of Cats' by Wanda Gág (Unknown) as an illustration
VERBOSE: Defined a published work of type [Illustration]
VERBOSE: Defined 'The Lion and the Mouse' by Jerry Pinkney (Watercolor) as an illustration

Category      Medium      Name                   Artist
--------      ------      ----                   ------
Illustrations Lithography The Funny Thing        Wanda Gág
Illustrations Unknown     Millions of Cats       Wanda Gág
Illustrations Watercolor  The Lion and the Mouse Jerry Pinkney

VERBOSE: Adding work 'The Funny Thing' to works list
VERBOSE: Adding artist 'Wanda Gág' to artists list
VERBOSE: Adding illustrator 'Wanda Gág' to artists list
VERBOSE: Adding work 'Millions of Cats' to works list
VERBOSE: Artist 'Wanda Gág' already registered.
VERBOSE: Illustrator 'Wanda Gág' already registered.
VERBOSE: Adding work 'The Lion and the Mouse' to works list
VERBOSE: Adding artist 'Jerry Pinkney' to artists list
VERBOSE: Adding illustrator 'Jerry Pinkney' to artists list

VERBOSE: Work 'The Funny Thing' already registered.
VERBOSE: Artist 'Wanda Gág' already registered.
VERBOSE: Work 'Millions of Cats' already registered.
VERBOSE: Artist 'Wanda Gág' already registered.
VERBOSE: Work 'The Lion and the Mouse' already registered.
VERBOSE: Artist 'Jerry Pinkney' already registered.

Published work artists: Pink Floyd, Wu-Tang Clan, Wanda Gág, Jerry Pinkney

Illustration artists: Wanda Gág, Jerry Pinkney

创建实例时的详细消息传送显示:

  • 创建第一个实例时,在派生类默认构造函数之前调用基类默认构造函数。
  • 创建第二个实例时,在派生类构造函数之前为基类调用显式继承的构造函数。
  • 创建第三个实例时,在派生类构造函数之前调用基类默认构造函数。

该方法的详细消息 RegisterWork() 表明作品和艺术家已经注册。 这是因为 RegisterIllustration() 该方法在内部调用 RegisterWork() 了该方法。

但是,在比较基类和派生类的静态 Artist 属性的值时,这些值是不同的。 派生类的艺术家属性只包括插画家,而不是专辑艺术家。 重新定义 派生类中的 Artist 属性可防止类返回基类上的静态属性。

最终代码块对 ToString() 基类上静态 List 属性的条目调用该方法。

[PublishedWork]::List | ForEach-Object -Process { $_.ToString() }
The Dark Side of the Moon by Pink Floyd
The Wall by Pink Floyd
36 Chambers by Wu-Tang Clan
The Funny Thing by Wanda Gág (Lithography)
Millions of Cats by Wanda Gág (Unknown)
The Lion and the Mouse by Jerry Pinkney (Watercolor)

专辑实例仅返回其字符串中的名称和艺术家。 图 实例还包含在括号中,因为该类会覆盖该方法 ToString()

示例 2 - 实现接口

以下示例演示类如何实现一个或多个接口。 该示例扩展 Temperature 类的定义以支持更多操作和行为。

初始类定义

在实现任何接口之前, Temperature 类使用两个属性( DegreesScale)定义。 它定义构造函数和三个实例方法,用于将实例作为特定刻度返回。

该类使用 TemperatureScale 枚举定义可用的缩放

class Temperature {
    [float]            $Degrees
    [TemperatureScale] $Scale

    Temperature() {}
    Temperature([float] $Degrees)          { $this.Degrees = $Degrees }
    Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
    Temperature([float] $Degrees, [TemperatureScale] $Scale) {
        $this.Degrees = $Degrees
        $this.Scale   = $Scale
    }

    [float] ToKelvin() {
        switch ($this.Scale) {
            Celsius    { return $this.Degrees + 273.15 }
            Fahrenheit { return ($this.Degrees + 459.67) * 5/9 }
        }
        return $this.Degrees
    }
    [float] ToCelsius() {
        switch ($this.Scale) {
            Fahrenheit { return ($this.Degrees - 32) * 5/9 }
            Kelvin     { return $this.Degrees - 273.15 }
        }
        return $this.Degrees
    }
    [float] ToFahrenheit() {
        switch ($this.Scale) {
            Celsius    { return $this.Degrees * 9/5 + 32 }
            Kelvin     { return $this.Degrees * 9/5 - 459.67 }
        }
        return $this.Degrees
    }
}

enum TemperatureScale {
    Celsius    = 0
    Fahrenheit = 1
    Kelvin     = 2
}

但是,在此基本实现中,有一些限制,如以下示例输出中所示:

$Celsius    = [Temperature]::new()
$Fahrenheit = [Temperature]::new([TemperatureScale]::Fahrenheit)
$Kelvin     = [Temperature]::new(0, 'Kelvin')

$Celsius, $Fahrenheit, $Kelvin

"The temperatures are: $Celsius, $Fahrenheit, $Kelvin"

[Temperature]::new() -eq $Celsius

$Celsius -gt $Kelvin
Degrees      Scale
-------      -----
   0.00    Celsius
   0.00 Fahrenheit
   0.00     Kelvin

The temperatures are: Temperature, Temperature, Temperature

False

InvalidOperation:
Line |
  11 |  $Celsius -gt $Kelvin
     |  ~~~~~~~~~~~~~~~~~~~~
     | Cannot compare "Temperature" because it is not IComparable.

输出显示 Temperature实例:

  • 不要正确显示为字符串。
  • 无法正确检查是否等效。
  • 无法比较。

可以通过实现类的接口来解决这三个问题。

实现 IFormattable

要为 Temperature 类实现的第一个接口是 System.IFormattable。 此接口允许将类的实例的格式设置为不同的字符串。 若要实现接口,类需要从 System.IFormattable 继承并定义 ToString() 实例方法。

实例 ToString() 方法需要具有以下签名:

[string] ToString(
    [string]$Format,
    [System.IFormatProvider]$FormatProvider
) {
    # Implementation
}

接口所需的签名列在 参考文档中

对于 Temperature,该类应支持三种格式: C 以摄氏度返回实例、 F 以华氏为单位返回实例,并在 K Kelvin 中返回它。 对于任何其他格式,该方法应引发 System.FormatException

[string] ToString(
    [string]$Format,
    [System.IFormatProvider]$FormatProvider
) {
    # If format isn't specified, use the defined scale.
    if ([string]::IsNullOrEmpty($Format)) {
        $Format = switch ($this.Scale) {
            Celsius    { 'C' }
            Fahrenheit { 'F' }
            Kelvin     { 'K' }
        }
    }
    # If format provider isn't specified, use the current culture.
    if ($null -eq $FormatProvider) {
        $FormatProvider = [CultureInfo]::CurrentCulture
    }
    # Format the temperature.
    switch ($Format) {
        'C' {
            return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
        }
        'F' {
            return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
        }
        'K' {
            return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
        }
    }
    # If we get here, the format is invalid.
    throw [System.FormatException]::new(
        "Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
    )
}

在此实现中,该方法默认为格式化数字度值本身时的格式和当前区域性的实例刻度。 它使用 To<Scale>() 实例方法将度转换为小数位数,并将适当的度符号追加到字符串中。

实现所需的签名后,该类还可以定义重载,以便更轻松地返回格式化的实例。

[string] ToString([string]$Format) {
    return $this.ToString($Format, $null)
}

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

以下代码显示了温度的更新定义

class Temperature : System.IFormattable {
    [float]            $Degrees
    [TemperatureScale] $Scale

    Temperature() {}
    Temperature([float] $Degrees)          { $this.Degrees = $Degrees }
    Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
    Temperature([float] $Degrees, [TemperatureScale] $Scale) {
        $this.Degrees = $Degrees
        $this.Scale = $Scale
    }

    [float] ToKelvin() {
        switch ($this.Scale) {
            Celsius { return $this.Degrees + 273.15 }
            Fahrenheit { return ($this.Degrees + 459.67) * 5 / 9 }
        }
        return $this.Degrees
    }
    [float] ToCelsius() {
        switch ($this.Scale) {
            Fahrenheit { return ($this.Degrees - 32) * 5 / 9 }
            Kelvin { return $this.Degrees - 273.15 }
        }
        return $this.Degrees
    }
    [float] ToFahrenheit() {
        switch ($this.Scale) {
            Celsius { return $this.Degrees * 9 / 5 + 32 }
            Kelvin { return $this.Degrees * 9 / 5 - 459.67 }
        }
        return $this.Degrees
    }

    [string] ToString(
        [string]$Format,
        [System.IFormatProvider]$FormatProvider
    ) {
        # If format isn't specified, use the defined scale.
        if ([string]::IsNullOrEmpty($Format)) {
            $Format = switch ($this.Scale) {
                Celsius    { 'C' }
                Fahrenheit { 'F' }
                Kelvin     { 'K' }
            }
        }
        # If format provider isn't specified, use the current culture.
        if ($null -eq $FormatProvider) {
            $FormatProvider = [CultureInfo]::CurrentCulture
        }
        # Format the temperature.
        switch ($Format) {
            'C' {
                return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
            }
            'F' {
                return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
            }
            'K' {
                return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
            }
        }
        # If we get here, the format is invalid.
        throw [System.FormatException]::new(
            "Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
        )
    }

    [string] ToString([string]$Format) {
        return $this.ToString($Format, $null)
    }

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

enum TemperatureScale {
    Celsius    = 0
    Fahrenheit = 1
    Kelvin     = 2
}

方法重载的输出显示在以下块中。

$Temp = [Temperature]::new()
"The temperature is $Temp"
$Temp.ToString()
$Temp.ToString('K')
$Temp.ToString('F', $null)
The temperature is 0.00°C

0.00°C

273.15°K

32.00°F

实现 IEquatable

现在,温度类可以格式化为可读性,用户需要能够检查该类的两个实例是否相等。 若要支持此测试,类需要实现 System.IEquatable 接口。

若要实现接口,类需要从 System.IEquatable 继承并定义 Equals() 实例方法。 该方法 Equals() 需要具有以下签名:

[bool] Equals([object]$Other) {
    # Implementation
}

接口所需的签名列在 参考文档中

对于 Temperature,类应仅支持比较类的两个实例。 对于任何其他值或类型,包括 $null,它应返回 $false。 比较两个温度时,该方法应将这两个值转换为 Kelvin,因为即使温度与不同的刻度也可以相等。

[bool] Equals([object]$Other) {
    # If the other object is null, we can't compare it.
    if ($null -eq $Other) {
        return $false
    }

    # If the other object isn't a temperature, we can't compare it.
    $OtherTemperature = $Other -as [Temperature]
    if ($null -eq $OtherTemperature) {
        return $false
    }

    # Compare the temperatures as Kelvin.
    return $this.ToKelvin() -eq $OtherTemperature.ToKelvin()
}

实现接口方法后,温度的更新定义为:

class Temperature : System.IFormattable, System.IEquatable[object] {
    [float]            $Degrees
    [TemperatureScale] $Scale

    Temperature() {}
    Temperature([float] $Degrees)          { $this.Degrees = $Degrees }
    Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
    Temperature([float] $Degrees, [TemperatureScale] $Scale) {
        $this.Degrees = $Degrees
        $this.Scale = $Scale
    }

    [float] ToKelvin() {
        switch ($this.Scale) {
            Celsius { return $this.Degrees + 273.15 }
            Fahrenheit { return ($this.Degrees + 459.67) * 5 / 9 }
        }
        return $this.Degrees
    }
    [float] ToCelsius() {
        switch ($this.Scale) {
            Fahrenheit { return ($this.Degrees - 32) * 5 / 9 }
            Kelvin { return $this.Degrees - 273.15 }
        }
        return $this.Degrees
    }
    [float] ToFahrenheit() {
        switch ($this.Scale) {
            Celsius { return $this.Degrees * 9 / 5 + 32 }
            Kelvin { return $this.Degrees * 9 / 5 - 459.67 }
        }
        return $this.Degrees
    }

    [string] ToString(
        [string]$Format,
        [System.IFormatProvider]$FormatProvider
    ) {
        # If format isn't specified, use the defined scale.
        if ([string]::IsNullOrEmpty($Format)) {
            $Format = switch ($this.Scale) {
                Celsius    { 'C' }
                Fahrenheit { 'F' }
                Kelvin     { 'K' }
            }
        }
        # If format provider isn't specified, use the current culture.
        if ($null -eq $FormatProvider) {
            $FormatProvider = [CultureInfo]::CurrentCulture
        }
        # Format the temperature.
        switch ($Format) {
            'C' {
                return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
            }
            'F' {
                return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
            }
            'K' {
                return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
            }
        }
        # If we get here, the format is invalid.
        throw [System.FormatException]::new(
            "Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
        )
    }

    [string] ToString([string]$Format) {
        return $this.ToString($Format, $null)
    }

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

    [bool] Equals([object]$Other) {
        # If the other object is null, we can't compare it.
        if ($null -eq $Other) {
            return $false
        }

        # If the other object isn't a temperature, we can't compare it.
        $OtherTemperature = $Other -as [Temperature]
        if ($null -eq $OtherTemperature) {
            return $false
        }

        # Compare the temperatures as Kelvin.
        return $this.ToKelvin() -eq $OtherTemperature.ToKelvin()
    }
}

enum TemperatureScale {
    Celsius    = 0
    Fahrenheit = 1
    Kelvin     = 2
}

以下块显示了更新的类的行为方式:

$Celsius    = [Temperature]::new()
$Fahrenheit = [Temperature]::new(32, 'Fahrenheit')
$Kelvin     = [Temperature]::new([TemperatureScale]::Kelvin)

@"
Temperatures are: $Celsius, $Fahrenheit, $Kelvin
`$Celsius.Equals(`$Fahrenheit) = $($Celsius.Equals($Fahrenheit))
`$Celsius -eq `$Fahrenheit     = $($Celsius -eq $Fahrenheit)
`$Celsius -ne `$Kelvin         = $($Celsius -ne $Kelvin)
"@
Temperatures are: 0.00°C, 32.00°F, 0.00°K

$Celsius.Equals($Fahrenheit) = True
$Celsius -eq $Fahrenheit     = True
$Celsius -ne $Kelvin         = True

实现 IComparable

要为 Temperature 类实现的最后一个接口是 System.IComparable。 当类实现此接口时,用户可以使用 -lt-le-gt> 和-ge运算符来比较类的实例。

若要实现接口,类需要从 System.IComparable 继承并定义 Equals() 实例方法。 该方法 Equals() 需要具有以下签名:

[int] CompareTo([Object]$Other) {
    # Implementation
}

接口所需的签名列在 参考文档中

对于 Temperature,类应仅支持比较类的两个实例。 由于 Degrees 属性的基础类型(即使转换为其他刻度)是浮点数,因此该方法可以依赖基础类型进行实际比较。

[int] CompareTo([object]$Other) {
    # If the other object's null, consider this instance "greater than" it
    if ($null -eq $Other) {
        return 1
    }
    # If the other object isn't a temperature, we can't compare it.
    $OtherTemperature = $Other -as [Temperature]
    if ($null -eq $OtherTemperature) {
        throw [System.ArgumentException]::new(
            "Object must be of type 'Temperature'."
        )
    }
    # Compare the temperatures as Kelvin.
    return $this.ToKelvin().CompareTo($OtherTemperature.ToKelvin())
}

Temperature 类的最终定义是:

class Temperature : System.IFormattable,
                    System.IComparable,
                    System.IEquatable[object] {
    # Instance properties
    [float]            $Degrees
    [TemperatureScale] $Scale

    # Constructors
    Temperature() {}
    Temperature([float] $Degrees)          { $this.Degrees = $Degrees }
    Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
    Temperature([float] $Degrees, [TemperatureScale] $Scale) {
        $this.Degrees = $Degrees
        $this.Scale = $Scale
    }

    [float] ToKelvin() {
        switch ($this.Scale) {
            Celsius { return $this.Degrees + 273.15 }
            Fahrenheit { return ($this.Degrees + 459.67) * 5 / 9 }
        }
        return $this.Degrees
    }
    [float] ToCelsius() {
        switch ($this.Scale) {
            Fahrenheit { return ($this.Degrees - 32) * 5 / 9 }
            Kelvin { return $this.Degrees - 273.15 }
        }
        return $this.Degrees
    }
    [float] ToFahrenheit() {
        switch ($this.Scale) {
            Celsius { return $this.Degrees * 9 / 5 + 32 }
            Kelvin { return $this.Degrees * 9 / 5 - 459.67 }
        }
        return $this.Degrees
    }

    [string] ToString(
        [string]$Format,
        [System.IFormatProvider]$FormatProvider
    ) {
        # If format isn't specified, use the defined scale.
        if ([string]::IsNullOrEmpty($Format)) {
            $Format = switch ($this.Scale) {
                Celsius    { 'C' }
                Fahrenheit { 'F' }
                Kelvin     { 'K' }
            }
        }
        # If format provider isn't specified, use the current culture.
        if ($null -eq $FormatProvider) {
            $FormatProvider = [CultureInfo]::CurrentCulture
        }
        # Format the temperature.
        switch ($Format) {
            'C' {
                return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
            }
            'F' {
                return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
            }
            'K' {
                return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
            }
        }
        # If we get here, the format is invalid.
        throw [System.FormatException]::new(
            "Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
        )
    }

    [string] ToString([string]$Format) {
        return $this.ToString($Format, $null)
    }

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

    [bool] Equals([object]$Other) {
        # If the other object is null, we can't compare it.
        if ($null -eq $Other) {
            return $false
        }
        # If the other object isn't a temperature, we can't compare it.
        $OtherTemperature = $Other -as [Temperature]
        if ($null -eq $OtherTemperature) {
            return $false
        }
        # Compare the temperatures as Kelvin.
        return $this.ToKelvin() -eq $OtherTemperature.ToKelvin()
    }
    [int] CompareTo([object]$Other) {
        # If the other object's null, consider this instance "greater than" it
        if ($null -eq $Other) {
            return 1
        }
        # If the other object isn't a temperature, we can't compare it.
        $OtherTemperature = $Other -as [Temperature]
        if ($null -eq $OtherTemperature) {
            throw [System.ArgumentException]::new(
                "Object must be of type 'Temperature'."
            )
        }
        # Compare the temperatures as Kelvin.
        return $this.ToKelvin().CompareTo($OtherTemperature.ToKelvin())
    }
}

enum TemperatureScale {
    Celsius    = 0
    Fahrenheit = 1
    Kelvin     = 2
}

使用完整定义,用户可以像任何内置类型一样在 PowerShell 中设置类实例的格式和比较。

$Celsius    = [Temperature]::new()
$Fahrenheit = [Temperature]::new(32, 'Fahrenheit')
$Kelvin     = [Temperature]::new([TemperatureScale]::Kelvin)

@"
Temperatures are: $Celsius, $Fahrenheit, $Kelvin
`$Celsius.Equals(`$Fahrenheit)    = $($Celsius.Equals($Fahrenheit))
`$Celsius.Equals(`$Kelvin)        = $($Celsius.Equals($Kelvin))
`$Celsius.CompareTo(`$Fahrenheit) = $($Celsius.CompareTo($Fahrenheit))
`$Celsius.CompareTo(`$Kelvin)     = $($Celsius.CompareTo($Kelvin))
`$Celsius -lt `$Fahrenheit        = $($Celsius -lt $Fahrenheit)
`$Celsius -le `$Fahrenheit        = $($Celsius -le $Fahrenheit)
`$Celsius -eq `$Fahrenheit        = $($Celsius -eq $Fahrenheit)
`$Celsius -gt `$Kelvin            = $($Celsius -gt $Kelvin)
"@
Temperatures are: 0.00°C, 32.00°F, 0.00°K
$Celsius.Equals($Fahrenheit)    = True
$Celsius.Equals($Kelvin)        = False
$Celsius.CompareTo($Fahrenheit) = 0
$Celsius.CompareTo($Kelvin)     = 1
$Celsius -lt $Fahrenheit        = False
$Celsius -le $Fahrenheit        = True
$Celsius -eq $Fahrenheit        = True
$Celsius -gt $Kelvin            = True

示例 3 - 从泛型基类继承

此示例演示如何从 System.Collections.Generic.List泛型类派生。

使用内置类作为类型参数

运行以下代码块。 它显示新类如何从泛型类型继承,前提是类型参数已在分析时定义。

class ExampleStringList : System.Collections.Generic.List[string] {}

$List = [ExampleStringList]::New()
$List.AddRange([string[]]@('a','b','c'))
$List.GetType() | Format-List -Property Name, BaseType
$List
Name     : ExampleStringList
BaseType : System.Collections.Generic.List`1[System.String]

a
b
c

使用自定义类作为类型参数

下一个代码块首先使用单个实例属性和ToString()方法定义一个新类 ExampleItem。 然后,它定义从 System.Collections.Generic.List 基类继承的 ExampleItemList 类,并将 ExampleItem 作为类型参数。

复制整个代码块并将其作为单个语句运行。

class ExampleItem {
    [string] $Name
    [string] ToString() { return $this.Name }
}
class ExampleItemList : System.Collections.Generic.List[ExampleItem] {}
ParentContainsErrorRecordException: An error occurred while creating the pipeline.

运行整个代码块会引发错误,因为 PowerShell 尚未将 ExampleItem 类加载到运行时。 目前无法将类名用作 System.Collections.Generic.List 基类的类型参数

按照定义代码块的顺序运行以下代码块。

class ExampleItem {
    [string] $Name
    [string] ToString() { return $this.Name }
}
class ExampleItemList : System.Collections.Generic.List[ExampleItem] {}

这一次,PowerShell 不会引发任何错误。 现在定义了这两个类。 运行以下代码块以查看新类的行为。

$List = [ExampleItemList]::New()
$List.AddRange([ExampleItem[]]@(
    [ExampleItem]@{ Name = 'Foo' }
    [ExampleItem]@{ Name = 'Bar' }
    [ExampleItem]@{ Name = 'Baz' }
))
$List.GetType() | Format-List -Property Name, BaseType
$List
Name     : ExampleItemList
BaseType : System.Collections.Generic.List`1[ExampleItem]

Name
----
Foo
Bar
Baz

在模块中使用自定义类型参数派生泛型

以下代码块演示如何定义从泛型基类继承的类,该类使用类型参数的自定义类型。

将以下代码块另存为 GenericExample.psd1

@{
    RootModule        = 'GenericExample.psm1'
    ModuleVersion     = '0.1.0'
    GUID              = '2779fa60-0b3b-4236-b592-9060c0661ac2'
}

将以下代码块另存为 GenericExample.InventoryItem.psm1

class InventoryItem {
    [string] $Name
    [int]    $Count

    InventoryItem() {}
    InventoryItem([string]$Name) {
        $this.Name = $Name
    }
    InventoryItem([string]$Name, [int]$Count) {
        $this.Name  = $Name
        $this.Count = $Count
    }

    [string] ToString() {
        return "$($this.Name) ($($this.Count))"
    }
}

将以下代码块另存为 GenericExample.psm1

using namespace System.Collections.Generic
using module ./GenericExample.InventoryItem.psm1

class Inventory : List[InventoryItem] {}

# Define the types to export with type accelerators.
$ExportableTypes =@(
    [InventoryItem]
    [Inventory]
)
# Get the internal TypeAccelerators class to use its static methods.
$TypeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)
# Ensure none of the types would clobber an existing type accelerator.
# If a type accelerator with the same name exists, throw an exception.
$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get
foreach ($Type in $ExportableTypes) {
    if ($Type.FullName -in $ExistingTypeAccelerators.Keys) {
        $Message = @(
            "Unable to register type accelerator '$($Type.FullName)'"
            'Accelerator already exists.'
        ) -join ' - '

        throw [System.Management.Automation.ErrorRecord]::new(
            [System.InvalidOperationException]::new($Message),
            'TypeAcceleratorAlreadyExists',
            [System.Management.Automation.ErrorCategory]::InvalidOperation,
            $Type.FullName
        )
    }
}
# Add type accelerators for every exportable type.
foreach ($Type in $ExportableTypes) {
    $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach($Type in $ExportableTypes) {
        $TypeAcceleratorsClass::Remove($Type.FullName)
    }
}.GetNewClosure()

提示

根模块将自定义类型添加到 PowerShell 的类型加速器。 此模式使模块用户能够立即访问自定义类型的 IntelliSense 和自动完成,而无需先使用 using module 语句。

有关此模式的详细信息,请参阅about_Classes“使用类型加速器导出”部分。

导入模块并验证输出。

Import-Module ./GenericExample.psd1

$Inventory = [Inventory]::new()
$Inventory.GetType() | Format-List -Property Name, BaseType

$Inventory.Add([InventoryItem]::new('Bucket', 2))
$Inventory.Add([InventoryItem]::new('Mop'))
$Inventory.Add([InventoryItem]@{ Name = 'Broom' ; Count = 4 })
$Inventory
Name     : Inventory
BaseType : System.Collections.Generic.List`1[InventoryItem]

Name   Count
----   -----
Bucket     2
Mop        0
Broom      4

模块加载时不会出错,因为 InventoryItem 类是在不同于 Inventory 类的模块文件中定义的。 这两个类都可用于模块用户。

继承基类

当类从基类继承时,它将继承基类的属性和方法。 它不会直接继承基类构造函数,但可以调用它们。

在 .NET 而不是 PowerShell 中定义基类时,请注意:

  • PowerShell 类无法继承自密封类。
  • 从泛型基类继承时,泛型类的类型参数不能是派生类。 使用派生类作为类型参数引发分析错误。

若要查看继承和重写如何适用于派生类,请参阅 示例 1

派生类构造函数

派生类不直接继承基类的构造函数。 如果基类定义默认构造函数,并且派生类未定义任何构造函数,则派生类的新实例使用基类默认构造函数。 如果基类未定义默认构造函数,则派生类必须显式定义至少一个构造函数。

派生类构造函数可以使用关键字从基类 base 调用构造函数。 如果派生类未从基类显式调用构造函数,则会改为调用基类的默认构造函数。

若要调用非默认基构造函数,请在构造函数参数之后和正文块之前添加 : base(<parameters>)

class <derived-class> : <base-class> {
    <derived-class>(<derived-parameters>) : <base-class>(<base-parameters>) {
        # initialization code
    }
}

定义调用基类构造函数的构造函数时,参数可以是以下任一项:

  • 派生类构造函数上任何参数的变量。
  • 任何静态值。
  • 计算结果为参数类型值的任何表达式。

示例 1 中的“插图”类演示如何使用基类构造函数。

派生类方法

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

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

与构造函数不同,方法不能使用 : base(<parameters>) 语法为该方法调用基类重载。 派生类上重新定义的重载完全替换基类定义的重载。 若要调用实例的基类方法,请先将实例变量 ($this) 强制转换为基类,然后再调用该方法。

以下代码片段演示派生类如何调用基类方法。

class BaseClass {
    [bool] IsTrue() { return $true }
}
class DerivedClass : BaseClass {
    [bool] IsTrue()     { return $false }
    [bool] BaseIsTrue() { return ([BaseClass]$this).IsTrue() }
}

@"
[BaseClass]::new().IsTrue()        = $([BaseClass]::new().IsTrue())
[DerivedClass]::new().IsTrue()     = $([DerivedClass]::new().IsTrue())
[DerivedClass]::new().BaseIsTrue() = $([DerivedClass]::new().BaseIsTrue())
"@
[BaseClass]::new().IsTrue()        = True
[DerivedClass]::new().IsTrue()     = False
[DerivedClass]::new().BaseIsTrue() = True

有关显示派生类如何替代继承方法的扩展示例,请参阅示例 1 中的“插图”类。

派生类属性

当类派生自基类时,它将继承基类的属性。 基类上定义的任何属性(包括隐藏属性)都可用于派生类。

派生类可以通过在类定义中重新定义继承的属性来替代继承的属性。 派生类上的属性使用重新定义的类型和默认值(如果有)。 如果继承的属性定义了默认值并且重新定义的属性没有,则继承的属性没有默认值。

如果派生类不重写静态属性,则通过派生类访问静态属性将访问基类的静态属性。 通过派生类修改属性值会修改基类上的值。 任何其他不重写静态属性的派生类也使用基类上的属性的值。 更新不重写该属性的类中继承的静态属性的值可能对派生自同一基类的类产生意外的影响。

示例 1 显示了如何继承、扩展和重写基类属性的派生类。

从泛型派生

当类派生自泛型时,必须先定义类型参数,然后 PowerShell 才分析派生类。 如果泛型的类型参数是在同一文件或代码块中定义的 PowerShell 类或枚举,则 PowerShell 将引发错误。

若要从具有自定义类型的泛型基类派生类作为类型参数,请在其他文件或模块中定义类型参数的类或枚举,并使用 using module 语句加载类型定义。

有关演示如何从泛型基类继承的示例,请参阅 示例 3

用于继承的有用类

在创作 PowerShell 模块时,有几个类可用于继承。 本部分列出了几个基类,以及派生自它们的类可以用于哪些类。

  • System.Attribute - 派生类,用于定义可用于变量、参数、类和枚举定义的属性,等等。
  • System.Management.Automation.ArgumentTransformationAttribute - 派生类来处理将变量或参数的输入转换为特定数据类型。
  • System.Management.Automation.ValidateArgumentsAttribute - 派生类,以将自定义验证应用于变量、参数和类属性。
  • System.Collections.Generic.List - 派生类,以便更轻松地创建和管理特定数据类型的列表。
  • System.Exception - 派生类以定义自定义错误。

实现接口

实施接口的 PowerShell 类必须实施该接口的所有成员。 省略实现接口成员会导致脚本中的分析时错误。

注意

PowerShell 不支持在 PowerShell 脚本中声明新接口。 相反,必须在 .NET 代码中声明接口,并使用 cmdlet 或using assembly语句添加到会话Add-Type中。

类实现接口时,可以像实现该接口的任何其他类一样使用它。 某些命令和操作将其支持的类型限制为实现特定接口的类。

若要查看接口的示例实现,请参阅 示例 2

用于实现的有用接口

在创作 PowerShell 模块时,有几个接口类可用于继承。 本部分列出了几个基类,以及派生自它们的类可以用于哪些类。

  • System.IEquatable - 此接口使用户能够比较类的两个实例。 当类未实现此接口时,PowerShell 会使用引用相等性检查两个实例之间的等效性。 换句话说,即使两个实例上的属性值相同,类的实例也仅等于自身。
  • System.IComparable - 此接口使用户能够将类的实例与比较-lt-ge-gt运算符和-le比较运算符进行比较。 当类不实现此接口时,这些运算符将引发错误。
  • System.IFormattable - 此接口允许用户将类的实例格式化为不同的字符串。 这对于具有多个标准字符串表示形式的类非常有用,例如预算项、书目和温度。
  • System.IConvertible - 此接口允许用户将类的实例转换为其他运行时类型。 这对于具有基础数值或可转换为一个数值的类非常有用。

限制

  • PowerShell 不支持在脚本代码中定义接口。

    解决方法:在 C# 中定义接口并引用定义接口的程序集。

  • PowerShell 类只能继承自一个基类。

    解决方法:类继承是可传递的。 派生类可以从另一个派生类继承,以获取基类的属性和方法。

  • 从泛型类或接口继承时,必须已定义泛型的类型参数。 类不能将自身定义为类或接口的类型参数。

    解决方法:若要从泛型基类或接口派生,请在其他 .psm1 文件中定义自定义类型,并使用 using module 语句加载类型。 从泛型继承时,自定义类型无法自行用作类型参数。

另请参阅