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

[玩转系统] 关于类继承

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

关于类继承


简短描述

描述如何定义扩展其他类型的类。

详细描述

PowerShell 类支持继承,它允许您定义重用(继承)、扩展或修改父类行为的子类。其成员被继承的类称为基类。继承基类成员的类称为派生类

PowerShell 仅支持单继承。一个类只能继承自一个类。但是,继承是可传递的,它允许您为一组类型定义继承层次结构。换句话说,类型D可以继承自类型C,类型C又继承自类型B,而类型B又继承自基类类型A 。由于继承是传递性的,A 类型的成员可用于 D 类型。

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

  • 静态构造函数,初始化类的静态数据。
  • 实例构造函数,调用它来创建类的新实例。每个类必须定义自己的构造函数。

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

类还可以从定义契约的接口继承。从接口继承的类必须实现该约定。当它出现时,该类就可以像实现该接口的任何其他类一样使用。如果类继承自接口但未实现该接口,PowerShell 会引发该类的解析错误。

某些 PowerShell 运算符依赖于实现特定接口的类。例如,-eq 运算符仅检查引用相等性,除非该类实现了 System.IEquatable 接口。 -le-lt-ge-gt 运算符仅适用于实现 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。接下来,它定义静态 RegisterWork() 方法,将作品添加到静态 List 属性,将艺术家添加到 Artists 属性,并写入一条消息对于列表中的每个新条目。

该类定义了三个描述已发布作品的实例属性。最后,它定义了 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。它不会覆盖任何属性或方法。它添加了一个新的实例属性Genres,该属性在基类中不存在。

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 属性,派生类未重写这些属性。

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

[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 类。新类通过定义默认值UnknownMedium 实例属性来扩展基类。

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

  • 它覆盖静态 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 属性的值时,这些值是不同的。派生类的 Artists 属性仅包括插画家,而不包括专辑艺术家。在派生类中重新定义 Artist 属性可防止该类返回基类上的静态属性。

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

[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)

Album 实例仅返回其字符串中的名称和艺术家。 Illustration 实例还在括号中包含了媒体,因为该类重写了 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 以开尔文形式返回。对于任何其他格式,该方法应抛出 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() 实例方法来转换度数,将其格式化为小数点后两位,并将适当的度数符号附加到字符串中。

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

[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

既然可以对 Temperature 类进行格式化以提高可读性,用户需要能够检查该类的两个实例是否相等。为了支持此测试,该类需要实现System.IEquatable接口。

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

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

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

对于温度,该类应仅支持比较该类的两个实例。对于任何其他值或类型,包括 $null,它应该返回 $false。比较两个温度时,该方法应将两个值转换为开尔文,因为即使在不同的尺度下,温度也可能相等。

[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()
}

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

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
}

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

对于温度,该类应仅支持比较该类的两个实例。由于 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

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

下一个代码块首先定义一个新类 ExampleItem,它具有单个实例属性和 ToString() 方法。然后,它定义了从 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()

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

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

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

示例 1 中的说明类显示了派生类如何使用基类构造函数。

派生类方法

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

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

与构造函数不同,方法不能使用 : base() 语法来调用该方法的基类重载。派生类上重新定义的重载完全替换了基类定义的重载。要调用实例的基类方法,请在调用方法之前将实例变量 ($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 代码中声明,并使用 Add-Type cmdlet 或 using assembly 语句添加到会话中。

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

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

需要实现的有用接口

在创作 PowerShell 模块时,继承一些接口类很有用。本节列出了一些基类以及从它们派生的类的用途。

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

局限性

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

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

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

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

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

    解决方法:要从通用基类或接口派生,请在不同的 .psm1 文件中定义自定义类型,并使用 using module 语句加载该类型。从泛型继承时,没有解决方法可以让自定义类型将自身用作类型参数。

参见

  • 关于课程
  • about_类_构造函数
  • about_类_方法
  • about_Classes_Properties

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

取消回复欢迎 发表评论:

关灯