about_Classes

簡短描述

描述如何使用類別來建立自己的自定義類型。

詳細描述

從 5.0 版開始,PowerShell 具有定義類別和其他使用者定義型別的正式語法。 新增類別可讓開發人員和IT專業人員接受PowerShell,以取得更廣泛的使用案例。

類別宣告是用來在運行時間建立物件實例的藍圖。 當您定義類別時,類別名稱是型別的名稱。 例如,如果您宣告名為 Device 的類別,並將變數$dev初始化為 Device 的新實例,$dev則為 Device 類型的物件或實例。 裝置的每個實例在其屬性中都可以有不同的值。

支援的案例

  • 在 PowerShell 中使用面向物件程式設計語意定義自定義類型,例如類別、屬性、方法、繼承等。
  • 使用 PowerShell 語言定義 DSC 資源及其相關聯的類型。
  • 定義自訂屬性以裝飾變數、參數和自定義類型定義。
  • 定義可由其類型名稱攔截的自定義例外狀況。

語法

定義語法

類別定義使用下列語法:

class <class-name> [: [<base-class>][,<interface-list>]] {
    [[<attribute>] [hidden] [static] <property-definition> ...]
    [<class-name>([<constructor-argument-list>])
      {<constructor-statement-list>} ...]
    [[<attribute>] [hidden] [static] <method-definition> ...]
}

具現化語法

若要具現化 類別的實例,請使用下列其中一種語法:

[$<variable-name> =] New-Object -TypeName <class-name> [
  [-ArgumentList] <constructor-argument-list>]
[$<variable-name> =] [<class-name>]::new([<constructor-argument-list>])
[$<variable-name> =] [<class-name>]@{[<class-property-hashtable>]}

注意

使用 [<class-name>]::new() 語法時,類別名稱周圍的括弧是必要的。 括號表示 PowerShell 的類型定義。

哈希表語法僅適用於具有預設建構函式且不需要任何參數的類別。 它會使用預設建構函式建立 類別的實例,然後將索引鍵/值組指派給實例屬性。 如果哈希表中的任何索引鍵不是有效的屬性名稱,PowerShell 就會引發錯誤。

範例

範例 1 - 最小定義

此範例示範建立可用類別所需的最小語法。

class Device {
    [string]$Brand
}

$dev = [Device]::new()
$dev.Brand = "Fabrikam, Inc."
$dev
Brand
-----
Fabrikam, Inc.

範例 2 - 具有實例成員的類別

此範例會使用數個 屬性、建構函式和方法來定義 Book 類別。 每個定義的成員都是 實例 成員,而不是靜態成員。 屬性和方法只能透過 類別的建立實例來存取。

class Book {
    # Class properties
    [string]   $Title
    [string]   $Author
    [string]   $Synopsis
    [string]   $Publisher
    [datetime] $PublishDate
    [int]      $PageCount
    [string[]] $Tags
    # Default constructor
    Book() { $this.Init(@{}) }
    # Convenience constructor from hashtable
    Book([hashtable]$Properties) { $this.Init($Properties) }
    # Common constructor for title and author
    Book([string]$Title, [string]$Author) {
        $this.Init(@{Title = $Title; Author = $Author })
    }
    # Shared initializer method
    [void] Init([hashtable]$Properties) {
        foreach ($Property in $Properties.Keys) {
            $this.$Property = $Properties.$Property
        }
    }
    # Method to calculate reading time as 2 minutes per page
    [timespan] GetReadingTime() {
        if ($this.PageCount -le 0) {
            throw 'Unable to determine reading time from page count.'
        }
        $Minutes = $this.PageCount * 2
        return [timespan]::new(0, $Minutes, 0)
    }
    # Method to calculate how long ago a book was published
    [timespan] GetPublishedAge() {
        if (
            $null -eq $this.PublishDate -or
            $this.PublishDate -eq [datetime]::MinValue
        ) { throw 'PublishDate not defined' }

        return (Get-Date) - $this.PublishDate
    }
    # Method to return a string representation of the book
    [string] ToString() {
        return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))"
    }
}

下列代碼段會建立 類別的實例,並示範其運作方式。 建立 Book 類別的實例之後,此範例會使用 GetReadingTime()GetPublishedAge() 方法來撰寫有關書籍的訊息。

$Book = [Book]::new(@{
    Title       = 'The Hobbit'
    Author      = 'J.R.R. Tolkien'
    Publisher   = 'George Allen & Unwin'
    PublishDate = '1937-09-21'
    PageCount   = 310
    Tags        = @('Fantasy', 'Adventure')
})

$Book
$Time = $Book.GetReadingTime()
$Time = @($Time.Hours, 'hours and', $Time.Minutes, 'minutes') -join ' '
$Age  = [Math]::Floor($Book.GetPublishedAge().TotalDays / 365.25)

"It takes $Time to read $Book,`nwhich was published $Age years ago."
Title       : The Hobbit
Author      : J.R.R. Tolkien
Synopsis    :
Publisher   : George Allen & Unwin
PublishDate : 9/21/1937 12:00:00 AM
PageCount   : 310
Tags        : {Fantasy, Adventure}

It takes 10 hours and 20 minutes to read The Hobbit by J.R.R. Tolkien (1937),
which was published 86 years ago.

範例 3 - 具有靜態成員的類別

此範例中的 BookList 類別是以範例 2 中的 Book 類別為基礎。雖然 BookList 類別無法標示為靜態本身,但實作只會定義 Books 靜態屬性和一組用來管理該屬性的靜態方法。

class BookList {
    # Static property to hold the list of books
    static [System.Collections.Generic.List[Book]] $Books
    # Static method to initialize the list of books. Called in the other
    # static methods to avoid needing to explicit initialize the value.
    static [void] Initialize()             { [BookList]::Initialize($false) }
    static [bool] Initialize([bool]$force) {
        if ([BookList]::Books.Count -gt 0 -and -not $force) {
            return $false
        }

        [BookList]::Books = [System.Collections.Generic.List[Book]]::new()

        return $true
    }
    # Ensure a book is valid for the list.
    static [void] Validate([book]$Book) {
        $Prefix = @(
            'Book validation failed: Book must be defined with the Title,'
            'Author, and PublishDate properties, but'
        ) -join ' '
        if ($null -eq $Book) { throw "$Prefix was null" }
        if ([string]::IsNullOrEmpty($Book.Title)) {
            throw "$Prefix Title wasn't defined"
        }
        if ([string]::IsNullOrEmpty($Book.Author)) {
            throw "$Prefix Author wasn't defined"
        }
        if ([datetime]::MinValue -eq $Book.PublishDate) {
            throw "$Prefix PublishDate wasn't defined"
        }
    }
    # Static methods to manage the list of books.
    # Add a book if it's not already in the list.
    static [void] Add([Book]$Book) {
        [BookList]::Initialize()
        [BookList]::Validate($Book)
        if ([BookList]::Books.Contains($Book)) {
            throw "Book '$Book' already in list"
        }

        $FindPredicate = {
            param([Book]$b)

            $b.Title -eq $Book.Title -and
            $b.Author -eq $Book.Author -and
            $b.PublishDate -eq $Book.PublishDate
        }.GetNewClosure()
        if ([BookList]::Books.Find($FindPredicate)) {
            throw "Book '$Book' already in list"
        }

        [BookList]::Books.Add($Book)
    }
    # Clear the list of books.
    static [void] Clear() {
      [BookList]::Initialize()
      [BookList]::Books.Clear()
    }
    # Find a specific book using a filtering scriptblock.
    static [Book] Find([scriptblock]$Predicate) {
        [BookList]::Initialize()
        return [BookList]::Books.Find($Predicate)
    }
    # Find every book matching the filtering scriptblock.
    static [Book[]] FindAll([scriptblock]$Predicate) {
        [BookList]::Initialize()
        return [BookList]::Books.FindAll($Predicate)
    }
    # Remove a specific book.
    static [void] Remove([Book]$Book) {
        [BookList]::Initialize()
        [BookList]::Books.Remove($Book)
    }
    # Remove a book by property value.
    static [void] RemoveBy([string]$Property, [string]$Value) {
        [BookList]::Initialize()
        $Index = [BookList]::Books.FindIndex({
            param($b)
            $b.$Property -eq $Value
        }.GetNewClosure())
        if ($Index -ge 0) {
            [BookList]::Books.RemoveAt($Index)
        }
    }
}

現在已定義 BookList,就可以將上一個範例中的書籍新增至清單。

$null -eq [BookList]::Books

[BookList]::Add($Book)

[BookList]::Books
True

Title       : The Hobbit
Author      : J.R.R. Tolkien
Synopsis    :
Publisher   : George Allen & Unwin
PublishDate : 9/21/1937 12:00:00 AM
PageCount   : 310
Tags        : {Fantasy, Adventure}

下列代碼段會呼叫 類別的靜態方法。

[BookList]::Add([Book]::new(@{
    Title       = 'The Fellowship of the Ring'
    Author      = 'J.R.R. Tolkien'
    Publisher   = 'George Allen & Unwin'
    PublishDate = '1954-07-29'
    PageCount   = 423
    Tags        = @('Fantasy', 'Adventure')
}))

[BookList]::Find({
    param ($b)

    $b.PublishDate -gt '1950-01-01'
}).Title

[BookList]::FindAll({
    param($b)

    $b.Author -match 'Tolkien'
}).Title

[BookList]::Remove($Book)
[BookList]::Books.Title

[BookList]::RemoveBy('Author', 'J.R.R. Tolkien')
"Titles: $([BookList]::Books.Title)"

[BookList]::Add($Book)
[BookList]::Add($Book)
The Fellowship of the Ring

The Hobbit
The Fellowship of the Ring

The Fellowship of the Ring

Titles:

Exception:
Line |
  84 |              throw "Book '$Book' already in list"
     |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Book 'The Hobbit by J.R.R. Tolkien (1937)' already in list

範例 4 - 具有和不含 Runspace 親和性的類別定義

ShowRunspaceId()[UnsafeClass] 方法會報告不同的線程標識符,但相同的 Runspace 識別碼。 最後,工作階段狀態已損毀,導致錯誤,例如 Global scope cannot be removed

# Class definition with Runspace affinity (default behavior)
class UnsafeClass {
    static [object] ShowRunspaceId($val) {
        return [PSCustomObject]@{
            ThreadId   = [Threading.Thread]::CurrentThread.ManagedThreadId
            RunspaceId = [runspace]::DefaultRunspace.Id
        }
    }
}

$unsafe = [UnsafeClass]::new()

while ($true) {
    1..10 | ForEach-Object -Parallel {
        Start-Sleep -ms 100
        ($using:unsafe)::ShowRunspaceId($_)
    }
}

注意

這個範例會在無限迴圈中執行。 輸入 Ctrl+C 以停止執行。

報告ShowRunspaceId()[SafeClass]不同線程和 Runspace 識別符的方法。

# Class definition with NoRunspaceAffinity attribute
[NoRunspaceAffinity()]
class SafeClass {
    static [object] ShowRunspaceId($val) {
        return [PSCustomObject]@{
            ThreadId   = [Threading.Thread]::CurrentThread.ManagedThreadId
            RunspaceId = [runspace]::DefaultRunspace.Id
        }
    }
}

$safe = [SafeClass]::new()

while ($true) {
    1..10 | ForEach-Object -Parallel {
        Start-Sleep -ms 100
        ($using:safe)::ShowRunspaceId($_)
    }
}

注意

這個範例會在無限迴圈中執行。 輸入 Ctrl+C 以停止執行。

類別屬性

屬性是在類別範圍中宣告的變數。 屬性可以是任何內建類型或另一個類別的實例。 類別可以有零個或多個屬性。 類別沒有屬性計數上限。

如需詳細資訊,請參閱 about_Classes_Properties

類別方法

方法會定義類別可以執行的動作。 方法可以接受指定輸入數據的參數。 方法一律會定義輸出類型。 如果方法未傳回任何輸出,它必須具有 Void 輸出類型。 如果方法未明確定義輸出類型,則方法的輸出類型為 Void

如需詳細資訊,請參閱 about_Classes_Methods

類別建構函式

建構函式可讓您設定預設值,並在建立 類別的實例時驗證對象邏輯。 建構函式的名稱與類別相同。 建構函式可能會有參數,以初始化新對象的數據成員。

如需詳細資訊,請參閱 about_Classes_Constructors

Hidden 關鍵詞

關鍵詞 hidden 會隱藏類別成員。 該成員仍可供使用者存取,而且可在物件可供使用的所有範圍內使用。 隱藏的成員會隱藏在 Cmdlet 中 Get-Member ,且無法在類別定義外部使用 Tab 鍵自動完成或 IntelliSense 來顯示。

關鍵詞 hidden 只適用於類別成員,而不是類別本身。

隱藏的類別成員包括:

  • 不包含在 類別的預設輸出中。
  • Cmdlet 所 Get-Member 傳回的類別成員清單中未包含。 若要使用 Get-Member顯示隱藏的成員,請使用 Force 參數。
  • 除非完成發生在定義隱藏成員的類別中,否則不會顯示在索引標籤完成或 IntelliSense 中。
  • 類別的公用成員。 您可以存取、繼承和修改它們。 隱藏成員不會讓它成為私用。 它只會隱藏上一個點中所述的成員。

注意

當您隱藏方法的任何多載時,該方法會從 IntelliSense 移除、完成結果,以及的預設 Get-Member輸出。 當您隱藏任何建構函式時,選項 new() 會從 IntelliSense 和完成結果中移除。

如需 關鍵詞的詳細資訊,請參閱 about_Hidden。 如需隱藏屬性的詳細資訊,請參閱 about_Classes_Properties。 如需隱藏方法的詳細資訊,請參閱 about_Classes_Methods。 如需隱藏建構函式的詳細資訊,請參閱 about_Classes_Constructors

Static 關鍵詞

static關鍵詞會定義存在於 類別中的屬性或方法,而且不需要實例。

靜態屬性一律可供使用,與類別具現化無關。 靜態屬性會在 類別的所有實例之間共用。 靜態方法一律可供使用。 整個工作階段範圍的所有靜態屬性都是即時的。

關鍵詞 static 只適用於類別成員,而不是類別本身。

如需靜態屬性的詳細資訊,請參閱 about_Classes_Properties。 如需靜態方法的詳細資訊,請參閱 about_Classes_Methods。 如需靜態建構函式的詳細資訊,請參閱 about_Classes_Constructors

PowerShell 類別中的繼承

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

PowerShell 不支援多重繼承。 類別無法直接繼承自一個以上的類別。

類別也可以繼承自定義合約的介面。 繼承自介面的類別必須實作該合約。 執行此作業時,可以使用 類別,就像實作該介面的任何其他類別一樣。

如需衍生繼承自基類或實作介面之類別的詳細資訊,請參閱 about_Classes_Inheritance

NoRunspaceAffinity 屬性

Runspace 是 PowerShell 所叫用命令的作業環境。 此環境包含目前存在的命令和數據,以及目前套用的任何語言限制。

根據預設,PowerShell 類別會與 建立它的 Runspace 相關聯。 在中使用 ForEach-Object -Parallel PowerShell類別並不安全。 類別上的方法調用會封送回建立它的 Runspace,這可能會損毀 Runspace 的狀態或造成死結。

NoRunspaceAffinity 屬性新增至類別定義可確保PowerShell類別與特定 Runspace 無關。 實例和靜態的方法調用都會使用 執行中線程的 Runspace 和線程目前的會話狀態。

已在 PowerShell 7.4 中新增 屬性。

如需具有 和 不含 NoRunspaceAffinity 屬性之類別行為差異的圖例,請參閱 範例 4

匯出具有類型加速器的類別

根據預設,PowerShell 模組不會自動匯出 PowerShell 中定義的類別和列舉。 自定義類型無法在模組外部使用,而不需要呼叫 using module 語句。

不過,如果模組新增類型加速器,這些類型加速器會在使用者匯入模組之後立即在會話中使用。

注意

將類型加速器新增至會話會使用內部(非公用)API。 使用此 API 可能會導致衝突。 如果您匯入模組時已有相同名稱的類型加速器存在,以下所述的模式會擲回錯誤。 當您從會話中移除模組時,它也會移除類型加速器。

此模式可確保類型可在會話中使用。 在 VS Code 中撰寫腳本檔案時,不會影響 IntelliSense 或完成。 若要在 VS Code 中取得自定義類型的 IntelliSense 和完成建議,您必須將 語句新增 using module 至腳本頂端。

下列模式示範如何在模組中將PowerShell類別和列舉註冊為類型加速器。 將代碼段新增至任何類型定義之後的根腳本模組。 請確定 $ExportableTypes 變數包含您要在匯入模組時提供給使用者使用的每個類型。 其他程序代碼不需要任何編輯。

# Define the types to export with type accelerators.
$ExportableTypes =@(
    [DefinedTypeName]
)
# 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()

當使用者匯入模組時,任何新增至會話類型加速器的類型都會立即可供 IntelliSense 和完成使用。 拿掉模組時,類型加速器也是如此。

從 PowerShell 模組手動匯入類別

Import-Module#requires和語句只會匯入模組函式、別名和變數,如模組所定義。 類別不會匯入。

如果模組定義類別和列舉,但不會為這些類型新增型別加速器,請使用 using module 語句來匯入它們。

語句 using module 會從文本模組或二進位模組的根模組 (ModuleToProcess) 匯入類別和列舉。 它不會一致地匯入巢狀模組中定義的類別,或定義在根模組中以點來源撰寫的腳本中所定義的類別。 定義您想要直接在根模組中供模組外部使用者使用的類別。

如需 語句的詳細資訊 using ,請參閱 about_Using

在開發期間載入新變更的程序代碼

在開發文本模組期間,通常會變更程式碼,然後使用 Force 參數載入新版的模組Import-Module。 重載模組只適用於根模組中函式的變更。 Import-Module 不會重載任何巢狀模組。 此外,無法載入任何更新的類別。

若要確保您執行的是最新版本,您必須啟動新的工作階段。 在 PowerShell 中定義的類別和列舉,且無法卸除使用 using 語句匯入的類別和列舉。

另一個常見的開發做法是將您的程式代碼分成不同的檔案。 如果您在某個檔案中使用另一個模組中定義的類別,您應該使用 using module 語句來確保函式具有所需的類別定義。

類別成員不支援 PSReference 類型

類型[ref]加速器是 PSReference 類別的速記。 使用 [ref] 進行類型轉換時,類別成員會以無訊息方式失敗。 使用 [ref] 參數的 API 無法與類別成員搭配使用。 PSReference 類別是設計來支援 COM 物件。 COM 物件有需要以傳址方式傳入值的情況。

如需詳細資訊,請參閱 PSReference 類別

限制

下列清單包含定義 PowerShell 類別的限制,以及如果有的話,這些限制的因應措施。

一般限制

  • 類別成員無法使用 PSReference 作為其類型。

    因應措施:無。

  • 無法在會話中卸除或重載 PowerShell 類別。

    因應措施:啟動新的工作階段。

  • 模組中定義的 PowerShell 類別不會自動匯入。

    因應措施:將定義的類型新增至根模組中的類型加速器清單。 這可在模組匯入時提供類型。

  • hiddenstatic 關鍵詞僅適用於類別成員,而非類別定義。

    因應措施:無。

  • 根據預設,PowerShell 類別在 Runspace 之間平行執行並不安全。 當您在類別上叫用方法時,PowerShell 會將調用封送回建立類別的 Runspace,這可能會損毀 Runspace 的狀態或造成死結。

    因應措施:將 NoRunspaceAffinity 屬性新增至類別宣告。

建構函式限制

  • 未實作建構函式鏈結。

    因應措施:定義隱藏 Init() 的方法,並從建構函式內呼叫它們。

  • 建構函式參數無法使用任何屬性,包括驗證屬性。

    因應措施:使用驗證屬性重新指派建構函式主體中的參數。

  • 建構函式參數無法定義預設值。 參數一律為必要參數。

    因應措施:無。

  • 如果隱藏任何建構函式的多載,建構函式的每個多載也會被視為隱藏。

    因應措施:無。

方法限制

  • 方法參數無法使用任何屬性,包括驗證屬性。

    因應措施:使用驗證屬性重新指派方法主體中的參數,或使用 Cmdlet 在靜態建構 Update-TypeData 函式中定義 方法。

  • 方法參數無法定義預設值。 參數一律為必要參數。

    因應措施:使用 Update-TypeData Cmdlet 在靜態建構函式中定義 方法。

  • 方法一律為公用,即使它們已隱藏也一樣。 繼承類別時可以覆寫它們。

    因應措施:無。

  • 如果隱藏方法的任何多載,該方法的每個多載也會被視為隱藏。

    因應措施:無。

屬性限制

  • 靜態屬性一律為可變動。 PowerShell 類別無法定義不可變的靜態屬性。

    因應措施:無。

  • 屬性無法使用 ValidateScript 屬性,因為類別屬性屬性自變數必須是常數。

    因應措施:定義繼承自 ValidateArgumentsAttribute 類型的類別,並改用該屬性。

  • 直接宣告的屬性無法定義自定義 getter 和 setter 實作。

    因應措施:定義隱藏的屬性,並使用 Update-TypeData 來定義可見的 getter 和 setter 邏輯。

  • 屬性無法使用 Alias 屬性。 屬性僅適用於參數、Cmdlet 和函式。

    因應 Update-TypeData 措施:使用 Cmdlet 在類別建構函式中定義別名。

  • 當 PowerShell 類別使用 ConvertTo-Json Cmdlet 轉換成 JSON 時,輸出 JSON 會包含所有隱藏的屬性及其值。

    因應措施:無

繼承限制

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

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

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

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

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

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

另請參閱