共用方式為


關於類別繼承

簡短描述

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

完整描述

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

PowerShell 僅支援單一繼承。 類別只能繼承自單一類別。 不過,繼承是可轉移的,可讓您定義一組類型的繼承階層。 換句話說,類型 D 可以繼承自類型 C,該類型繼承自類型 B,其繼承自 A基類類型。因為繼承是可轉移的,A 類型的成員可用於類型 D

衍生類別不會繼承基類的所有成員。 下列成員不會被繼承:

  • 靜態建構函式,其會初始化 類別的靜態數據。
  • 實例建構函式,您可以呼叫它來建立類別的新實例。 每個類別都必須定義自己的建構函式。

您可以建立衍生自現有類別的新類別來擴充類別。 衍生類別會繼承基類的屬性和方法。 您可以視需要新增或覆寫基類成員。

類別也可以繼承介面,這些介面定義了一個契約。 繼承自介面的類別必須實作該合約。 這樣做時,類別就像實作該介面的任何其他類別一樣可供使用。 如果類別繼承自 介面,但未實作 介面,PowerShell 會引發 類別的剖析錯誤。

某些 PowerShell 運算符相依於實作特定介面的類別。 例如,除非類別實作 -eq 介面,否則 運算符只會檢查參考是否相等。 運算符只能在實作 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 屬性,而衍生類別並未覆寫這些屬性。

下一個程式代碼區塊會清除登錄,並在 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

定義具有覆寫功能的派生類別

下一個程式代碼區塊會定義繼承自base PublishedWork 類別的 Illustration 類別。 新的類別會定義 Medium 實例屬性,其預設值為 Unknown,藉此擴充基類。

與衍生 Album 類別不同的是,Illustration 會覆寫下列屬性和方法:

  • 它會覆寫 static Artists 屬性。 定義相同,但 圖例 類別會直接宣告它。
  • 它會覆寫 類別 實例屬性,將預設值設定為 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"
    }
}

下列程式代碼區塊顯示衍生 圖例 類別的行為。 它會建立 類別的三個實例、在數據表中顯示它們,然後向繼承的靜態 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() 屬性的項目上呼叫 方法。

[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() 實例方法必須具有下列簽章:

[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() 方法必須具有下列簽章:

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

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

針對 Temperature,類別應該只支援比較 類別的兩個實例。 對於任何其他值或類型,包括 $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() 方法必須具有下列簽章:

[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 - 繼承自泛型基類

此範例顯示如何從泛型衍生,只要在剖解析時已定義類型參數。

使用內建類別作為類型參數

執行下列程式代碼區塊。 它會顯示新類別如何繼承自泛型型別,只要在語法剖析時已經定義型別參數。

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()。 然後,它會定義類別 ExampleItemList,這個類別繼承自 System.Collections.Generic.List 基類,並使用 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 程式代碼中宣告,並使用 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 語句載入類型。 自定義類型在從泛型繼承時無法使用自身作為類型參數。

另請參閱