about_Classes_Inheritance

簡短描述

描述如何定義擴充其他類型的類別。

詳細描述

PowerShell 類別支援 繼承,可讓您定義可重複使用(繼承)、擴充或修改父類別行為的子類別。 其成員可供繼承的類別稱為「基底類別」。 繼承基底類別成員的類別則稱為「衍生類別」

PowerShell 僅支援單一繼承。 類別只能繼承自單一類別。 不過,繼承可以轉移,這可讓您定義一組型別的繼承階層。 換句話說,類型 D 可以繼承自類型 C,其繼承自 類型 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() 方法,將 Works 新增至靜態 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。 它不會覆寫任何屬性或方法。 它會新增基類上不存在的新實例屬性 內容類型

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)

專輯實例只會傳回其字串中的名稱和藝術家。 圖例實例也包含在括弧中,因為該類別會覆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)
}

下列程式代碼顯示 Temperature更新定義:

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
}

介面所需的簽章會列在 參考檔中

針對 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()
}

實作介面方法時,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
}

介面所需的簽章會列在 參考檔中

針對 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 中的 Illustration 類別示範衍生類別如何使用基類建構函式。

衍生類別方法

當類別衍生自基類時,它會繼承基類及其多載的方法。 在基類上定義的任何方法多載,包括隱藏的方法,都可以在衍生類別上使用。

衍生類別可以覆寫繼承的方法多載,方法是在類別定義中重新定義。 若要覆寫多載,參數類型必須與基類的 型別相同。 多載的輸出類型可能不同。

不同於建構函式,方法無法使用 語法來叫用 : 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 - 此介面可讓使用者比較 類別的實例與 -le-lt-ge-gt 比較運算符。 當類別未實作這個介面時,這些運算子就會引發錯誤。
  • System.IFormattable - 此介面可讓使用者將 類別的實例格式化為不同的字串。 這適用於具有多個標準字串表示法的類別,例如預算專案、書目和溫度。
  • System.IConvertible - 此介面可讓使用者將 類別的實例轉換成其他運行時間類型。 這對於具有基礎數值或可轉換成一個的類別很有用。

限制

  • PowerShell 不支援在腳本程式代碼中定義介面。

    因應措施:在 C# 中定義介面,並參考定義介面的元件。

  • PowerShell 類別只能繼承自一個基類。

    因應措施:類別繼承是可轉移的。 衍生類別可以繼承自另一個衍生類別,以取得基類的屬性和方法。

  • 從泛型類別或介面繼承時,必須已經定義泛型的類型參數。 類別無法將本身定義為類別或介面的類型參數。

    因應措施:若要衍生自泛型基類或介面,請在不同的 .psm1 檔案中定義自定義類型,並使用 using module 語句載入類型。 自定義類型在繼承自泛型時,不會有任何因應措施,以做為型別參數。

另請參閱