about_Classes

简短说明

介绍如何使用类创建自己的自定义类型。

长说明

从版本 5.0 开始,PowerShell 具有用于定义类和其他用户定义的类型的正式语法。 添加类使开发人员和 IT 专业人员能够将 PowerShell 用于更广泛的用例。

类声明是用于在运行时创建对象的实例的蓝图。 定义类时,类名是类型的名称。 例如,如果声明名为 Device 的类并将变量 $dev 初始化为 Device 的新实例, $dev 则 是 Device 类型的对象或实例。 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 - 并行执行损坏运行空间

ShowRunspaceId()[UnsafeClass] 方法报告不同的线程 ID,但相同的运行空间 ID。 最终,会话状态已损坏,导致错误,例如 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 停止执行。

类属性

属性是在类范围中声明的变量。 属性可以是任何内置类型,也可以是另一个类的实例。 类可以有零个或多个属性。 类没有最大属性计数。

有关详细信息,请参阅 about_Classes_Properties

类方法

方法定义类可以执行的操作。 方法可以采用指定输入数据的参数。 方法始终定义输出类型。 如果方法未返回任何输出,则必须具有 Void 输出类型。 如果方法未显式定义输出类型,则该方法的输出类型为 Void

有关详细信息,请参阅 about_Classes_Methods

类构造函数

构造函数使你能够在创建 类的实例时设置默认值并验证对象逻辑。 构造函数与 类同名。 构造函数可能具有用于初始化新对象的数据成员的参数。

有关详细信息,请参阅 about_Classes_Constructors

隐藏关键字 (keyword)

关键字 (keyword) hidden 隐藏类成员。 该成员仍可供用户访问,并在对象可用的所有范围内可用。 隐藏成员在 cmdlet 中 Get-Member 隐藏,并且无法使用选项卡补全或 IntelliSense 在类定义之外显示。

关键字 (keyword) hidden 仅适用于类成员,不适用于类本身。

隐藏的类成员包括:

  • 未包含在 类的默认输出中。
  • 不包括在 cmdlet 返回 Get-Member 的类成员列表中。 若要使用 Get-Member显示隐藏成员,请使用 Force 参数。
  • 除非完成发生在定义隐藏成员的类中,否则不会显示在 Tab 自动补全或 IntelliSense 中。
  • 类的公共成员。 可以访问、继承和修改它们。 隐藏成员不会使其成为私有成员。 它只隐藏成员,如前几点所述。

注意

隐藏某个方法的任何重载时,将从 IntelliSense、完成结果和 的默认输出 Get-Member中删除该方法。 隐藏任何构造函数时,将从 new() IntelliSense 和完成结果中删除该选项。

有关关键字 (keyword) 的详细信息,请参阅about_Hidden。 有关隐藏属性的详细信息,请参阅 about_Classes_Properties。 有关隐藏方法的详细信息,请参阅 about_Classes_Methods。 有关隐藏构造函数的详细信息,请参阅 about_Classes_Constructors

Static 关键字

static 关键字 (keyword) 定义类中存在且不需要实例的属性或方法。

静态属性始终可用,独立于类实例化。 静态属性在 类的所有实例之间共享。 静态方法始终可用。 整个会话跨度的所有静态属性都处于活动状态。

关键字 (keyword) static 仅适用于类成员,不适用于类本身。

有关静态属性的详细信息,请参阅 about_Classes_Properties。 有关静态方法的详细信息,请参阅 about_Classes_Methods。 有关静态构造函数的详细信息,请参阅 about_Classes_Constructors

PowerShell 类中的继承

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

PowerShell 不支持多重继承。 类不能直接从多个类继承。

类也可以继承自定义协定的接口。 从接口继承的类必须实现该协定。 当它这样做时,类可以像实现该接口的任何其他类一样使用。

有关派生自基类或实现接口的类的详细信息,请参阅 about_Classes_Inheritance

Runspace 相关性

runspace 是 PowerShell 调用的命令的操作环境。 此环境包括当前存在的命令和数据,以及当前应用的任何语言限制。

PowerShell 类与创建它的 Runspace 关联。 在 中使用 ForEach-Object -Parallel PowerShell 类是不安全的。 类上的方法调用被封送回创建它的 Runspace ,这可能会损坏 Runspace 的状态或导致死锁。

有关运行空间相关性如何导致错误的插图,请参阅 示例 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 类不安全。 在类上调用方法时,PowerShell 会将调用封送回创建类的 Runspace ,这可能会损坏 Runspace 的状态或导致死锁。

    解决方法:无。

构造函数限制

  • 构造函数链接未实现。

    解决方法:定义隐藏 Init() 的方法,并从构造函数中调用它们。

  • 构造函数参数不能使用任何属性,包括验证属性。

    解决方法:使用验证属性重新分配构造函数正文中的参数。

  • 构造函数参数无法定义默认值。 参数始终是必需的。

    解决方法:无。

  • 如果构造函数的任何重载被隐藏,则构造函数的每个重载也被视为隐藏。

    解决方法:无。

方法限制

  • 方法参数不能使用任何属性,包括验证属性。

    解决方法:使用验证属性重新分配方法正文中的参数,或使用 cmdlet 在静态构造函数 Update-TypeData 中定义 方法。

  • 方法参数无法定义默认值。 参数始终是必需的。

    解决方法:使用 Update-TypeData cmdlet 在静态构造函数中定义 方法。

  • 方法始终是公开的,即使它们处于隐藏状态。 当继承 类时,可以重写它们。

    解决方法:无。

  • 如果方法的任何重载被隐藏,则该方法的每个重载也被视为隐藏。

    解决方法:无。

属性限制

  • 静态属性始终可变。 PowerShell 类无法定义不可变静态属性。

    解决方法:无。

  • 属性不能使用 ValidateScript 属性,因为类属性属性参数必须是常量。

    解决方法:定义从 ValidateArgumentsAttribute 类型继承的类,并改用该特性。

  • 直接声明的属性无法定义自定义 getter 和 setter 实现。

    解决方法:定义隐藏属性,并使用 Update-TypeData 定义可见的 getter 和 setter 逻辑。

  • 属性不能使用 Alias 属性。 属性仅适用于参数、cmdlet 和函数。

    解决方法:使用 Update-TypeData cmdlet 在类构造函数中定义别名。

  • 使用 cmdlet 将 PowerShell 类转换为 JSON ConvertTo-Json 时,输出 JSON 将包含所有隐藏属性及其值。

    解决方法:无

继承限制

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

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

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

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

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

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

另请参阅