about_Classes

简短说明

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

长说明

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

类声明是用于在运行时创建对象实例的蓝图。 定义类时,类名就是类型的名称。 例如,如果你声明一个名为 Device 的类并将变量 $dev 初始化为 Device 的新实例,则 $devDevice 类型的对象或实例。 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()报告不同的线程 ID,但相同的运行空间 ID 的方法[UnsafeClass]。 最终,会话状态已损坏,导致错误,例如 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 可停止执行。

[SafeClass]ShowRunspaceId() 方法报告不同的线程 Runspace ID。

# 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 隐藏类成员。 该成员仍可供用户访问,并且可在对象可用的所有范围内使用。 隐藏成员在 Get-Member cmdlet 中隐藏,不能在类定义之外使用 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 相关联。 使用 PowerShell 类 ForEach-Object -Parallel 不安全。 类上的方法调用将封送回创建它的 Runspace,这可能会破坏 Runspace 的状态或造成死锁。

NoRunspaceAffinity 属性添加到类定义可确保 PowerShell 类不与特定的运行空间关联。 实例和静态方法调用都使用运行线程的 Runspace 和线程的当前会话状态。

此属性已在 PowerShell 7.4 中添加。

有关具有和不使用 NoRunspaceAffinity 属性的类的行为差异的图示,请参阅 示例 4

导出具有类型加速器的类

默认情况下,PowerShell 模块不会自动导出 PowerShell 中定义的类和枚举。 自定义类型在模块外部不可用,无需调用 using module 语句。

但是,如果模块添加类型加速器,则用户导入模块后,这些类型加速器将在会话中立即可用。

注意

向会话添加类型加速器使用内部 API(非公共)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 的状态或导致死锁。

    解决方法:将 NoRunspaceAffinity 属性添加到类声明。

构造函数限制

  • 未实现构造函数链接。

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

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

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

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

    解决方法:无。

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

    解决方法:无。

方法限制

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

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

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

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

  • 方法始终为公共方法,即使它们被隐藏也是如此。 当继承类时,可以重写它们。

    解决方法:无。

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

    解决方法:无。

属性限制

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

    解决方法:无。

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

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

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

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

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

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

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

    解决方法:无

继承限制

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

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

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

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

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

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

另请参阅