Muokkaa

Jaa


about_Classes

Short description

Describes how you can use classes to create your own custom types.

Long description

Starting with version 5.0, PowerShell has a formal syntax to define classes and other user-defined types. The addition of classes enables developers and IT professionals to embrace PowerShell for a wider range of use cases.

A class declaration is a blueprint used to create instances of objects at run time. When you define a class, the class name is the name of the type. For example, if you declare a class named Device and initialize a variable $dev to a new instance of Device, $dev is an object or instance of type Device. Each instance of Device can have different values in its properties.

Supported scenarios

  • Define custom types in PowerShell using object-oriented programming semantics like classes, properties, methods, inheritance, etc.
  • Define DSC resources and their associated types using the PowerShell language.
  • Define custom attributes to decorate variables, parameters, and custom type definitions.
  • Define custom exceptions that can be caught by their type name.

Syntax

Definition syntax

Class definitions use the following syntax:

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> ...]
}

Instantiation syntax

To instantiate an instance of a class, use one of the following syntaxes:

[$<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>]}

Note

When using the [<class-name>]::new() syntax, brackets around the class name are mandatory. The brackets signal a type definition for PowerShell.

The hashtable syntax only works for classes that have a default constructor that doesn't expect any parameters. It creates an instance of the class with the default constructor and then assigns the key-value pairs to the instance properties. If any key in the hashtable isn't a valid property name, PowerShell raises an error.

Examples

Example 1 - Minimal definition

This example shows the minimum syntax needed to create a usable class.

class Device {
    [string]$Brand
}

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

Example 2 - Class with instance members

This example defines a Book class with several properties, constructors, and methods. Every defined member is an instance member, not a static member. The properties and methods can only be accessed through a created instance of the class.

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))"
    }
}

The following snippet creates an instance of the class and shows how it behaves. After creating an instance of the Book class, the example uses the GetReadingTime() and GetPublishedAge() methods to write a message about the book.

$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.

Example 3 - Class with static members

The BookList class in this example builds on the Book class in example 2. While the BookList class can't be marked static itself, the implementation only defines the Books static property and a set of static methods for managing that property.

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)
        }
    }
}

Now that BookList is defined, the book from the previous example can be added to the list.

$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}

The following snippet calls the static methods for the class.

[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

Example 4 - Class definition with and without Runspace affinity

The ShowRunspaceId() method of [UnsafeClass] reports different thread Ids but the same runspace ID. Eventually, the session state is corrupted causing an error, such as 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($_)
    }
}

Note

This example runs in an infinite loop. Enter Ctrl+C to stop the execution.

The ShowRunspaceId() method of [SafeClass] reports different thread and Runspace ids.

# 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($_)
    }
}

Note

This example runs in an infinite loop. Enter Ctrl+C to stop the execution.

Class properties

Properties are variables declared in the class scope. A property can be of any built-in type or an instance of another class. Classes can have zero or more properties. Classes don't have a maximum property count.

For more information, see about_Classes_Properties.

Class methods

Methods define the actions that a class can perform. Methods can take parameters that specify input data. Methods always define an output type. If a method doesn't return any output, it must have the Void output type. If a method doesn't explicitly define an output type, the method's output type is Void.

For more information, see about_Classes_Methods.

Class constructors

Constructors enable you to set default values and validate object logic at the moment of creating the instance of the class. Constructors have the same name as the class. Constructors might have parameters, to initialize the data members of the new object.

For more information, see about_Classes_Constructors.

Hidden keyword

The hidden keyword hides a class member. The member is still accessible to the user and is available in all scopes in which the object is available. Hidden members are hidden from the Get-Member cmdlet and can't be displayed using tab completion or IntelliSense outside the class definition.

The hidden keyword only applies to class members, not a class itself.

Hidden class members are:

  • Not included in the default output for the class.
  • Not included in the list of class members returned by the Get-Member cmdlet. To show hidden members with Get-Member, use the Force parameter.
  • Not displayed in tab completion or IntelliSense unless the completion occurs in the class that defines the hidden member.
  • Public members of the class. They can be accessed, inherited, and modified. Hiding a member doesn't make it private. It only hides the member as described in the previous points.

Note

When you hide any overload for a method, that method is removed from IntelliSense, completion results, and the default output for Get-Member. When you hide any constructor, the new() option is removed from IntelliSense and completion results.

For more information about the keyword, see about_Hidden. For more information about hidden properties, see about_Classes_Properties. For more information about hidden methods, see about_Classes_Methods. For more information about hidden constructors, see about_Classes_Constructors.

Static keyword

The static keyword defines a property or a method that exists in the class and needs no instance.

A static property is always available, independent of class instantiation. A static property is shared across all instances of the class. A static method is available always. All static properties live for the entire session span.

The static keyword only applies to class members, not a class itself.

For more information about static properties, see about_Classes_Properties. For more information about static methods, see about_Classes_Methods. For more information about static constructors, see about_Classes_Constructors.

Inheritance in PowerShell classes

You can extend a class by creating a new class that derives from an existing class. The derived class inherits the properties and methods of the base class. You can add or override the base class members as required.

PowerShell doesn't support multiple inheritance. Classes can't inherit directly from more than one class.

Classes can also inherit from interfaces, which define a contract. A class that inherits from an interface must implement that contract. When it does, the class can be used like any other class implementing that interface.

For more information about deriving classes that inherit from a base class or implement interfaces, see about_Classes_Inheritance.

NoRunspaceAffinity attribute

A runspace is the operating environment for the commands invoked by PowerShell. This environment includes the commands and data that are currently present, and any language restrictions that currently apply.

By default, a PowerShell class is affiliated with the Runspace where it's created. Using a PowerShell class in ForEach-Object -Parallel isn't safe. Method invocations on the class are marshalled back to the Runspace where it was created, which can corrupt the state of the Runspace or cause a deadlock.

Adding the NoRunspaceAffinity attribute to the class definition ensures that the PowerShell class isn't affiliated with a particular runspace. Method invocations, both instance and static, use the Runspace of the running thread and the thread's current session state.

The attribute was added in PowerShell 7.4.

For an illustration of the difference in behavior for classes with and without the NoRunspaceAffinity attribute, see Example 4.

Exporting classes with type accelerators

By default, PowerShell modules don't automatically export classes and enumerations defined in PowerShell. The custom types aren't available outside of the module without calling a using module statement.

However, if a module adds type accelerators, those type accelerators are immediately available in the session after users import the module.

Note

Adding type accelerators to the session uses an internal (not public) API. Using this API may cause conflicts. The pattern described below throws an error if a type accelerator with the same name already exists when you import the module. It also removes the type accelerators when you remove the module from the session.

This pattern ensures that the types are available in a session. It doesn't affect IntelliSense or completion when authoring a script file in VS Code. To get IntelliSense and completion suggestions for custom types in VS Code, you need to add a using module statement to the top of the script.

The following pattern shows how you can register PowerShell classes and enumerations as type accelerators in a module. Add the snippet to the root script module after any type definitions. Make sure the $ExportableTypes variable contains each of the types you want to make available to users when they import the module. The other code doesn't require any editing.

# 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()

When users import the module, any types added to the type accelerators for the session are immediately available for IntelliSense and completion. When the module is removed, so are the type accelerators.

Manually importing classes from a PowerShell module

Import-Module and the #requires statement only import the module functions, aliases, and variables, as defined by the module. Classes aren't imported.

If a module defines classes and enumerations but doesn't add type accelerators for those types, use a using module statement to import them.

The using module statement imports classes and enumerations from the root module (ModuleToProcess) of a script module or binary module. It doesn't consistently import classes defined in nested modules or classes defined in scripts that are dot-sourced into the root module. Define classes that you want to be available to users outside of the module directly in the root module.

For more information about the using statement, see about_Using.

Loading newly changed code during development

During development of a script module, it's common to make changes to the code then load the new version of the module using Import-Module with the Force parameter. Reloading the module only works for changes to functions in the root module. Import-Module doesn't reload any nested modules. Also, there's no way to load any updated classes.

To ensure that you're running the latest version, you must start a new session. Classes and enumerations defined in PowerShell and imported with a using statement can't be unloaded.

Another common development practice is to separate your code into different files. If you have function in one file that use classes defined in another module, you should use the using module statement to ensure that the functions have the class definitions that are needed.

The PSReference type isn't supported with class members

The [ref] type accelerator is shorthand for the PSReference class. Using [ref] to type-cast a class member fails silently. APIs that use [ref] parameters can't be used with class members. The PSReference class was designed to support COM objects. COM objects have cases where you need to pass a value in by reference.

For more information, see PSReference Class.

Limitations

The following lists include limitations for defining PowerShell classes and workaround for those limitations, if any.

General limitations

  • Class members can't use PSReference as their type.

    Workaround: None.

  • PowerShell classes can't be unloaded or reloaded in a session.

    Workaround: Start a new session.

  • PowerShell classes defined in a module aren't automatically imported.

    Workaround: Add the defined types to the list of type accelerators in the root module. This makes the types available on module import.

  • The hidden and static keywords only apply to class members, not a class definition.

    Workaround: None.

  • By default, PowerShell classes aren't safe to use in parallel execution across runspaces. When you Invoke methods on a class, PowerShell marshalls the invocations back to the Runspace where the class was created, which can corrupt the state of the Runspace or cause a deadlock.

    Workaround: Add the NoRunspaceAffinity attribute to the class declaration.

Constructor limitations

  • Constructor chaining isn't implemented.

    Workaround: Define hidden Init() methods and call them from within the constructors.

  • Constructor parameters can't use any attributes, including validation attributes.

    Workaround: Reassign the parameters in the constructor body with the validation attribute.

  • Constructor parameters can't define default values. The parameters are always mandatory.

    Workaround: None.

  • If any overload of a constructor is hidden, every overload for the constructor is treated as hidden too.

    Workaround: None.

Method limitations

  • Method parameters can't use any attributes, including validation attributes.

    Workaround: Reassign the parameters in the method body with the validation attribute or define the method in the static constructor with the Update-TypeData cmdlet.

  • Method parameters can't define default values. The parameters are always mandatory.

    Workaround: Define the method in the static constructor with the Update-TypeData cmdlet.

  • Methods are always public, even when they're hidden. They can be overridden when the class is inherited.

    Workaround: None.

  • If any overload of a method is hidden, every overload for that method is treated as hidden too.

    Workaround: None.

Property limitations

  • Static properties are always mutable. PowerShell classes can't define immutable static properties.

    Workaround: None.

  • Properties can't use the ValidateScript attribute, because class property attribute arguments must be constants.

    Workaround: Define a class that inherits from the ValidateArgumentsAttribute type and use that attribute instead.

  • Directly declared properties can't define custom getter and setter implementations.

    Workaround: Define a hidden property and use Update-TypeData to define the visible getter and setter logic.

  • Properties can't use the Alias attribute. The attribute only applies to parameters, cmdlets, and functions.

    Workaround: Use the Update-TypeData cmdlet to define aliases in the class constructors.

  • When a PowerShell class is converted to JSON with the ConvertTo-Json cmdlet, the output JSON includes all hidden properties and their values.

    Workaround: None

Inheritance limitations

  • PowerShell doesn't support defining interfaces in script code.

    Workaround: Define interfaces in C# and reference the assembly that defines the interfaces.

  • PowerShell classes can only inherit from one base class.

    Workaround: Class inheritance is transitive. A derived class can inherit from another derived class to get the properties and methods of a base class.

  • When inheriting from a generic class or interface, the type parameter for the generic must already be defined. A class can't define itself as the type parameter for a class or interface.

    Workaround: To derive from a generic base class or interface, define the custom type in a different .psm1 file and use the using module statement to load the type. There's no workaround for a custom type to use itself as the type parameter when inheriting from a generic.

See also