From ff596aee3945da602c4a0e1c425c85c6a4c3eddb Mon Sep 17 00:00:00 2001 From: Michael Lombardi Date: Wed, 25 Oct 2023 09:07:19 -0500 Subject: [PATCH] (AB-161630) Refactor and extend class docs Prior to this change, the class documentation did not go into sufficient detail for the syntax, definition, and limitations of class members. This change decomposes the `about_Classes` documentation into multiple files: - `about_Classes` - overview, basic information, full list of limitations. - `about_Classes_Properties` - Full documentation for defining class properties. - `about_Classes_Methods` - Full documentation for defining class methods. - `about_Classes_Constructors` - Full documentation for defining class constructors. - `about_Classes_Inheritance` - Full documentation for defining classes that inherit from a base class or implement an interface. This change fixes AB#161630. --- .../Microsoft.PowerShell.Core/About/About.md | 14 +- .../About/about_Classes.md | 1137 +++++------- .../About/about_Classes_Constructors.md | 539 ++++++ .../About/about_Classes_Inheritance.md | 1567 +++++++++++++++++ .../About/about_Classes_Methods.md | 750 ++++++++ .../About/about_Classes_Properties.md | 953 ++++++++++ 6 files changed, 4279 insertions(+), 681 deletions(-) create mode 100644 reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md create mode 100644 reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md create mode 100644 reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Methods.md create mode 100644 reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Properties.md diff --git a/reference/7.3/Microsoft.PowerShell.Core/About/About.md b/reference/7.3/Microsoft.PowerShell.Core/About/About.md index f6970fb9ac22..bf99e03e7d30 100644 --- a/reference/7.3/Microsoft.PowerShell.Core/About/About.md +++ b/reference/7.3/Microsoft.PowerShell.Core/About/About.md @@ -2,7 +2,7 @@ description: About topics cover a range of concepts about PowerShell. Help Version: 7.2.0.0 Locale: en-US -ms.date: 03/18/2022 +ms.date: 11/07/2023 title: About topics --- # About topics @@ -61,6 +61,18 @@ Describes a **CimSession** object and the difference between CIM sessions and Po ### [about_Classes](about_Classes.md) Describes how you can use classes to create your own custom types. +### [about_Classes_Constructors](about_Classes_Constructors.md) +Describes how to define constructors for PowerShell classes. + +### [about_Classes_Inheritance](about_Classes_Inheritance.md) +Describes how you can define classes that extend other types. + +### [about_Classes_Methods](about_Classes_Methods.md) +Describes how to define methods for PowerShell classes. + +### [about_Classes_Properties](about_Classes_Properties.md) +Describes how to define properties for PowerShell classes. + ### [about_Command_Precedence](about_Command_Precedence.md) Describes how PowerShell determines which command to run. diff --git a/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes.md b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes.md index 8dd97262119d..9f517a3a6ac4 100644 --- a/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes.md +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes.md @@ -1,7 +1,7 @@ --- description: Describes how you can use classes to create your own custom types. Locale: en-US -ms.date: 08/17/2023 +ms.date: 11/07/2023 online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.3&WT.mc_id=ps-gethelp schema: 2.0.0 title: about Classes @@ -13,10 +13,9 @@ Describes how you can use classes to create your own custom types. ## Long description -PowerShell 5.0 adds 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. It simplifies development of -PowerShell artifacts and accelerates coverage of management surfaces. +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 @@ -27,18 +26,21 @@ properties. ## Supported scenarios -- Define custom types in PowerShell using familiar object-oriented programming - semantics like classes, properties, methods, inheritance, etc. -- Debug types using the PowerShell language. -- Generate and handle exceptions using formal mechanisms. +- 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 -Classes are declared using the following syntax: +### Definition syntax -```syntax +Class definitions use the following syntax: + +```Syntax class [: [][,]] { [[] [hidden] [static] ...] [([]) @@ -47,22 +49,36 @@ class [: [][,]] { } ``` -Classes are instantiated using either of the following syntaxes: +### Instantiation syntax + +To instantiate an instance of a class, use one of the following syntaxes: -```syntax +```Syntax [$ =] New-Object -TypeName [ [-ArgumentList] ] ``` -```syntax +```Syntax [$ =] []::new([]) ``` +```Syntax +[$ =] []@{[]} +``` + > [!NOTE] > When using the `[]::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 hastable isn't a valid property name, +> PowerShell raises an error. -### Example syntax and usage +## Examples + +### Example 1 - Minimal definition This example shows the minimum syntax needed to create a usable class. @@ -82,375 +98,293 @@ Brand Fabrikam, Inc. ``` -## Class properties - -Properties are variables declared at class scope. A property may be of any -built-in type or an instance of another class. Classes have no restriction in -the number of properties they have. +### Example 2 - Class with instance members -### Example class with simple properties +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. ```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku +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 30 seconds per page + [timespan] GetReadingTime() { + if ($this.Pages -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.Pages * 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))" + } } - -$device = [Device]::new() -$device.Brand = "Fabrikam, Inc." -$device.Model = "Fbk5040" -$device.VendorSku = "5072641000" - -$device -``` - -```Output -Brand Model VendorSku ------ ----- --------- -Fabrikam, Inc. Fbk5040 5072641000 ``` -### Example complex types in class properties - -This example defines an empty **Rack** class using the **Device** class. The -examples, following this one, show how to add devices to the rack and how to -start with a pre-loaded rack. +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. ```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku -} - -class Rack { - [string]$Brand - [string]$Model - [string]$VendorSku - [string]$AssetId - [Device[]]$Devices = [Device[]]::new(8) - -} +$Book = [Book]::new(@{ + Title = 'The Hobbit' + Author = 'J.R.R. Tolkien' + Publisher = 'George Allen & Unwin' + PublishDate = '1937-09-21' + PageCount = 310 + Tags = @('Fantasy', 'Adventure') +}) -$rack = [Rack]::new() +$Book +$Time = $Book.GetReadingTime() +$Time = @($Time.Hours, 'hours and', $Time.Minutes, 'minutes') -join ' ' +$Age = [Math]::Floor($Book.GetPublishedAge().TotalDays / 365.25) -$rack +"It takes $Time to read $Book,`nwhich was published $Age years ago." ``` ```Output +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} -Brand : -Model : -VendorSku : -AssetId : -Devices : {$null, $null, $null, $null...} - - +It takes 10 hours and 20 minutes to read The Hobbit by J.R.R. Tolkien (1937), +which was published 86 years ago. ``` -## Class methods +### Example 3 - Class with static members -Methods define the actions that a class can perform. Methods may take -parameters that provide input data. Methods can return output. Data returned by -a method can be any defined data type. - -When defining a method for a class, you reference the current class object by -using the `$this` automatic variable. This allows you to access properties and -other methods defined in the current class. - -### Example simple class with properties and methods - -Extending the **Rack** class to add and remove devices -to or from it. +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. ```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku +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 + } - [string]ToString(){ - return ('{0}|{1}|{2}' -f $this.Brand, $this.Model, $this.VendorSku) - } -} + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() -class Rack { - [int]$Slots = 8 - [string]$Brand - [string]$Model - [string]$VendorSku - [string]$AssetId - [Device[]]$Devices = [Device[]]::new($this.Slots) - - [void] AddDevice([Device]$dev, [int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $dev + return $true } - - [void]RemoveDevice([int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $null + # 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]::Books.Add($Book) } - - [int[]] GetAvailableSlots(){ - [int]$i = 0 - return @($this.Devices.foreach{ if($_ -eq $null){$i}; $i++}) + # 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) + } } } - -$rack = [Rack]::new() - -$device = [Device]::new() -$device.Brand = "Fabrikam, Inc." -$device.Model = "Fbk5040" -$device.VendorSku = "5072641000" - -$rack.AddDevice($device, 2) - -$rack -$rack.GetAvailableSlots() ``` -```Output - -Slots : 8 -Devices : {$null, $null, Fabrikam, Inc.|Fbk5040|5072641000, $null…} -Brand : -Model : -VendorSku : -AssetId : - -0 -1 -3 -4 -5 -6 -7 - -``` - -## Output in class methods - -Methods should have a return type defined. If a method doesn't return output, -then the output type should be `[void]`. - -In class methods, no objects get sent to the pipeline except those mentioned in -the `return` statement. There's no accidental output to the pipeline from the -code. - -> [!NOTE] -> This is fundamentally different from how PowerShell functions handle output, -> where everything goes to the pipeline. - -Non-terminating errors written to the error stream from inside a class method -aren't passed through. You must use `throw` to surface a terminating error. -Using the `Write-*` cmdlets, you can still write to PowerShell's output streams -from within a class method. However, this should be avoided so that the method -emits objects using only the `return` statement. - -### Method output - -This example demonstrates no accidental output to the pipeline from class -methods, except on the `return` statement. +Now that **BookList** is defined, the book from the previous example can be +added to the list. ```powershell -class FunWithIntegers -{ - [int[]]$Integers = 0..10 - - [int[]]GetOddIntegers(){ - return $this.Integers.Where({ ($_ % 2) }) - } - - [void] GetEvenIntegers(){ - # this following line doesn't go to the pipeline - $this.Integers.Where({ ($_ % 2) -eq 0}) - } - - [string]SayHello(){ - # this following line doesn't go to the pipeline - "Good Morning" +$null -eq [BookList]::Books - # this line goes to the pipeline - return "Hello World" - } -} +[BookList]::Add($Book) -$ints = [FunWithIntegers]::new() -$ints.GetOddIntegers() -$ints.GetEvenIntegers() -$ints.SayHello() +[BookList]::Books ``` ```Output -1 -3 -5 -7 -9 -Hello World +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} ``` -## Constructor +The following snippet calls the static methods for the class. -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 arguments, to initialize the data members -of the new object. +```powershell +[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') +})) -The class can have zero or more constructors defined. If no constructor is -defined, the class is given a default parameterless constructor. This -constructor initializes all members to their default values. Object types and -strings are given null values. When you define constructor, no default -parameterless constructor is created. Create a parameterless constructor if one -is needed. +[BookList]::Find({ + param ($b) -### Constructor basic syntax + $b.PublishDate -gt '1950-01-01' +}).Title -In this example, the Device class is defined with properties and a constructor. -To use this class, the user is required to provide values for the parameters -listed in the constructor. +[BookList]::FindAll({ + param($b) -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku - - Device( - [string]$b, - [string]$m, - [string]$vsk - ){ - $this.Brand = $b - $this.Model = $m - $this.VendorSku = $vsk - } -} + $b.Author -match 'Tolkien' +}).Title -[Device]$device = [Device]::new( - "Fabrikam, Inc.", - "Fbk5040", - "5072641000" -) +[BookList]::Remove($Book) +[BookList]::Books.Title -$device -``` +[BookList]::RemoveBy('Author', 'J.R.R. Tolkien') +"Titles: $([BookList]::Books.Title)" -```Output -Brand Model VendorSku ------ ----- --------- -Fabrikam, Inc. Fbk5040 5072641000 +[BookList]::Add($Book) +[BookList]::Add($Book) ``` -### Example with multiple constructors - -In this example, the **Device** class is defined with properties, a default -constructor, and a constructor to initialize the instance. - -The default constructor sets the **brand** to **Undefined**, and leaves -**model** and **vendor-sku** with null values. - -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku +```Output +The Fellowship of the Ring - Device(){ - $this.Brand = 'Undefined' - } +The Hobbit +The Fellowship of the Ring - Device( - [string]$b, - [string]$m, - [string]$vsk - ){ - $this.Brand = $b - $this.Model = $m - $this.VendorSku = $vsk - } -} +The Fellowship of the Ring -[Device]$someDevice = [Device]::new() -[Device]$server = [Device]::new( - "Fabrikam, Inc.", - "Fbk5040", - "5072641000" -) +Titles: -$someDevice, $server +Exception: +Line | + 84 | throw "Book '$Book' already in list" + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + | Book 'The Hobbit by J.R.R. Tolkien (1937)' already in list ``` -```Output -Brand Model VendorSku ------ ----- --------- -Undefined -Fabrikam, Inc. Fbk5040 5072641000 -``` +## Class properties -## Hidden keyword +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. -The `hidden` keyword hides a property or method. The property or method 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. +For more information, see [about_Classes_Properties][01]. -For more information, see [about_Hidden][04]. +## Class methods -### Example using hidden keywords +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**. -When a **Rack** object is created, the number of slots for devices is a fixed -value that shouldn't be changed at any time. This value is known at creation -time. +For more information, see [about_Classes_Methods][02]. -Using the hidden keyword allows the developer to keep the number of slots -hidden and prevents unintentional changes to the size of the rack. +## Class constructors -```powershell -class Device { - [string]$Brand - [string]$Model -} +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. -class Rack { - [int] hidden $Slots = 8 - [string]$Brand - [string]$Model - [Device[]]$Devices = [Device[]]::new($this.Slots) +For more information, see [about_Classes_Constructors][03]. - Rack ([string]$b, [string]$m, [int]$capacity){ - ## argument validation here +## Hidden keyword - $this.Brand = $b - $this.Model = $m - $this.Slots = $capacity +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. - ## reset rack size to new capacity - $this.Devices = [Device[]]::new($this.Slots) - } -} +The `hidden` keyword only applies to class members, not a class itself. -[Rack]$r1 = [Rack]::new("Fabrikam, Inc.", "Fbk5040", 16) +Hidden class members are: -$r1 -$r1.Devices.Length -$r1.Slots -``` +- 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. -```Output -Devices Brand Model -------- ----- ----- -{$null, $null, $null, $null…} Fabrikam, Inc. Fbk5040 -16 -16 -``` +> [!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. -Notice **Slots** property isn't shown in `$r1` output. However, the size was -changed by the constructor. +For more information about the keyword, see [about_Hidden][04]. For more +information about hidden properties, see [about_Classes_Properties][05]. For +more information about hidden methods, see [about_Classes_Methods][06]. For +more information about hidden constructors, see +[about_Classes_Constructors][07]. ## Static keyword @@ -461,436 +395,279 @@ 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. -### Example using static properties and methods +The `static` keyword only applies to class members, not a class itself. -Assume the racks instantiated here exist in your data center and you want to -keep track of the racks in your code. - -```powershell -class Device { - [string]$Brand - [string]$Model -} - -class Rack { - hidden [int] $Slots = 8 - static [Rack[]]$InstalledRacks = @() - [string]$Brand - [string]$Model - [string]$AssetId - [Device[]]$Devices = [Device[]]::new($this.Slots) - - Rack ([string]$b, [string]$m, [string]$id, [int]$capacity){ - ## argument validation here - - $this.Brand = $b - $this.Model = $m - $this.AssetId = $id - $this.Slots = $capacity - - ## reset rack size to new capacity - $this.Devices = [Device[]]::new($this.Slots) - - ## add rack to installed racks - [Rack]::InstalledRacks += $this - } - - static [void]PowerOffRacks(){ - foreach ($rack in [Rack]::InstalledRacks) { - Write-Warning ("Turning off rack: " + ($rack.AssetId)) - } - } -} -``` - -### Testing static property and method exist - -``` -PS> [Rack]::InstalledRacks.Length -0 - -PS> [Rack]::PowerOffRacks() - -PS> (1..10) | ForEach-Object { ->> [Rack]::new("Adatum Corporation", "Standard-16", ->> $_.ToString("Std0000"), 16) ->> } > $null - -PS> [Rack]::InstalledRacks.Length -10 - -PS> [Rack]::InstalledRacks[3] -Brand Model AssetId Devices ------ ----- ------- ------- -Adatum Corporation Standard-16 Std0004 {$null, $null, $null, $null...} - -PS> [Rack]::PowerOffRacks() -WARNING: Turning off rack: Std0001 -WARNING: Turning off rack: Std0002 -WARNING: Turning off rack: Std0003 -WARNING: Turning off rack: Std0004 -WARNING: Turning off rack: Std0005 -WARNING: Turning off rack: Std0006 -WARNING: Turning off rack: Std0007 -WARNING: Turning off rack: Std0008 -WARNING: Turning off rack: Std0009 -WARNING: Turning off rack: Std0010 -``` - -Notice that the number of racks increases each time you run this example. - -## Using property attributes - -PowerShell includes several attribute classes that you can use to enhance data -type information and validate the data assigned to a property. Validation -attributes allow you to test that values given to properties meet defined -requirements. Validation is triggered the moment that the value is assigned. - -```powershell -class Device { - [ValidateNotNullOrEmpty()] [string]$Brand - [ValidateNotNullOrEmpty()] [string]$Model -} - -[Device]$dev = [Device]::new() - -Write-Output "Testing dev" -$dev - -$dev.Brand = "" -``` - -```Output -Testing dev - -Brand Model ------ ----- - -Exception setting "Brand": "The argument is null or empty. Provide an -argument that isn't null or empty, and then try the command again." -At C:\tmp\Untitled-5.ps1:11 char:1 -+ $dev.Brand = "" -+ ~~~~~~~~~~~~~~~ - + CategoryInfo : NotSpecified: (:) [], SetValueInvocationException - + FullyQualifiedErrorId : ExceptionWhenSetting -``` - -For more information on available attributes, see -[about_Functions_Advanced_Parameters][03]. +For more information about static properties, see +[about_Classes_Properties][08]. For more information about static methods, see +[about_Classes_Methods][09]. For more information about static constructors, +see [about_Classes_Constructors][10]. ## 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 of the base class. You can add -or override methods and properties as required. +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 from -more than one class. However, you can use interfaces for that purpose. +PowerShell doesn't support multiple inheritance. Classes can't inherit directly +from more than one class. -An inheritance implementation is defined using the `:` syntax to extend the -class or implement interfaces. The derived class should always be leftmost in -the class declaration. - -This example shows the basic PowerShell class inheritance syntax. - -```powershell -Class Derived : Base {...} -``` - -This example shows inheritance with an interface declaration coming after the -base class. - -```powershell -Class Derived : Base, Interface {...} -``` +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. -### Example of inheritance in PowerShell classes +For more information about deriving classes that inherit from a base class or +implement interfaces, see +[about_Classes_Inheritance][11]. -In this example the **Rack** and **Device** classes used in the previous -examples are better defined to: avoid property repetitions, better align common -properties, and reuse common business logic. +## Exporting classes with type accelerators -Most objects in the data center are company assets, which makes sense to start -tracking them as assets. The `DeviceType` enumeration defines device types -used by the class. For more information about enumerations, see -[about_Enum][02]. +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. -```powershell -enum DeviceType { - Undefined = 0 - Compute = 1 - Storage = 2 - Networking = 4 - Communications = 8 - Power = 16 - Rack = 32 -} -``` +However, if a module adds type accelerators, those type accelerators are +immediately available in the session after users import the module. -In our example, we're defining `Rack` and `ComputeServer` as extensions to the -`Device` class. +> [!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. ```powershell -class Asset { - [string]$Brand - [string]$Model -} - -class Device : Asset { - hidden [DeviceType]$devtype = [DeviceType]::Undefined - [string]$Status - - [DeviceType] GetDeviceType(){ - return $this.devtype +# 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 + ) } } - -class ComputeServer : Device { - hidden [DeviceType]$devtype = [DeviceType]::Compute - [string]$ProcessorIdentifier - [string]$Hostname +# 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() +``` -class Rack : Device { - hidden [DeviceType]$devtype = [DeviceType]::Rack - hidden [int]$Slots = 8 +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. - [string]$Datacenter - [string]$Location - [Device[]]$Devices = [Device[]]::new($this.Slots) +## Manually importing classes from a PowerShell module - Rack (){ - ## Just create the default rack with 8 slots - } +`Import-Module` and the `#requires` statement only import the module functions, +aliases, and variables, as defined by the module. Classes aren't imported. - Rack ([int]$s){ - ## Add argument validation logic here - $this.Devices = [Device[]]::new($s) - } +If a module defines classes and enumerations but doesn't add type accelerators +for those types, use a `using module` statement to import them. - [void] AddDevice([Device]$dev, [int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $dev - } +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. - [void] RemoveDevice([int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $null - } -} +For more information about the `using` statement, see [about_Using][12]. -$FirstRack = [Rack]::new(16) -$FirstRack.Status = "Operational" -$FirstRack.Datacenter = "PNW" -$FirstRack.Location = "F03R02.J10" - -(0..15).ForEach({ - $ComputeServer = [ComputeServer]::new() - $ComputeServer.Brand = "Fabrikam, Inc." ## Inherited from Asset - $ComputeServer.Model = "Fbk5040" ## Inherited from Asset - $ComputeServer.Status = "Installed" ## Inherited from Device - $ComputeServer.ProcessorIdentifier = "x64" ## ComputeServer - $ComputeServer.Hostname = ("r1s" + $_.ToString("000")) ## ComputeServer - $FirstRack.AddDevice($ComputeServer, $_) - }) - -$FirstRack -$FirstRack.Devices -``` +## Loading newly changed code during development -```Output -Datacenter : PNW -Location : F03R02.J10 -Devices : {r1s000, r1s001, r1s002, r1s003...} -Status : Operational -Brand : -Model : - -ProcessorIdentifier : x64 -Hostname : r1s000 -Status : Installed -Brand : Fabrikam, Inc. -Model : Fbk5040 - -ProcessorIdentifier : x64 -Hostname : r1s001 -Status : Installed -Brand : Fabrikam, Inc. -Model : Fbk5040 - -<... content truncated here for brevity ...> - -ProcessorIdentifier : x64 -Hostname : r1s015 -Status : Installed -Brand : Fabrikam, Inc. -Model : Fbk5040 -``` +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. -### Calling base class constructors +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. -To invoke a base class constructor from a subclass, add the `base` keyword. +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. -```powershell -class Person { - [int]$Age +## The PSReference type isn't supported with class members - Person([int]$a) - { - $this.Age = $a - } -} +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. -class Child : Person -{ - [string]$School +For more information, see [PSReference Class][13]. - Child([int]$a, [string]$s ) : base($a) { - $this.School = $s - } -} +## Limitations -[Child]$littleOne = [Child]::new(10, "Silver Fir Elementary School") +The following lists include limitations for defining PowerShell classes and +workaround for those limitations, if any. -$littleOne.Age -``` +### General limitations -```Output +- Class members can't use **PSReference** as their type. -10 -``` + Workaround: None. +- PowerShell classes can't be unloaded or reloaded in a session. -### Invoke base class methods + Workaround: Start a new session. +- PowerShell classes defined in a module aren't automatically imported. -To override existing methods in subclasses, declare methods using the same name -and signature. + 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. -```powershell -class BaseClass -{ - [int]days() {return 1} -} -class ChildClass1 : BaseClass -{ - [int]days () {return 2} -} + Workaround: None. -[ChildClass1]::new().days() -``` +### Constructor limitations -```Output +- Constructor chaining isn't implemented. -2 -``` + Workaround: Define hidden `Init()` methods and call them from within the + constructors. +- Constructor parameters can't use any attributes, including validation + attributes. -To call base class methods from overridden implementations, cast to the base -class (`[baseclass]$this`) on invocation. + Workaround: Reassign the parameters in the constructor body with the + validation attribute. +- Constructor parameters can't define default values. The parameters are + always mandatory. -```powershell -class BaseClass -{ - [int]days() {return 1} -} -class ChildClass1 : BaseClass -{ - [int]days () {return 2} - [int]basedays() {return ([BaseClass]$this).days()} -} + Workaround: None. +- If any overload of a constructor is hidden, every overload for the + constructor is treated as hidden too. -[ChildClass1]::new().days() -[ChildClass1]::new().basedays() -``` + Workaround: None. -```Output +### Method limitations -2 -1 -``` +- Method parameters can't use any attributes, including validation + attributes. -### Inheriting from interfaces + 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. -PowerShell classes can implement an interface using the same inheritance syntax -used to extend base classes. Because interfaces allow multiple inheritance, a -PowerShell class implementing an interface may inherit from multiple types, by -separating the type names after the colon (`:`) with commas (`,`). A PowerShell -class that implements an interface must implement all the members of that -interface. Omitting the implemention interface members causes a parse-time -error in the script. + 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. -> [!NOTE] -> PowerShell doesn't support declaring new interfaces in PowerShell script. + Workaround: None. +- If any overload of a method is hidden, every overload for that method is + treated as hidden too. -```powershell -class MyComparable : System.IComparable -{ - [int] CompareTo([object] $obj) - { - return 0; - } -} + Workaround: None. -class MyComparableBar : bar, System.IComparable -{ - [int] CompareTo([object] $obj) - { - return 0; - } -} -``` +### Property limitations -## Importing classes from a PowerShell module +- Static properties are always mutable. PowerShell classes can't define + immutable static properties. -`Import-Module` and the `#requires` statement only import the module functions, -aliases, and variables, as defined by the module. Classes aren't imported. + Workaround: None. +- Properties can't use the **ValidateScript** attribute, because class + property attribute arguments must be constants. -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. + 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. -For more information about the `using` statement, see [about_Using][07]. + Workaround: Define a hidden property and use `Add-Member` to define the + visible getter and setter logic. +- Properties can't use the **Alias** attribute. The attribute only applies to + parameters, cmdlets, and functions. -## Loading newly changed code during development + Workaround: Use the `Add-Member` 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. -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. This works for changes to functions in the root module -only. `Import-Module` doesn't reload any nested modules. Also, there's no way -to load any updated classes. + Workaround: None -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. +### Inheritance limitations -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 using the `using module` statement to ensure that the -functions have the class definitions that are needed. +- PowerShell doesn't support defining interfaces in script code. -## The PSReference type isn't supported with class members + Workaround: Define interfaces in C# and reference the assembly that defines + the interfaces. +- PowerShell classes can only inherit from one base class. -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. + 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. -For more information, see [PSReference Class][01]. + 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 -- [about_Enum][02] +- [about_Classes_Constructors][03] +- [about_Classes_Inheritance][11] +- [about_Classes_Methods][02] +- [about_Classes_Properties][01] +- [about_Enum][14] - [about_Hidden][04] -- [about_Language_Keywords][05] -- [about_Methods][06] -- [about_Using][07] +- [about_Language_Keywords][15] +- [about_Methods][16] +- [about_Using][12] -[01]: /dotnet/api/system.management.automation.psreference -[02]: about_Enum.md -[03]: about_functions_advanced_parameters.md +[01]: about_Classes_Properties.md +[02]: about_Classes_Methods.md +[03]: about_Classes_Constructors.md [04]: about_Hidden.md -[05]: about_language_keywords.md -[06]: about_methods.md -[07]: about_Using.md +[05]: about_Classes_Properties.md#hidden-properties +[06]: about_Classes_Methods.md#hidden-methods +[07]: about_Classes_Constructors.md#hidden-constructors +[08]: about_Classes_Properties.md#static-properties +[09]: about_Classes_Methods.md#static-methods +[10]: about_Classes_Constructors.md#static-constructors +[11]: about_Classes_Inheritance.md +[12]: about_Using.md +[13]: /dotnet/api/system.management.automation.psreference +[14]: about_Enum.md +[15]: about_language_keywords.md +[16]: about_methods.md diff --git a/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md new file mode 100644 index 000000000000..f5204dfa6d35 --- /dev/null +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md @@ -0,0 +1,539 @@ +--- +description: Describes how to define constructors for PowerShell classes. +Locale: en-US +ms.date: 11/07/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_constructors?view=powershell-7.3&WT.mc_id=ps-gethelp +schema: 2.0.0 +title: about Classes Constructors +--- + +# about_Classes_Constructors + +## Short description + +Describes how to define constructors for PowerShell classes. + +## Long description + +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. + +PowerShell class constructors are defined as special methods on the class. They +behave the same as PowerShell class methods with the following exceptions: + +- Constructors don't have an output type. They can't use the `return` keyword. +- Constructors always have the same name as the class. +- Constructors can't be called directly. They only run when an instance is + created. +- Constructors never appear in the output for the `Get-Member` cmdlet. + +For more information about PowerShell class methods, see +[about_Classes_Methods][01]. + +The class can have zero or more constructors defined. If no constructor is +defined, the class is given a default parameterless constructor. This +constructor initializes all members to their default values. Object types and +strings are given null values. When you define constructor, no default +parameterless constructor is created. Create a parameterless constructor if one +is needed. + +You can also define a parameterless [static constructor][02]. + +## Syntax + +Class constructors use the following syntaxes: + +### Default constructor syntax + +```Syntax + () [: base([])] { + +} +``` + +### Static constructor syntax + +```Syntax +static () [: base([])] { + +} +``` + +### Parameterized constructor syntax (one-line) + +```Syntax + ([[]$[, []$...]]) [: base([])] { + +} +``` + +### Parameterized constructor syntax (multiline) + +```Syntax + ( + []$[, + []$...] +) [: base([])] { + +} +``` + +## Examples + +### Example 1 - Defining a class with the default constructor + +The **ExampleBook1** class doesn't define a constructor. Instead, it uses the +automatic default constructor. + +```powershell +class ExampleBook1 { + [string] $Name + [string] $Author + [int] $Pages + [datetime] $PublishedOn +} + +[ExampleBook1]::new() +``` + +```Output +Name Author Pages PublishedOn +---- ------ ----- ----------- + 0 1/1/0001 12:00:00 AM +``` + +### Example 2 - Overriding the default constructor + +**ExampleBook2** explicitly defines the default constructor, setting the values +for **PublishedOn** to the current date and **Pages** to `1`. + +```powershell +class ExampleBook2 { + [string] $Name + [string] $Author + [int] $Pages + [datetime] $PublishedOn + + ExampleBook2() { + $this.PublishedOn = (Get-Date).Date + $this.Pages = 1 + } +} + +[ExampleBook2]::new() +``` + +```Output +Name Author Pages PublishedOn +---- ------ ----- ----------- + 1 11/1/2023 12:00:00 AM +``` + +### Example 3 - Defining constructor overloads + +The **ExampleBook3** class defines three constructor overloads, enabling users +to create an instance of the class from a hashtable, by passing every property +value, and by passing the name of the book and author. The class doesn't define +the default constructor. + +```powershell +class ExampleBook3 { + [string] $Name + [string] $Author + [int] $Pages + [datetime] $PublishedOn + + ExampleBook3([hashtable]$Info) { + switch ($Info.Keys) { + 'Name' { $this.Name = $Info.Name } + 'Author' { $this.Author = $Info.Author } + 'Pages' { $this.Pages = $Info.Pages } + 'PublishedOn' { $this.PublishedOn = $Info.PublishedOn } + } + } + + ExampleBook3( + [string] $Name, + [string] $Author, + [int] $Pages, + [datetime] $PublishedOn + ) { + $this.Name = $Name + $this.Author = $Author + $this.Pages = $Pages + $this.PublishedOn = $PublishedOn + } + + ExampleBook3([string]$Name, [string]$Author) { + $this.Name = $Name + $this.Author = $Author + } +} + +[ExampleBook3]::new(@{ + Name = 'The Hobbit' + Author = 'J.R.R. Tolkien' + Pages = 310 + PublishedOn = '1937-09-21' +}) +[ExampleBook3]::new('The Hobbit', 'J.R.R. Tolkien', 310, '1937-09-21') +[ExampleBook3]::new('The Hobbit', 'J.R.R. Tolkien') +``` + +```Output +Name Author Pages PublishedOn +---- ------ ----- ----------- +The Hobbit J.R.R. Tolkien 310 9/21/1937 12:00:00 AM +The Hobbit J.R.R. Tolkien 310 9/21/1937 12:00:00 AM +The Hobbit J.R.R. Tolkien 0 1/1/0001 12:00:00 AM + +MethodException: +Line | + 42 | [ExampleBook3]::new() + | ~~~~~~~~~~~~~~~~~~~~~ + | Cannot find an overload for "new" and the argument count: "0". +``` + +Calling the default constructor returns a method exception. The automatic +default constructor is only defined for a class when the class doesn't define +any constructors. Because **ExampleBook3** defines multiple overloads, the +default constructor isn't automatically added to the class. + +### Example 4 - Chaining constructors with a shared method + +```powershell +class ExampleBook4 { + [string] $Name + [string] $Author + [datetime] $PublishedOn + [int] $Pages + + ExampleBook4() { + $this.Init() + } + ExampleBook4([string]$Name) { + $this.Init($Name) + } + ExampleBook4([string]$Name, [string]$Author) { + $this.Init($Name, $Author) + } + ExampleBook4([string]$Name, [string]$Author, [datetime]$PublishedOn) { + $this.Init($Name, $Author, $PublishedOn) + } + ExampleBook4( + [string]$Name, + [string]$Author, + [datetime]$PublishedOn, + [int]$Pages + ) { + $this.Init($Name, $Author, $PublishedOn, $Pages) + } + + hidden Init() { + $this.Init('Unknown') + } + hidden Init([string]$Name) { + $this.Init($Name, 'Unknown') + } + hidden Init([string]$Name, [string]$Author) { + $this.Init($Name, $Author, (Get-Date).Date) + } + hidden Init([string]$Name, [string]$Author, [datetime]$PublishedOn) { + $this.Init($Name, $Author, $PublishedOn, 1) + } + hidden Init( + [string]$Name, + [string]$Author, + [datetime]$PublishedOn, + [int]$Pages + ) { + $this.Name = $Name + $this.Author = $Author + $this.PublishedOn = $PublishedOn + $this.Pages = $Pages + } +} + +[ExampleBook4]::new() +[ExampleBook4]::new('The Hobbit') +[ExampleBook4]::new('The Hobbit', 'J.R.R. Tolkien') +[ExampleBook4]::new('The Hobbit', 'J.R.R. Tolkien', (Get-Date '1937-9-21')) +[ExampleBook4]::new( + 'The Hobbit', + 'J.R.R. Tolkien', + (Get-Date '1937-9-21'), + 310 +) +``` + +```Output +Name Author PublishedOn Pages +---- ------ ----------- ----- +Unknown Unknown 11/1/2023 12:00:00 AM 1 +The Hobbit Unknown 11/1/2023 12:00:00 AM 1 +The Hobbit J.R.R. Tolkien 11/1/2023 12:00:00 AM 1 +The Hobbit J.R.R. Tolkien 9/21/1937 12:00:00 AM 1 +The Hobbit J.R.R. Tolkien 9/21/1937 12:00:00 AM 310 +``` + +### Example 5 - Derived class constructors + +The following examples use classes that define the static, default, and +parameterized constructors for a base class and a derived class that inherits +from the base class. + +```powershell +class BaseExample { + static [void] DefaultMessage([type]$Type) { + Write-Verbose "[$($Type.Name)] default constructor" + } + + static [void] StaticMessage([type]$Type) { + Write-Verbose "[$($Type.Name)] static constructor" + } + + static [void] ParamMessage([type]$Type, [object]$Value) { + Write-Verbose "[$($Type.Name)] param constructor ($Value)" + } + + static BaseExample() { [BaseExample]::StaticMessage([BaseExample]) } + BaseExample() { [BaseExample]::DefaultMessage([BaseExample]) } + BaseExample($Value) { [BaseExample]::ParamMessage([BaseExample], $Value) } +} + +class DerivedExample : BaseExample { + static DerivedExample() { [BaseExample]::StaticMessage([DerivedExample]) } + DerivedExample() { [BaseExample]::DefaultMessage([DerivedExample]) } + + DerivedExample([int]$Number) : base($Number) { + [BaseExample]::ParamMessage([DerivedExample], $Number) + } + DerivedExample([string]$String) { + [BaseExample]::ParamMessage([DerivedExample], $String) + } +} +``` + +The following block shows the verbose messaging for calling the base class +constructors. The static constructor message is only emitted the first time an +instance of the class is created. + +```powershell +PS> $VerbosePreference = 'Continue' +PS> $b = [BaseExample]::new() + +VERBOSE: [BaseExample] static constructor +VERBOSE: [BaseExample] default constructor + +PS> $b = [BaseExample]::new() + +VERBOSE: [BaseExample] default constructor + +PS> $b = [BaseExample]::new(1) + +VERBOSE: [BaseExample] param constructor (1) +``` + +The next block shows the verbose messaging for calling the derived class +constructors in a new session. The first time a derived class constructor is +called, the static constructors for the base class and derived class are +called. Those constructors aren't called again in the session. The constructors +for the base class always run before the constructors for the derived class. + +```powershell +PS> $VerbosePreference = 'Continue' +PS> $c = [DerivedExample]::new() + +VERBOSE: [BaseExample] static constructor +VERBOSE: [DerivedExample] static constructor +VERBOSE: [BaseExample] default constructor +VERBOSE: [DerivedExample] default constructor + +PS> $c = [DerivedExample]::new() + +VERBOSE: [BaseExample] default constructor +VERBOSE: [DerivedExample] default constructor + +PS> $c = [DerivedExample]::new(1) + +VERBOSE: [BaseExample] param constructor (1) +VERBOSE: [DerivedExample] param constructor (1) + +PS> $c = [DerivedExample]::new('foo') + +VERBOSE: [BaseExample] default constructor +VERBOSE: [DerivedExample] param constructor (foo) +``` + +## Constructor run ordering + +When a class instantiates, the code for one or more constructors executes. + +For classes that don't inherit from another class, the ordering is: + +1. The static constructor for the class. +1. The applicable constructor overload for the class. + +For derived classes that inherit from another class, the ordering is: + +1. The static constructor for the base class. +1. The static constructor for the derived class. +1. If the derived class constructor explicitly calls a base constructor + overload, it runs that constructor for the base class. If it doesn't + explicitly call a base constructor, it runs the default constructor for the + base class. +1. The applicable constructor overload for the derived class. + +In all cases, static constructors only run once in a session. + +For an example of constructor behavior and ordering, see [Example 5][05]. + +## Hidden constructors + +You can hide constructors of a class by declaring them with the `hidden` +keyword. Hidden class constructors 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 properties 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 property. +- Public members of the class. They can be accessed and modified. Hiding a + property doesn't make it private. It only hides the property as described in + the previous points. + +> [!NOTE] +> When you hide any constructor, the `new()` option is removed from +> IntelliSense and completion results. + +For more information about the `hidden` keyword, see [about_Hidden][03]. + +## Static constructors + +You can define a constructor as belonging to the class itself instead of +instances of the class by declaring the constructor with the `static` keyword. +Static class constructors: + +- Only invoke the first time an instance of the class is created in the + session. +- Can't have any parameters. +- Can't access instance properties or methods with the `$this` variable. + +## Constructors for derived classes + +When a class inherits from another class, constructors can invoke a constructor +from the base class with the `base` keyword. If the derived class doesn't +explicitly invoke a constructor from the base class, it invokes the default +constructor for the base class instead. + +To invoke a nondefault base constructor, add `: base()` after the +constructor parameters and before the body block. + +```Syntax +class : { + () : () { + # initialization code + } +} +``` + +When defining a constructor that calls a base class constructor, the parameters +can be any of the following items: + +- The variable of any parameter on the derived class constructor. +- Any static value. +- Any expression that evaluates to a value of the parameter type. + +For an example of constructors on a derived class, see [Example 5][05]. + +## Chaining constructors + +Unlike C#, PowerShell class constructors can't use chaining with the +`: this()` syntax. To reduce code duplication, use a hidden +`Init()` method with multiple overloads to the same effect. [Example 4][04] +shows a class using this pattern. + +## Adding instance properties and methods with Update-TypeData + +Beyond declaring properties and methods directly in the class definition, you +can define properties for instances of a class in the static constructor using +the `Update-TypeData` cmdlet. + +Use this snippet as a starting point for the pattern. Replace the placeholder +text in angle brackets as needed. + +```powershell +class { + static [hashtable[]] $MemberDefinitions = @( + @{ + Name = '' + MemberType = '' + Value = + } + ) + + static () { + $TypeName = [].Name + foreach ($Definition in []::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } +} +``` + +> [!TIP] +> The `Add-Member` cmdlet can add properties and methods to a class in +> non-static constructors, but the cmdlet is run every time the constructor +> is called. Using `Update-TypeData` in the static constructor ensures that the +> code for adding the members to the class only needs to run once in a session. +> +> Only add properties to the class in non-static constructors when they can't +> be defined with `Update-TypeData`, like read-only properties. + +For more information about defining instance methods with `Update-TypeData`, +see [about_Classes_Methods][06]. For more information about defining instance +properties with `Update-TypeData`, see [about_Classes_Properties][07]. + +## Limitations + +PowerShell class constructors have the following 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. + +## See also + +- [about_Classes][09] +- [about_Classes_Inheritance][10] +- [about_Classes_Methods][01] +- [about_Classes_Properties][08] + + +[01]: about_Classes_Methods.md +[02]: #static-constructors +[03]: about_Hidden.md +[04]: #example-4---chaining-constructors-with-a-shared-method +[05]: #example-5---derived-class-constructors +[06]: about_Classes_Methods.md#defining-instance-methods-with-update-typedata +[07]: about_Classes_Properties.md#defining-instance-properties-with-update-typedata +[08]: about_Classes_Properties.md +[09]: about_Classes.md +[10]: about_Classes_Inheritance.md diff --git a/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md new file mode 100644 index 000000000000..2e870426bdcb --- /dev/null +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md @@ -0,0 +1,1567 @@ +--- +description: Describes how you can define classes that extend other types. +Locale: en-US +ms.date: 11/07/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_inheritance?view=powershell-7.3&WT.mc_id=ps-gethelp +schema: 2.0.0 +title: about Classes Inheritance +--- + +# about_Classes_Inheritance + +## Short description + +Describes how you can define classes that extend other types. + +## Long description + +PowerShell classes support _inheritance_, which allows you to define a child +class that reuses (inherits), extends, or modifies the behavior of a parent +class. The class whose members are inherited is called the _base class_. The +class that inherits the members of the base class is called the _derived +class_. + +PowerShell supports single inheritance only. A class can only inherit from a +single class. However, inheritance is transitive, which allows you to define an +inheritance hierarchy for a set of types. In other words, type **D** can +inherit from type **C**, which inherits from type **B**, which inherits from +the base class type **A**. Because inheritance is transitive, the members of +type **A** are available to type **D**. + +Derived classes don't inherit all members of the base class. The following +members aren't inherited: + +- Static constructors, which initialize the static data of a class. +- Instance constructors, which you call to create a new instance of the class. + Each class must define its own constructors. + +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. + +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 is usable like any other class implementing that interface. If a class +inherits from an interface but doesn't implement the interface, PowerShell +raises a parsing error for the class. + +Some PowerShell operators depend on a class implementing a specific interface. +For example, the `-eq` operator only checks for reference equality unless the +class implements the **System.IEquatable** interface. The `-le`, `-lt`, `-ge`, +and `-gt` operators only work on classes that implement the +**System.IComparable** interface. + +A derived class uses the `:` syntax to extend a base class or implement +interfaces. The derived class should always be leftmost in the class +declaration. + +This example shows the basic PowerShell class inheritance syntax. + +```powershell +Class Derived : Base {...} +``` + +This example shows inheritance with an interface declaration coming after the +base class. + +```powershell +Class Derived : Base, Interface {...} +``` + +## Syntax + +Class inheritance uses the following syntaxes: + +### One line syntax + +```Syntax +class : [, ...] { + +} +``` + +For example: + +```powershell +# Base class only +class Derived : Base {...} +# Interface only +class Derived : System.IComparable {...} +# Base class and interface +class Derived : Base, System.IComparable {...} +``` + +### Multiline syntax + +```Syntax +class : [, + ...] { + +} +``` + +For example: + +```powershell +class Derived : Base, + System.IComparable, + System.IFormattable, + System.IConvertible { + # Derived class definition +} +``` + +## Examples + +### Example 1 - Inheriting and overriding from a base class + +The following example shows the behavior of inherited properties with and +without overriding. Run the code blocks in order after reading their +description. + +#### Defining the base class + +The first code block defines **PublishedWork** as a base class. It has two +static properties, **List** and **Artists**. Next, it defines the static +`RegisterWork()` method to add works to the static **List** property and the +artists to the **Artists** property, writing a message for each new entry in +the lists. + +The class defines three instance properties that describe a published work. +Finally, it defines the `Register()` and `ToString()` instance methods. + +```powershell +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)" } +} +``` + +#### Defining a derived class without overrides + +The first derived class is **Album**. It doesn't override any properties or +methods. It adds a new instance property, **Genres**, that doesn't exist on the +base class. + +```powershell +class Album : PublishedWork { + [string[]] $Genres = @() +} +``` + +The following code block shows the behavior of the derived **Album** class. +First, it sets the `$VerbosePreference` so that the messages from the class +methods emit to the console. It creates three instances of the class, shows +them in a table, and then registers them with the inherited static +`RegisterWork()` method. It then calls the same static method on the base class +directly. + +```powershell +$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($_) } +``` + +```Output +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. +``` + +Notice that even though the **Album** class didn't define a value for +**Category** or any constructors, the property was defined by the default +constructor of the base class. + +In the verbose messaging, the second call to the `RegisterWork()` method +reports that the works and artists are already registered. Even though the +first call to `RegisterWork()` was for the derived **Album** class, it used the +inherited static method from the base **PublishedWork** class. That method +updated the static **List** and **Artist** properties on the base class, which +the derived class didn't override. + +The next code block clears the registry and calls the `Register()` instance +method on the **Album** objects. + +```powershell +[PublishedWork]::ClearRegistry() +$Albums.Register() +``` + +```Output +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 +``` + +The instance method on the **Album** objects has the same effect as calling the +static method on the derived or base class. + +The following code block compares the static properties for the base class and +the derived class, showing that they're the same. + +```powershell +[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 +``` + +```Output +[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 +``` + +#### Defining a derived class with overrides + +The next code block defines the **Illustration** class inheriting from the base +**PublishedWork** class. The new class extends the base class by defining the +**Medium** instance property with a default value of `Unknown`. + +Unlike the derived **Album** class, **Illustration** overrides the following +properties and methods: + +- It overrides the static **Artists** property. The definition is the same, but + the **Illustration** class declares it directly. +- It overrides the **Category** instance property, setting the default value to + `Illustrations`. +- It overrides the `ToString()` instance method so the string representation of + an illustration includes the medium it was created with. + +The class also defines the static `RegisterIllustration()` method to first call +the base class `RegisterWork()` method and then add the artist to the +overridden **Artists** static property on the derived class. + +Finally, the class overrides all three constructors: + +1. The default constructor is empty except for a verbose message indicating it + created an illustration. +1. The next constructor takes two string values for the name and artist that + created the illustration. Instead of implementing the logic for setting the + **Name** and **Artist** properties, the constructor calls the appropriate + constructor from the base class. +1. The last constructor takes three string values for the name, artist, and + medium of the illustration. Both constructors write a verbose message + indicating that they created an illustration. + +```powershell +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" + } +} +``` + +The following code block shows the behavior of the derived **Illustration** +class. It creates three instances of the class, shows them in a table, and then +registers them with the inherited static `RegisterWork()` method. It then calls +the same static method on the base class directly. Finally, it writes messages +showing the list of registered artists for the base class and the derived +class. + +```powershell +$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 ', ')" +``` + +```Output +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 +``` + +The verbose messaging from creating the instances shows that: + +- When creating the first instance, the base class default constructor was + called before the derived class default constructor. +- When creating the second instance, the explicitly inherited constructor was + called for the base class before the derived class constructor. +- When creating the third instance, the base class default constructor was + called before the derived class constructor. + +The verbose messages from the `RegisterWork()` method indicate that the works +and artists were already registered. This is because the +`RegisterIllustration()` method called the `RegisterWork()` method internally. + +However, when comparing the value of the static **Artist** property for both +the base class and derived class, the values are different. The **Artists** +property for the derived class only includes illustrators, not the album +artists. Redefining the **Artist** property in the derived class prevents the +class from returning the static property on the base class. + +The final code block calls the `ToString()` method on the entries of the +static **List** property on the base class. + +```powershell +[PublishedWork]::List | ForEach-Object -Process { $_.ToString() } +``` + +```Output +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) +``` + +The **Album** instances only return the name and artist in their string. The +**Illustration** instances also included the medium in parentheses, because +that class overrode the `ToString()` method. + +### Example 2 - Implementing interfaces + +The following example shows how a class can implement one or more interfaces. +The example extends the definition of a **Temperature** class to support more +operations and behaviors. + +#### Initial class definition + +Before implementing any interfaces, the **Temperature** class is defined with +two properties, **Degrees** and **Scale**. It defines constructors and three +instance methods for returning the instance as degrees of a particular scale. + +The class defines the available scales with the **TemperatureScale** +enumeration. + +```powershell +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 +} +``` + +However, in this basic implementation, there's a few limitations as shown in +the following example output: + +```powershell +$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 +``` + +```Output +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. +``` + +The output shows that instances of **Temperature**: + +- Don't display correctly as strings. +- Can't be checked properly for equivalency. +- Can't be compared. + +These three problems can be addressed by implementing interfaces for the class. + +#### Implementing IFormattable + +The first interface to implement for the **Temperature** class is +**System.IFormattable**. This interface enables formatting an instance of the +class as different strings. To implement the interface, the class needs to +inherit from **System.IFormattable** and define the `ToString()` instance +method. + +The `ToString()` instance method needs to have the following signature: + +```powershell +[string] ToString( + [string]$Format, + [System.IFormatProvider]$FormatProvider +) { + # Implementation +} +``` + +The signature that the interface requires is listed in the +[reference documentation][01]. + +For **Temperature**, the class should support three formats: `C` to return the +instance in Celsius, `F` to return it in Fahrenheit, and `K` to return it in +Kelvin. For any other format, the method should throw a +**System.FormatException**. + +```powershell +[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'" + ) +} +``` + +In this implementation, the method defaults to the instance scale for +format and the current culture when formatting the numerical degree value +itself. It uses the `To()` instance methods to convert the degrees, +formats them to two-decimal places, and appends the appropriate degree symbol +to the string. + +With the required signature implemented, the class can also define overloads to +make it easier to return the formatted instance. + +```powershell +[string] ToString([string]$Format) { + return $this.ToString($Format, $null) +} + +[string] ToString() { + return $this.ToString($null, $null) +} +``` + +The following code shows the updated definition for **Temperature**: + +```powershell +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 +} +``` + +The output for the method overloads is shown in the following block. + +```powershell +$Temp = [Temperature]::new() +"The temperature is $Temp" +$Temp.ToString() +$Temp.ToString('K') +$Temp.ToString('F', $null) +``` + +```Output +The temperature is 0.00°C + +0.00°C + +273.15°K + +32.00°F +``` + +#### Implementing IEquatable + +Now that the **Temperature** class can be formatted for readability, users need +be able to check whether two instances of the class are equal. To support this +test, the class needs to implement the **System.IEquatable** interface. + +To implement the interface, the class needs to inherit from +**System.IEquatable** and define the `Equals()` instance method. The `Equals()` +method needs to have the following signature: + +```powershell +[bool] Equals([object]$Other) { + # Implementation +} +``` + +The signature that the interface requires is listed in the +[reference documentation][02]. + +For **Temperature**, the class should only support comparing two instances of +the class. For any other value or type, including `$null`, it should return +`$false`. When comparing two temperatures, the method should convert both +values to Kelvin, since temperatures can be equivalent even with different +scales. + +```powershell +[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() +} +``` + +With the interface method implemented, the updated definition for +**Temperature** is: + +```powershell +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() + } +} +``` + +The following block shows how the updated class behaves: + +```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 -eq `$Fahrenheit = $($Celsius -eq $Fahrenheit) +`$Celsius -ne `$Kelvin = $($Celsius -ne $Kelvin) +"@ +``` + +```Output +Temperatures are: 0.00°C, 32.00°F, 0.00°K + +$Celsius.Equals($Fahrenheit) = True +$Celsius -eq $Fahrenheit = True +$Celsius -ne $Kelvin = True +``` + +#### Implementing IComparable + +The last interface to implement for the **Temperature** class is +**System.IComparable**. When the class implements this interface, users can use +the `-lt`, `-le`, `-gt`, and `-ge` operators to compare instances of the class. + +To implement the interface, the class needs to inherit from +**System.IComparable** and define the `Equals()` instance method. The `Equals()` +method needs to have the following signature: + +```powershell +[int] CompareTo([Object]$Other) { + # Implementation +} +``` + +The signature that the interface requires is listed in the +[reference documentation][03]. + +For **Temperature**, the class should only support comparing two instances of +the class. Because the underlying type for the **Degrees** property, even when +converted to a different scale, is a floating point number, the method can rely +on the underlying type for the actual comparison. + +```powershell +[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()) +} +``` + +The final definition for the **Temperature** class is: + +```powershell +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()) + } +} +``` + +With the full definition, users can format and compare instances of the class +in PowerShell like any builtin type. + +```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) +"@ +``` + +```Output +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 +``` + +### Example 3 - Inheriting from a generic base class + +This example shows how you can derive from a generic class like +**System.Collections.Generic.List**. + +#### Using a built-in class as the type parameter + +Run the following code block. It shows how a new class can inherit from a +generic type as long as the type parameter is already defined at parse time. + +```powershell +class ExampleStringList : System.Collections.Generic.List[string] {} + +$List = [ExampleStringList]::New() +$List.AddRange([string[]]@('a','b','c')) +$List.GetType() | Format-List -Property Name, BaseType +$List +``` + +```Output +Name : ExampleStringList +BaseType : System.Collections.Generic.List`1[System.String] + +a +b +c +``` + +#### Using a custom class as the type parameter + +The next code block first defines a new class, **ExampleItem**, +with a single instance property and the `ToString()` method. Then it defines +the **ExampleItemList** class inheriting from the +**System.Collections.Generic.List** base class with **ExampleItem** as the type +parameter. + +Copy the entire code block and run it as a single statement. + +```powershell +class ExampleItem { + [string] $Name + [string] ToString() { return $this.Name } +} +class ExampleItemList : System.Collections.Generic.List[ExampleItem] {} +``` + +```Output +ParentContainsErrorRecordException: An error occurred while creating the pipeline. +``` + +Running the entire code block raises an error because PowerShell hasn't loaded +the **ExampleItem** class into the runtime yet. You can't use class name as the +type parameter for the **System.Collections.Generic.List** base class yet. + +Run the following code blocks in the order they're defined. + +```powershell +class ExampleItem { + [string] $Name + [string] ToString() { return $this.Name } +} +``` + +```powershell +class ExampleItemList : System.Collections.Generic.List[ExampleItem] {} +``` + +This time, PowerShell doesn't raise any errors. Both classes are now defined. +Run the following code block to view the behavior of the new class. + +```powershell +$List = [ExampleItemList]::New() +$List.AddRange([ExampleItem[]]@( + [ExampleItem]@{ Name = 'Foo' } + [ExampleItem]@{ Name = 'Bar' } + [ExampleItem]@{ Name = 'Baz' } +)) +$List.GetType() | Format-List -Property Name, BaseType +$List +``` + +```output +Name : ExampleItemList +BaseType : System.Collections.Generic.List`1[ExampleItem] + +Name +---- +Foo +Bar +Baz +``` + +#### Deriving a generic with a custom type parameter in a module + +The following code blocks show how you can define a class that inherits from a +generic base class that uses a custom type for the type parameter. + +Save the following code block as `GenericExample.psd1`. + +```powershell +@{ + RootModule = 'GenericExample.psm1' + ModuleVersion = '0.1.0' + GUID = '2779fa60-0b3b-4236-b592-9060c0661ac2' +} +``` + +Save the following code block as `GenericExample.InventoryItem.psm1`. + +```powershell +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))" + } +} +``` + +Save the following code block as `GenericExample.psm1`. + +```powershell +using namespace System.Collections.Generic +using module ./GenericExample.InventoryItem.psm1 + +class Inventory : List[InventoryItem] {} + +$ExportableTypes =@( + [InventoryItem] + [Inventory] +) + +foreach ($Type in $ExportableTypes) { + [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Add( + $Type.FullName, + $Type + ) +} +``` + +> [!TIP] +> The root module adds the custom types to PowerShell's type accelerators. This +> pattern enables module users to immediately access IntelliSense and +> autocomplete for the custom types without needing to use the `using module` +> statement first. +> +> For more information about this pattern, see the "Exporting with type +> accelerators" section of [about_Classes][04]. + +Import the module and verify the output. + +```powershell +Import-Module ./GenericExample.psd1 +$Inventory = [Inventory]::new() +$Inventory.GetType() +$Inventory.Add([InventoryItem]::new('Bucket')) +``` + +```Output +Name : Inventory +BaseType : System.Collections.Generic.List`1[InventoryItem] + +Name Count +---- ----- +Bucket 2 +Mop 0 +Broom 4 +``` + +The module loads without errors because the **InventoryItem** class is defined +in a different module file than the **Inventory** class. Both classes are +available to module users. + +## Inheriting a base class + +When a class inherits from a base class, it inherits the properties and methods +of the base class. It doesn't inherit the base class constructors directly, +but it can call them. + +When the base class is defined in .NET rather than PowerShell, note that: + +- PowerShell classes can't inherit from sealed classes. +- When inheriting from a generic base class, the type parameter for the generic + class can't be the derived class. Using the derived class as the type + parameter raises a parse error. + +To see how inheritance and overriding works for derived classes, see +[Example 1][05]. + +### Derived class constructors + +Derived classes don't directly inherit the constructors of the base class. If +the base class defines a default constructor and the derived class doesn't +define any constructors, new instances of the derived class use the base class +default constructor. If the base class doesn't define a default constructor, +derived class must explicitly define at least one constructor. + +Derived class constructors can invoke a constructor from the base class with +the `base` keyword. If the derived class doesn't explicitly invoke a +constructor from the base class, it invokes the default constructor for the +base class instead. + +To invoke a nondefault base constructor, add `: base()` after the +constructor parameters and before the body block. + +```Syntax +class : { + () : () { + # initialization code + } +} +``` + +When defining a constructor that calls a base class constructor, the parameters +can be any of the following items: + +- The variable of any parameter on the derived class constructor. +- Any static value. +- Any expression that evaluates to a value of the parameter type. + +The **Illustration** class in [Example 1][05] shows how a derived class can use +the base class constructors. + +### Derived class methods + +When a class derives from a base class, it inherits the methods of the base +class and their overloads. Any method overloads defined on the base class, +including hidden methods, are available on the derived class. + +A derived class can override an inherited method overload by redefining it in +the class definition. To override the overload, the parameter types must be the +same as for the base class. The output type for the overload can be different. + +Unlike constructors, methods can't use the `: base()` syntax to +invoke a base class overload for the method. The redefined overload on the +derived class completely replaces the overload defined by the base class. To +call the base class method for an instance, cast the instance variable +(`$this`) to the base class before calling the method. + +The following snippet shows how a derived class can call the base class method. + +```powershell +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()) +"@ +``` + +```Output +[BaseClass]::new().IsTrue() = True +[DerivedClass]::new().IsTrue() = False +[DerivedClass]::new().BaseIsTrue() = True +``` + +For an extended sample showing how a derived class can override inherited +methods, see the **Illustration** class in +[Example 1][05]. + +### Derived class properties + +When a class derives from a base class, it inherits the properties of the base +class. Any properties defined on the base class, including hidden properties, +are available on the derived class. + +A derived class can override an inherited property by redefining it in the +class definition. The property on the derived class uses the redefined type and +default value, if any. If the inherited property defined a default value and +the redefined property doesn't, the inherited property has no default value. + +If a derived class doesn't override a static property, accessing the static +property through the derived class accesses the static property of the base +class. Modifying the property value through the derived class modifies the +value on the base class. Any other derived class that doesn't override the +static property also uses the value of the property on the base class. Updating +the value of an inherited static property in a class that doesn't override the +property might have unintended effects for classes derived from the same base +class. + +[Example 1][05] shows how +derived classes that inherit, extend, and override the base class properties. + +### Deriving from generics + +When a class derives from a generic, the type parameter must already be defined +before PowerShell parses the derived class. If the type parameter for the +generic is a PowerShell class or enumeration defined in the same file or +code block, PowerShell raises an error. + +To derive a class from a generic base class with a custom type as the type +parameter, define the class or enumeration for the type parameter in a +different file or module and use the `using module` statement to load the type +definition. + +For an example showing how to inherit from a generic base class, see +[Example 3][06]. + +### Useful classes to inherit + +There are a few classes that can be useful to inherit when authoring PowerShell +modules. This section lists a few base classes and what a class derived from +them can be used for. + +- **System.Attribute** - Derive classes to define attributes that can be used + for variables, parameters, class and enumeration definitions, and more. +- **System.Management.Automation.ArgumentTransformationAttribute** - Derive + classes to handle converting input for a variable or parameter into a + specific data type. +- **System.Management.Automation.ValidateArgumentsAttribute** - Derive classes + to apply custom validation to variables, parameters, and class properties. +- **System.Collections.Generic.List** - Derive classes to make creating and + managing lists of a specific data type easier. +- **System.Exception** - Derive classes to define custom errors. + +## Implementing interfaces + +A PowerShell class that implements an interface must implement all the members +of that interface. Omitting the implementation interface members causes a +parse-time error in the script. + +> [!NOTE] +> PowerShell doesn't support declaring new interfaces in PowerShell script. +> Instead, interfaces must be declared in .NET code and added to the session +> with the `Add-Type` cmdlet or the `using assembly` statement. + +When a class implements an interface, it can be used like any other class that +implements that interface. Some commands and operations limit their supported +types to classes that implement a specific interface. + +To review a sample implementation of interfaces, see [Example 2][07]. + +### Useful interfaces to implement + +There are a few interface classes that can be useful to inherit when authoring +PowerShell modules. This section lists a few base classes and what a class +derived from them can be used for. + +- **System.IEquatable** - This interface enables users to compare two instances + of the class. When a class doesn't implement this interface, PowerShell + checks for equivalency between two instances using reference equality. In + other words, an instance of the class only equals itself, even if the + property values on two instances are the same. +- **System.IComparable** - This interface enables users to compare instances of + the class with the `-le`, `-lt`, `-ge`, and `-gt` comparison operators. When + a class doesn't implement this interface, those operators raise an error. +- **System.IFormattable** - This interface enables users to format instances of + the class into different strings. This is useful for classes that have more + than one standard string representation, like budget items, bibliographies, + and temperatures. +- **System.IConvertible** - This interface enables users to convert instances + of the class to other runtime types. This is useful for classes that have an + underlying numerical value or can be converted to one. + +## 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 + +- [about_Classes][08] +- [about_Classes_Constructors][09] +- [about_Classes_Methods][10] +- [about_Classes_Properties][11] + + +[01]: /dotnet/api/system.iformattable#methods +[02]: /dotnet/api/system.iequatable-1#methods +[03]: /dotnet/api/system.icomparable#methods +[04]: about_Classes.md#exporting-classes-with-type-accelerators +[05]: #example-1---inheriting-and-overriding-from-a-base-class +[06]: #example-3---inheriting-from-a-generic-base-class +[07]: #example-2---implementing-interfaces +[08]: about_Classes.md +[09]: about_Classes_Constructors.md +[10]: about_Classes_Inheritance.md +[11]: about_Classes_Properties.md diff --git a/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Methods.md b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Methods.md new file mode 100644 index 000000000000..54bdd255b7e8 --- /dev/null +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Methods.md @@ -0,0 +1,750 @@ +--- +description: Describes how to define methods for PowerShell classes. +Locale: en-US +ms.date: 11/07/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_methods?view=powershell-7.3&WT.mc_id=ps-gethelp +schema: 2.0.0 +title: about Classes Methods +--- + +# about_Classes_Methods + +## Short description + +Describes how to define methods for PowerShell classes. + +## Long description + +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**. + +In class methods, no objects get sent to the pipeline except those specified in +the `return` statement. There's no accidental output to the pipeline from the +code. + +> [!NOTE] +> This is fundamentally different from how PowerShell functions handle output, +> where everything goes to the pipeline. + +Nonterminating errors written to the error stream from inside a class method +aren't passed through. You must use `throw` to surface a terminating error. +Using the `Write-*` cmdlets, you can still write to PowerShell's output streams +from within a class method. The cmdlets respect the [preference variables][01] +in the calling scope. However, you should avoid using the `Write-*` cmdlets so +that the method only outputs objects using the `return` statement. + +Class methods can reference the current instance of the class object by using +the `$this` automatic variable to access properties and other methods defined +in the current class. The `$this` automatic variable isn't available in static +methods. + +Class methods can have any number of attributes, including the [hidden][02] and +[static][03] attributes. + +## Syntax + +Class methods use the following syntaxes: + +### One-line syntax + +```Syntax +[[]...] [hidden] [static] [] ([]) { } +``` + +### Multiline syntax + +```Syntax +[[]...] +[hidden] +[static] +[] ([]) { + +} +``` + +## Examples + +### Example 1 - Minimal method definition + +The `GetVolume()` method of the **ExampleCube1** class returns the volume of +the cube. It defines the output type as a floating number and returns the +result of multiplying the **Height**, **Length**, and **Width** properties of +the instance. + +```powershell +class ExampleCube1 { + [float] $Height + [float] $Length + [float] $Width + + [float] GetVolume() { return $this.Height * $this.Length * $this.Width } +} + +$box = [ExampleCube1]@{ + Height = 2 + Length = 2 + Width = 3 +} + +$box.GetVolume() +``` + +```Output +12 +``` + +### Example 2 - Method with parameters + +The `GeWeight()` method takes a floating number input for the density of the +cube and returns the weight of the cube, calculated as volume multiplied by +density. + +```powershell +class ExampleCube2 { + [float] $Height + [float] $Length + [float] $Width + + [float] GetVolume() { return $this.Height * $this.Length * $this.Width } + [float] GetWeight([float]$Density) { + return $this.GetVolume() * $Density + } +} + +$cube = [ExampleCube2]@{ + Height = 2 + Length = 2 + Width = 3 +} + +$cube.GetWeight(2.5) +``` + +```Output +30 +``` + +### Example 3 - Method without output + +This example defines the `Validate()` method with the output type as +**System.Void**. This method returns no output. Instead, if the validation +fails, it throws an error. The `GetVolume()` method calls `Validate()` before +calculating the volume of the cube. If validation fails, the method terminates +before the calculation. + +```powershell +class ExampleCube3 { + [float] $Height + [float] $Length + [float] $Width + + [float] GetVolume() { + $this.Validate() + + return $this.Height * $this.Length * $this.Width + } + + [void] Validate() { + $InvalidProperties = @() + foreach ($Property in @('Height', 'Length', 'Width')) { + if ($this.$Property -le 0) { + $InvalidProperties += $Property + } + } + + if ($InvalidProperties.Count -gt 0) { + $Message = @( + 'Invalid cube properties' + "('$($InvalidProperties -join "', '")'):" + "Cube dimensions must all be positive numbers." + ) -join ' ' + throw $Message + } + } +} + +$Cube = [ExampleCube3]@{ Length = 1 ; Width = -1 } +$Cube + +$Cube.GetVolume() +``` + +```Output +Height Length Width +------ ------ ----- + 0.00 1.00 -1.00 + +Exception: +Line | + 20 | throw $Message + | ~~~~~~~~~~~~~~ + | Invalid cube properties ('Height', 'Width'): Cube dimensions must + | all be positive numbers. +``` + +The method throws an exception because the **Height** and **Width** properties +are invalid, preventing the class from calculating the current volume. + +### Example 4 - Static method with overloads + +The **ExampleCube4** class defines the static method `GetVolume()` with two +overloads. The first overload has parameters for the dimensions of the cube and +a flag to indicate whether the method should validate the input. + +The second overload only includes the numeric inputs. It calls the first +overload with `$Static` as `$true`. The second overload gives users a way to +call the method without always having to define whether to strictly validate +the input. + +The class also defines `GetVolume()` as an instance (nonstatic) method. This +method calls the second static overload, ensuring that the instance +`GetVolume()` method always validates the cube's dimensions before returning +the output value. + +```powershell +class ExampleCube4 { + [float] $Height + [float] $Length + [float] $Width + + static [float] GetVolume( + [float]$Height, + [float]$Length, + [float]$Width, + [boolean]$Strict + ) { + $Signature = "[ExampleCube4]::GetVolume({0}, {1}, {2}, {3})" + $Signature = $Signature -f $Height, $Length, $Width, $Strict + Write-Verbose "Called $Signature" + + if ($Strict) { + [ValidateScript({$_ -gt 0 })]$Height = $Height + [ValidateScript({$_ -gt 0 })]$Length = $Length + [ValidateScript({$_ -gt 0 })]$Width = $Width + } + + return $Height * $Length * $Width + } + + static [float] GetVolume([float]$Height, [float]$Length, [float]$Width) { + $Signature = "[ExampleCube4]::GetVolume($Height, $Length, $Width)" + Write-Verbose "Called $Signature" + + return [ExampleCube4]::GetVolume($Height, $Length, $Width, $true) + } + + [float] GetVolume() { + Write-Verbose "Called `$this.GetVolume()" + return [ExampleCube4]::GetVolume( + $this.Height, + $this.Length, + $this.Width + ) + } +} + +$VerbosePreference = 'Continue' +$Cube = [ExampleCube4]@{ Height = 2 ; Length = 2 } +$Cube.GetVolume() +``` + +```Output +VERBOSE: Called $this.GetVolume() +VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0) +VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0, True) + +MetadataError: +Line | + 19 | [ValidateScript({$_ -gt 0 })]$Width = $Width + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + | The variable cannot be validated because the value 0 is not a valid + | value for the Width variable. +``` + +The verbose messages in the method definitions show how the initial call to +`$this.GetVolume()` calls the static method. + +Calling the static method directly with the **Strict** parameter as `$false` +returns `0` for the volume. + +```powershell +[ExampleCube4]::GetVolume($Cube.Height, $Cube.Length, $Cube.Width, $false) +``` + +```Output +VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0, False) +0 +``` + +## Method signatures and overloads + +Every class method has a unique signature that defines how to call the method. +The method's output type, name, and parameters define the method signature. + +When a class defines more than one method with the same name, the definitions +of that method are _overloads_. Overloads for a method must have different +parameters. A method can't define two implementations with the same parameters, +even if the output types are different. + +The following class defines two methods, `Shuffle()` and `Deal()`. The `Deal()` +method defines two overloads, one without any parameters and the other with the +**Count** parameter. + +```powershell +class CardDeck { + [string[]]$Cards = @() + hidden [string[]]$Dealt = @() + hidden [string[]]$Suits = @('Clubs', 'Diamonds', 'Hearts', 'Spades') + hidden [string[]]$Values = 2..10 + @('Jack', 'Queen', 'King', 'Ace') + + CardDeck() { + foreach($Suit in $this.Suits) { + foreach($Value in $this.Values) { + $this.Cards += "$Value of $Suit" + } + } + $this.Shuffle() + } + + [void] Shuffle() { + $this.Cards = $this.Cards + $this.Dealt | Where-Object -FilterScript { + -not [string]::IsNullOrEmpty($_) + } | Get-Random -Count $this.Cards.Count + } + + [string] Deal() { + if ($this.Cards.Count -eq 0) { throw "There are no cards left." } + + $Card = $this.Cards[0] + $this.Cards = $this.Cards[1..$this.Cards.Count] + $this.Dealt += $Card + + return $Card + } + + [string[]] Deal([int]$Count) { + if ($Count -gt $this.Cards.Count) { + throw "There are only $($this.Cards.Count) cards left." + } elseif ($Count -lt 1) { + throw "You must deal at least 1 card." + } + + return (1..$Count | ForEach-Object { $this.Deal() }) + } +} +``` + +## Method output + +By default, methods don't have any output. If a method signature includes an +explicit output type other than **Void**, the method must return an object of +that type. Methods don't emit any output except when the `return` keyword +explicitly returns an object. + +## Method parameters + +Class methods can define input parameters to use in the method body. Method +parameters are enclosed in parentheses and are separated by commas. Empty +parentheses indicate that the method requires no parameters. + +Parameters can be defined on a single line or multiple lines. The following +blocks show the syntax for method parameters. + +```Syntax +([[]]$[, [[]]$]) +``` + +```Syntax +( + [[]]$[, + [[]]$] +) +``` + +Method parameters can be strongly typed. If a parameter isn't typed, the method +accepts any object for that parameter. If the parameter is typed, the method +tries to convert the value for that parameter to the correct type, throwing an +exception if the input can't be converted. + +Method parameters can't define default values. All method parameters are +mandatory. + +Method parameters can't have any other attributes. This prevents methods from +using parameters with the `Validate*` attributes. For more information about +the validation attributes, see [about_Functions_Advanced_Parameters][04]. + +You can use one of the following patterns to add validation to method +parameters: + +1. Reassign the parameters to the same variables with the required validation + attributes. This works for both static and instance methods. For an example + of this pattern, see [Example 4][05]. +1. Use `Update-TypeData` to define a `ScriptMethod` that uses validation + attributes on the parameters directly. This only works for instance methods. + For more information, see the + [Defining instance methods with Update-TypeData][06] section. + +## Hidden methods + +You can hide methods of a class by declaring them with the `hidden` keyword. +Hidden class methods are: + +- Not included in the list of class members returned by the `Get-Member` + cmdlet. To show hidden methods 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 method. +- Public members of the class. They can be called and inherited. Hiding a + method doesn't make it private. It only hides the method 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`. + +For more information about the `hidden` keyword, see [about_Hidden][07]. + +## Static methods + +You can define a method as belonging to the class itself instead of instances +of the class by declaring the method with the `static` keyword. Static class +methods: + +- Are always available, independent of class instantiation. +- Are shared across all instances of the class. +- Are always available. +- Can't access instance properties of the class. They can only access static + properties. +- Live for the entire session span. + +## Derived class methods + +When a class derives from a base class, it inherits the methods of the base +class and their overloads. Any method overloads defined on the base class, +including hidden methods, are available on the derived class. + +A derived class can override an inherited method overload by redefining it in +the class definition. To override the overload, the parameter types must be the +same as for the base class. The output type for the overload can be different. + +Unlike constructors, methods can't use the `: base()` syntax to +invoke a base class overload for the method. The redefined overload on the +derived class completely replaces the overload defined by the base class. + +The following example shows the behavior for static and instance methods on +derived classes. + +The base class defines: + +- The static methods `Now()` for returning the current time and `DaysAgo()` for + returning a date in the past. +- The instance property **TimeStamp** and a `ToString()` instance method that + returns the string representation of that property. This ensures that when an + instance is used in a string it converts to the datetime string instead of + the class name. +- The instance method `SetTimeStamp()` with two overloads. When the method is + called without parameters, it sets the **TimeStamp** to the current time. + When the method is called with a **DateTime**, it sets the **TimeStamp** to + that value. + +```powershell +class BaseClass { + static [datetime] Now() { + return Get-Date + } + static [datetime] DaysAgo([int]$Count) { + return [BaseClass]::Now().AddDays(-$Count) + } + + [datetime] $TimeStamp = [BaseClass]::Now() + + [string] ToString() { + return $this.TimeStamp.ToString() + } + + [void] SetTimeStamp([datetime]$TimeStamp) { + $this.TimeStamp = $TimeStamp + } + [void] SetTimeStamp() { + $this.TimeStamp = [BaseClass]::Now() + } +} +``` + +The next block defines classes derived from **BaseClass**: + +- **DerivedClassA** inherits from **BaseClass** without any overrides. +- **DerivedClassB** overrides the `DaysAgo()` static method to return a string + representation instead of the **DateTime** object. It also overrides the + `ToString()` instance method to return the timestamp as an ISO8601 date + string. +- **DerivedClassC** overrides the parameterless overload of the + `SetTimeStamp()` method so that setting the timestamp without parameters sets + the date to 10 days before the current date. + +```powershell +class DerivedClassA : BaseClass {} +class DerivedClassB : BaseClass { + static [string] DaysAgo([int]$Count) { + return [BaseClass]::DaysAgo($Count).ToString('yyyy-MM-dd') + } + [string] ToString() { + return $this.TimeStamp.ToString('yyyy-MM-dd') + } +} +class DerivedClassC : BaseClass { + [void] SetTimeStamp() { + $this.SetTimeStamp([BaseClass]::Now().AddDays(-10)) + } +} +``` + +The following block shows the output of the static `Now()` method for the +defined classes. The output is the same for every class, because the derived +classes don't override the base class implementation of the method. + +```powershell +"[BaseClass]::Now() => $([BaseClass]::Now())" +"[DerivedClassA]::Now() => $([DerivedClassA]::Now())" +"[DerivedClassB]::Now() => $([DerivedClassB]::Now())" +"[DerivedClassC]::Now() => $([DerivedClassC]::Now())" +``` + +```Output +[BaseClass]::Now() => 11/06/2023 09:41:23 +[DerivedClassA]::Now() => 11/06/2023 09:41:23 +[DerivedClassB]::Now() => 11/06/2023 09:41:23 +[DerivedClassC]::Now() => 11/06/2023 09:41:23 +``` + +The next block calls the `DaysAgo()` static method of each class. Only the +output for **DerivedClassB** is different, because it overrode the base +implementation. + +```powershell +"[BaseClass]::DaysAgo(3) => $([BaseClass]::DaysAgo(3))" +"[DerivedClassA]::DaysAgo(3) => $([DerivedClassA]::DaysAgo(3))" +"[DerivedClassB]::DaysAgo(3) => $([DerivedClassB]::DaysAgo(3))" +"[DerivedClassC]::DaysAgo(3) => $([DerivedClassC]::DaysAgo(3))" +``` + +```Output +[BaseClass]::DaysAgo(3) => 11/03/2023 09:41:38 +[DerivedClassA]::DaysAgo(3) => 11/03/2023 09:41:38 +[DerivedClassB]::DaysAgo(3) => 2023-11-03 +[DerivedClassC]::DaysAgo(3) => 11/03/2023 09:41:38 +``` + +The following block shows the string presentation of a new instance for each +class. The representation for **DerivedClassB** is different because it +overrode the `ToString()` instance method. + +```powershell +"`$base = [BaseClass]::New() => $($base = [BaseClass]::New(); $base)" +"`$a = [DerivedClassA]::New() => $($a = [DerivedClassA]::New(); $a)" +"`$b = [DerivedClassB]::New() => $($b = [DerivedClassB]::New(); $b)" +"`$c = [DerivedClassC]::New() => $($c = [DerivedClassC]::New(); $c)" +``` + +```Output +$base = [BaseClass]::New() => 11/6/2023 9:44:57 AM +$a = [DerivedClassA]::New() => 11/6/2023 9:44:57 AM +$b = [DerivedClassB]::New() => 2023-11-06 +$c = [DerivedClassC]::New() => 11/6/2023 9:44:57 AM +``` + +The next block calls the `SetTimeStamp()` instance method for each instance, +setting the **TimeStamp** property to a specific date. Each instance has the +same date, because none of the derived classes override the parameterized +overload for the method. + +```powershell +[datetime]$Stamp = '2024-10-31' +"`$base.SetTimeStamp(`$Stamp) => $($base.SetTimeStamp($Stamp) ; $base)" +"`$a.SetTimeStamp(`$Stamp) => $($a.SetTimeStamp($Stamp); $a)" +"`$b.SetTimeStamp(`$Stamp) => $($b.SetTimeStamp($Stamp); $b)" +"`$c.SetTimeStamp(`$Stamp) => $($c.SetTimeStamp($Stamp); $c)" +``` + +```Output +$base.SetTimeStamp($Stamp) => 10/31/2024 12:00:00 AM +$a.SetTimeStamp($Stamp) => 10/31/2024 12:00:00 AM +$b.SetTimeStamp($Stamp) => 2024-10-31 +$c.SetTimeStamp($Stamp) => 10/31/2024 12:00:00 AM +``` + +The last block calls `SetTimeStamp()` without any parameters. The output shows +that the value for the **DerivedClassC** instance is set to 10 days before the +others. + +```powershell +"`$base.SetTimeStamp() => $($base.SetTimeStamp() ; $base)" +"`$a.SetTimeStamp() => $($a.SetTimeStamp(); $a)" +"`$b.SetTimeStamp() => $($b.SetTimeStamp(); $b)" +"`$c.SetTimeStamp() => $($c.SetTimeStamp(); $c)" +``` + +```Output +$base.SetTimeStamp() => 11/6/2023 9:53:58 AM +$a.SetTimeStamp() => 11/6/2023 9:53:58 AM +$b.SetTimeStamp() => 2023-11-06 +$c.SetTimeStamp() => 10/27/2023 9:53:58 AM +``` + +## Defining instance methods with Update-TypeData + +Beyond declaring methods directly in the class definition, you can define +methods for instances of a class in the static constructor using the +`Update-TypeData` cmdlet. + +Use this snippet as a starting point for the pattern. Replace the placeholder +text in angle brackets as needed. + +```powershell +class { + static [hashtable[]] $MemberDefinitions = @( + @{ + MemberName = '' + MemberType = 'ScriptMethod' + Value = { + param() + + + } + } + ) + + static () { + $TypeName = [].Name + foreach ($Definition in []::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } +} +``` + +> [!TIP] +> The `Add-Member` cmdlet can add properties and methods to a class in +> non-static constructors, but the cmdlet runs every time the constructor is +> called. Using `Update-TypeData` in the static constructor ensures that the +> code for adding the members to the class only needs to run once in a session. + +### Defining methods with default parameter values and validation attributes + +Methods defined directly in a class declaration can't define default values or +validation attributes on the method parameters. To define class methods with +default values or validation attributes, they must be defined as +**ScriptMethod** members. + +In this example, the **CardDeck** class defines a `Draw()` method that uses +both a validation attribute and a default value for the **Count** parameter. + +```powershell +class CookieJar { + [int] $Cookies = 12 + + static [hashtable[]] $MemberDefinitions = @( + @{ + MemberName = 'Eat' + MemberType = 'ScriptMethod' + Value = { + param( + [ValidateScript({ $_ -ge 1 -and $_ -le $this.Cookies })] + [int] $Count = 1 + ) + + $this.Cookies -= $Count + if ($Count -eq 1) { + "You ate 1 cookie. There are $($this.Cookies) left." + } else { + "You ate $Count cookies. There are $($this.Cookies) left." + } + } + } + ) + + static CookieJar() { + $TypeName = [CookieJar].Name + foreach ($Definition in [CookieJar]::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } +} + +$Jar = [CookieJar]::new() +$Jar.Eat(1) +$Jar.Eat() +$Jar.Eat(20) +$Jar.Eat(6) +``` + +```Output +You ate 1 cookie. There are 11 left. + +You ate 1 cookie. There are 10 left. + +MethodInvocationException: +Line | + 36 | $Jar.Eat(20) + | ~~~~~~~~~~~~ + | Exception calling "Eat" with "1" argument(s): "The attribute + | cannot be added because variable Count with value 20 would no + | longer be valid." + +You ate 6 cookies. There are 4 left. +``` + +> [!NOTE] +> While this pattern works for validation attributes, notice that the exception +> is misleading, referencing an inability to add an attribute. It might be a +> better user experience to explicitly check the value for the parameter and +> raise a meaningful error instead. That way, users can understand why they're +> seeing the error and what to do about it. + +## Limitations + +PowerShell class methods have the following 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. + +## See also + +- [about_Classes][08] +- [about_Classes_Constructors][09] +- [about_Classes_Inheritance][10] +- [about_Classes_Properties][11] +- [about_Using][12] + + +[01]: about_Preference_Variables.md +[02]: #hidden-methods +[03]: #static-methods +[04]: about_functions_advanced_parameters.md#parameter-and-variable-validation-attributes +[05]: #example-4---static-method-with-overloads +[06]: #defining-instance-methods-with-update-typedata +[07]: about_Hidden.md +[08]: about_Classes.md +[09]: about_Classes_Constructors.md +[10]: about_Classes_Inheritance.md +[11]: about_Classes_Properties.md +[12]: about_Using.md diff --git a/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Properties.md b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Properties.md new file mode 100644 index 000000000000..e5806da85947 --- /dev/null +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Properties.md @@ -0,0 +1,953 @@ +--- +description: Describes how to define properties for PowerShell classes. +Locale: en-US +ms.date: 11/07/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_properties?view=powershell-7.3&WT.mc_id=ps-gethelp +schema: 2.0.0 +title: about Classes Properties +--- + +# about_Classes_Properties + +## Short description + +Describes how to define properties for PowerShell classes. + +## Long description + +Properties are members of the class that contain data. Properties are declared +as variables in the class scope. A property can be of any built-in type or an +instance of another class. Classes can zero or more properties. Classes don't +have a maximum property count. + +Class properties can have any number of attributes, including the [hidden][01] +and [static][02] attributes. Every property definition must include a type for +the property. You can define a default value for a property. + +## Syntax + +Class properties use the following syntaxes: + +### One-line syntax + +```Syntax +[[]...] [] $ [= ] +``` + +### Multiline syntax + +```Syntax +[[]...] +[] +$ [= ] +``` + +## Examples + +### Example 1 - Minimal class properties + +The properties of the **ExampleProject1** class use built-in types without any +attributes or default values. + +```powershell +class ExampleProject1 { + [string] $Name + [int] $Size + [bool] $Completed + [string] $Assignee + [datetime] $StartDate + [datetime] $EndDate + [datetime] $DueDate +} + +[ExampleProject1]::new() + +$null -eq ([ExampleProject1]::new()).Name +``` + +```Output +Name : +Size : 0 +Completed : False +StartDate : 1/1/0001 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM + +True +``` + +The default value for the **Name** and **Assignee** properties is `$null` +because they're typed as strings, which is a reference type. The other +properties have the default value for their defined type, because they're +value type properties. For more information on the default values for +properties, see [Default property values][03]. + +### Example 2 - Class properties with custom types + +The properties for **ExampleProject2** include a custom enumeration and class +defined in PowerShell before the **ExampleProject2** class. + +```powershell +enum ProjectState { + NotTriaged + ReadyForWork + Committed + Blocked + InProgress + Done +} + +class ProjectAssignee { + [string] $DisplayName + [string] $UserName + + [string] ToString() { + return "$($this.DisplayName) ($($this.UserName))" + } +} + +class ExampleProject2 { + [string] $Name + [int] $Size + [ProjectState] $State + [ProjectAssignee] $Assignee + [datetime] $StartDate + [datetime] $EndDate + [datetime] $DueDate +} + +[ExampleProject2]@{ + Name = 'Class Property Documentation' + Size = 8 + State = 'InProgress' + Assignee = @{ + DisplayName = 'Mikey Lombardi' + UserName = 'michaeltlombardi' + } + StartDate = '2023-10-23' + DueDate = '2023-10-27' +} +``` + +```Output +Name : Class Property Documentation +Size : 8 +State : InProgress +Assignee : Mikey Lombardi (michaeltlombardi) +StartDate : 10/23/2023 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 10/27/2023 12:00:00 AM +``` + +### Example 3 - Class property with a validation attribute + +The **ExampleProject3** class defines the **Size** property as an integer that +must be greater than or equal to 0 and less than or equal to 16. It uses the +**ValidateRange** attribute to limit the value. + +```powershell +class ExampleProject3 { + [string] $Name + [ValidateRange(0, 16)] [int] $Size + [bool] $Completed + [string] $Assignee + [datetime] $StartDate + [datetime] $EndDate + [datetime] $DueDate +} + +$project = [ExampleProject3]::new() +$project +``` + +```Output +Name : +Size : 0 +Completed : False +Assignee : +StartDate : 1/1/0001 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM +``` + +When **ExampleProject3** instantiates, the **Size** defaults to 0. Setting the +property to a value within the valid range updates the value. + +```powershell +$project.Size = 8 +$project +``` + +```Output +Name : +Size : 8 +Completed : False +Assignee : +StartDate : 1/1/0001 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM +``` + +When **Size** is set to an invalid value outside the range, PowerShell raises +an exception and the value isn't changed. + +```powershell +$project.Size = 32 +$project.Size = -1 + +$project +``` + +```Output +SetValueInvocationException: +Line | + 1 | $project.Size = 32 + | ~~~~~~~~~~~~~~~~~~ + | Exception setting "Size": "The 32 argument is greater than the + | maximum allowed range of 16. Supply an argument that is less than + | or equal to 16 and then try the command again." + +SetValueInvocationException: +Line | + 2 | $project.Size = -1 + | ~~~~~~~~~~~~~~~~~~ + | Exception setting "Size": "The -1 argument is less than the minimum + | allowed range of 0. Supply an argument that is greater than or + | equal to 0 and then try the command again." + +Name : +Size : 8 +Completed : False +Assignee : +StartDate : 1/1/0001 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM +``` + +### Example 4 - Class property with an explicit default value + +The **ExampleProject4** class defaults the value for the **StartDate** property +to the current date. + +```powershell +class ExampleProject4 { + [string] $Name + [int] $Size + [bool] $Completed + [string] $Assignee + [datetime] $StartDate = (Get-Date).Date + [datetime] $EndDate + [datetime] $DueDate +} + +[ExampleProject4]::new() + +[ExampleProject4]::new().StartDate -eq (Get-Date).Date +``` + +```Output +Name : +Size : 0 +Completed : False +Assignee : +StartDate : 10/23/2023 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM + +True +``` + +### Example 5 - Hidden class property + +The **Guid** property of the **ExampleProject5** class has the `hidden` +keyword. The **Guid** property doesn't show in the default output for the +class or in the list of properties returned by `Get-Member`. + +```powershell +class ExampleProject5 { + [string] $Name + [int] $Size + [bool] $Completed + [string] $Assignee + [datetime] $StartDate + [datetime] $EndDate + [datetime] $DueDate + hidden [string] $Guid = (New-Guid).Guid +} + +$project = [ExampleProject5]::new() + +"Project GUID: $($project.Guid)" + +$project + +$project | Get-Member -MemberType Properties | Format-Table +``` + +```Output +Project GUID: c72cef84-057c-4649-8940-13490dcf72f0 + +Name : +Size : 0 +Completed : False +Assignee : +StartDate : 1/1/0001 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM + + + TypeName: ExampleProject5 + +Name MemberType Definition +---- ---------- ---------- +Assignee Property string Assignee {get;set;} +Completed Property bool Completed {get;set;} +DueDate Property datetime DueDate {get;set;} +EndDate Property datetime EndDate {get;set;} +Name Property string Name {get;set;} +Size Property int Size {get;set;} +StartDate Property datetime StartDate {get;set;} +``` + +### Example 6 - Static class property + +The **ExampleProject6** class defines the static **Projects** property as a +list of all created projects. The default constructor for the class adds the +new instance to the list of projects. + +```powershell +class ExampleProject6 { + [string] $Name + [int] $Size + [bool] $Completed + [string] $Assignee + [datetime] $StartDate + [datetime] $EndDate + [datetime] $DueDate + hidden [string] $Guid = (New-Guid).Guid + static [ExampleProject6[]] $Projects = @() + + ExampleProject6() { + [ExampleProject6]::Projects += $this + } +} + +"Project Count: $([ExampleProject6]::Projects.Count)" + +$project1 = [ExampleProject6]@{ Name = 'Project_1' } +$project2 = [ExampleProject6]@{ Name = 'Project_2' } + +[ExampleProject6]::Projects | Select-Object -Property Name, Guid +``` + +```Output +Project Count: 0 + +Name Guid +---- ---- +Project_1 75e7c8a0-f8d1-433a-a5be-fd7249494694 +Project_2 6c501be4-e68c-4df5-8fce-e49dd8366afe +``` + +### Example 7 - Defining a property in the constructor + +The **ExampleProject7** class defines the **Duration** script property in the +static class constructor with the `Update-TypeData` cmdlet. Using the +`Update-TypeData` or `Add-Member` cmdlet is the only way to define advanced +properties for PowerShell classes. + +The **Duration** property returns a value of `$null` unless both the +**StartDate** and **EndDate** properties are set and **StartDate** is defined +to be earlier than the **EndDate**. + +```powershell +class ExampleProject7 { + [string] $Name + [int] $Size + [bool] $Completed + [string] $Assignee + [datetime] $StartDate + [datetime] $EndDate + [datetime] $DueDate + + static [hashtable[]] $MemberDefinitions = @( + @{ + Name = 'Duration' + MemberType = 'ScriptProperty' + Value = { + [datetime]$UnsetDate = 0 + + $StartNotSet = $this.StartDate -eq $UnsetDate + $EndNotSet = $this.EndDate -eq $UnsetDate + $StartAfterEnd = $this.StartDate -gt $this.EndDate + + if ($StartNotSet -or $EndNotSet -or $StartAfterEnd) { + return $null + } + + return $this.EndDate - $this.StartDate + } + } + ) + + static ExampleProject7() { + $TypeName = [ExampleProject7].Name + foreach ($Definition in [ExampleProject7]::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } + + ExampleProject7() {} + + ExampleProject7([string]$Name) { + $this.Name = $Name + } +} + +$Project = [ExampleProject7]::new() +$Project + +$null -eq $Project.Duration +``` + +```Output +Duration : +Name : +Size : 0 +Completed : False +Assignee : +StartDate : 1/1/0001 12:00:00 AM +EndDate : 1/1/0001 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM + +True +``` + +The default view for an instance of the **ExampleProject7** class includes the +duration. Because the **StartDate** and **EndDate** properties aren't set, the +**Duration** property is `$null`. + +```powershell +$Project.StartDate = '2023-01-01' +$Project.EndDate = '2023-01-08' + +$Project +``` + +```Output +Duration : 7.00:00:00 +Name : +Size : 0 +Completed : False +Assignee : +StartDate : 1/1/2023 12:00:00 AM +EndDate : 1/8/2023 12:00:00 AM +DueDate : 1/1/0001 12:00:00 AM +``` + +With the properties set correctly, the **Duration** property returns a timespan +representing how long the project ran. + +## Default property values + +Every class property has an implicit default value depending on the type of the +property. + +If a property is a [reference type][04], like a string or an object, the +implicit default value is `$null`. If a property is a [value type][05], like a +number, boolean, or enumeration, the property has a default value depending on +the type: + +- Numeric types, like integers and floating-point numbers, default to `0` +- Boolean values default to `$false` +- Enumerations default to `0`, even the enumeration doesn't define a label for + `0`. + +For more information about default values in .NET, see +[Default values of C# types (C# reference)][06]. + +To define an explicit default value for a property, declare the property with +an assignment to the default value. + +For example, this definition for the **ProjectTask** class defines an explicit +default value for the **Guid** property, assigning a random GUID to each new +instance. + +```powershell +class ProjectTask { + [string] $Name + [string] $Description + [string] $Guid = (New-Guid).Guid +} + +[ProjectTask]::new() +``` + +Hidden and static properties can also have default values. + +## Hidden properties + +You can hide properties of a class by declaring them with the `hidden` keyword. +Hidden class properties 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 properties 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 property. +- Public members of the class. They can be accessed and modified. Hiding a + property doesn't make it private. It only hides the property as described in + the previous points. + +For more information about the `hidden` keyword, see [about_Hidden][07]. + +## Static properties + +You can define a property as belonging to the class itself instead of instances +of the class by declaring the property with the `static` keyword. Static class +properties: + +- Are always available, independent of class instantiation. +- Are shared across all instances of the class. +- Are always available. +- Are modifiable. Static properties can be updated. They aren't immutable by + default. +- Live for the entire session span. + +> [!IMPORTANT] +> Static properties for classes defined in PowerShell aren't immutable. They +> can + +## Derived class properties + +When a class derives from a base class, it inherits the properties of the base +class. Any properties defined on the base class, including hidden properties, +are available on the derived class. + +A derived class can override an inherited property by redefining it in the +class definition. The property on the derived class uses the redefined type and +default value, if any. If the inherited property defined a default value and +the redefined property doesn't, the inherited property has no default value. + +If a derived class doesn't override a static property, accessing the static +property through the derived class accesses the static property of the base +class. Modifying the property value through the derived class modifies the +value on the base class. Any other derived class that doesn't override the +static property also uses the value of the property on the base class. Updating +the value of an inherited static property in a class that doesn't override the +property might have unintended effects for classes derived from the same base +class. + +The following example shows the behavior for static and instance properties on +derived classes. + +```powershell +class BaseClass { + static [string] $StaticProperty = 'Static' + [string] $InstanceProperty = 'Instance' +} +class DerivedClassA : BaseClass {} +class DerivedClassB : BaseClass {} +class DerivedClassC : DerivedClassB { + [string] $InstanceProperty +} +class DerivedClassD : BaseClass { + static [string] $StaticProperty = 'Override' + [string] $InstanceProperty = 'Override' +} + +"Base instance => $([BaseClass]::new().InstanceProperty)" +"Derived instance A => $([DerivedClassA]::new().InstanceProperty)" +"Derived instance B => $([DerivedClassB]::new().InstanceProperty)" +"Derived instance C => $([DerivedClassC]::new().InstanceProperty)" +"Derived instance D => $([DerivedClassD]::new().InstanceProperty)" +``` + +```Output +Base instance => Instance +Derived instance A => Instance +Derived instance B => Instance +Derived instance C => +Derived instance D => Override +``` + +The **InstanceProperty** for **DerivedClassC** is an empty string because the +class redefined the property without setting a default value. For +**DerivedClassD** the value is `Override` because the class redefined the +property with that string as the default value. + +```powershell +"Base static => $([BaseClass]::StaticProperty)" +"Derived static A => $([DerivedClassA]::StaticProperty)" +"Derived static B => $([DerivedClassB]::StaticProperty)" +"Derived static C => $([DerivedClassC]::StaticProperty)" +"Derived static D => $([DerivedClassD]::StaticProperty)" +``` + +```Output +Base static => Static +Derived static A => Static +Derived static B => Static +Derived static C => Static +Derived static D => Override +``` + +Except for **DerivedClassD**, the value of the static property for the derived +classes is the same as the base class, because they don't redefine the +property. This applies even to **DerivedClassC**, which inherits from +**DerivedClassB** instead of directly from **BaseClass**. + +```powershell +[DerivedClassA]::StaticProperty = 'Updated from A' +"Base static => $([BaseClass]::StaticProperty)" +"Derived static A => $([DerivedClassA]::StaticProperty)" +"Derived static B => $([DerivedClassB]::StaticProperty)" +"Derived static C => $([DerivedClassC]::StaticProperty)" +"Derived static D => $([DerivedClassD]::StaticProperty)" +``` + +```Output +Base static => Updated from A +Derived static A => Updated from A +Derived static B => Updated from A +Derived static C => Updated from A +Derived static D => Override +``` + +When **StaticProperty** is accessed and modified through **DerivedClassA**, the +changed value affects every class except for **DerivedClassD**. + +For more information about class inheritance, including a comprehensive +example, see [about_Classes_Inheritance][08]. + +## Using property attributes + +PowerShell includes several attribute classes that you can use to enhance data +type information and validate the data assigned to a property. Validation +attributes allow you to test that values given to properties meet defined +requirements. Validation is triggered the moment that the value is assigned. + +For more information on available attributes, see +[about_Functions_Advanced_Parameters][09]. + +## Defining instance properties with Update-TypeData + +Beyond declaring properties directly in the class definition, you can define +properties for instances of a class in the static constructor using the +`Update-TypeData` cmdlet. + +Use this snippet as a starting point for the pattern. Replace the placeholder +text in angle brackets as needed. + +```powershell +class { + static [hashtable[]] $MemberDefinitions = @( + @{ + Name = '' + MemberType = '' + Value = + } + ) + + static () { + $TypeName = [].Name + foreach ($Definition in []::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } +} +``` + +> [!TIP] +> The `Add-Member` cmdlet can add properties and methods to a class in +> non-static constructors, but the cmdlet is run every time the constructor +> is called. Using `Update-TypeData` in the static constructor ensures that the +> code for adding the members to the class only needs to run once in a session. +> +> Only add properties to the class in non-static constructors when they can't +> be defined with `Update-TypeData`, like read-only properties. + +### Defining alias properties + +The **Alias** attribute has no effect when used on a class property +declaration. PowerShell only uses that attribute to define aliases for cmdlet, +parameter, and function names. + +To define an alias for a class property, use `Add-Member` with the +`AliasProperty` **MemberType**. + +For example, this definition of the **OperablePair** class defines two integer +properties **x** and **y** with the aliases **LeftHandSide** and +**RightHandSide** respectively. + +```powershell +class OperablePair { + [int] $x + [int] $y + + static [hashtable[]] $MemberDefinitions = @( + @{ + MemberType = 'AliasProperty' + MemberName = 'LeftHandSide' + Value = 'x' + } + @{ + MemberType = 'AliasProperty' + MemberName = 'RightHandSide' + Value = 'y' + } + ) + + static OperablePair() { + $TypeName = [OperablePair].Name + foreach ($Definition in [OperablePair]::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } + + OperablePair() {} + + OperablePair([int]$x, [int]$y) { + $this.x = $x + $this.y = $y + } + + # Math methods for the pair of values + [int] GetSum() { return $this.x + $this.y } + [int] GetProduct() { return $this.x * $this.y } + [int] GetDifference() { return $this.x - $this.y } + [float] GetQuotient() { return $this.x / $this.y } + [int] GetModulus() { return $this.x % $this.y } +} +``` + +With the aliases defined, users can access the properties with either name. + +```powershell +$pair = [OperablePair]@{ x = 8 ; RightHandSide = 3 } + +"$($pair.x) % $($pair.y) = $($pair.GetModulus())" + +$pair.LeftHandSide = 3 +$pair.RightHandSide = 2 +"$($pair.x) x $($pair.y) = $($pair.GetProduct())" +``` + +```Output +8 % 3 = 2 + +3 x 2 = 6 +``` + +### Defining calculated properties + +To define a property that references the values of other properties, use the +`Add-Member` cmdlet with the `ScriptProperty` **MemberType**. + +For example, this definition of the **Budget** class defines the **Expenses** +and **Revenues** properties as arrays of floating-point numbers. It uses the +`Add-Member` cmdlet to define calculated properties for total expenses, total +revenues, and net income. + +```powershell +class Budget { + [float[]] $Expenses + [float[]] $Revenues + + static [hashtable[]] $MemberDefinitions = @( + @{ + MemberType = 'ScriptProperty' + MemberName = 'TotalExpenses' + Value = { ($this.Expenses | Measure-Object -Sum).Sum } + } + @{ + MemberType = 'ScriptProperty' + MemberName = 'TotalRevenues' + Value = { ($this.Revenues | Measure-Object -Sum).Sum } + } + @{ + MemberType = 'ScriptProperty' + MemberName = 'NetIncome' + Value = { $this.TotalRevenues - $this.TotalExpenses } + } + ) + + static Budget() { + $TypeName = [Budget].Name + foreach ($Definition in [Budget]::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } + + Budget() {} + + Budget($Expenses, $Revenues) { + $this.Expenses = $Expenses + $this.Revenues = $Revenues + } +} + +[Budget]::new() + +[Budget]@{ + Expenses = @(2500, 1931, 3700) + Revenues = @(2400, 2100, 4150) +} +``` + +```Output +TotalExpenses : 0 +TotalRevenues : 0 +NetIncome : 0 +Expenses : +Revenues : + +TotalExpenses : 8131 +TotalRevenues : 8650 +NetIncome : 519 +Expenses : {2500, 1931, 3700} +Revenues : {2400, 2100, 4150} +``` + +### Defining properties with custom get and set logic + +PowerShell class properties can't define custom getter and setter logic +directly. You can approximate this functionality by defining a backing property +with the `hidden` keyword and using `Add-Member` to define a visible property +with custom logic for getting and setting the value. + +By convention, define the hidden backing property name with an underscore +prefix and use camel casing. For example, instead of `TaskCount`, name the +hidden backing property `_taskCount`. + +In this example, the **ProjectSize** class defines a hidden integer property +named **_value**. It defines **Value** as a `ScriptProperty` with custom logic +for getting and setting the **_value** property. The setter scriptblock handles +converting the string representation of the project to the correct size. + +```powershell +class ProjectSize { + hidden [ValidateSet(0, 1, 2, 3)] [int] $_value + + static [hashtable[]] $MemberDefinitions = @( + @{ + MemberType = 'ScriptProperty' + MemberName = 'Value' + Value = { $this._value } # Getter + SecondValue = { # Setter + $ProposedValue = $args[0] + + if ($ProposedValue -is [string]) { + switch ($ProposedValue) { + 'Small' { $this._value = 1 ; break } + 'Medium' { $this._value = 2 ; break } + 'Large' { $this._value = 3 ; break } + default { throw "Unknown size '$ProposedValue'" } + } + } else { + $this._value = $ProposedValue + } + } + } + ) + + static ProjectSize() { + $TypeName = [ProjectSize].Name + foreach ($Definition in [ProjectSize]::MemberDefinitions) { + Update-TypeData -TypeName $TypeName @Definition + } + } + + ProjectSize() {} + ProjectSize([int]$Size) { $this.Value = $Size } + ProjectSize([string]$Size) { $this.Value = $Size } + + [string] ToString() { + $Output = switch ($this._value) { + 1 { 'Small' } + 2 { 'Medium' } + 3 { 'Large' } + default { 'Undefined' } + } + + return $Output + } +} +``` + +With the custom getter and setter defined, you can set the **Value** property +as either an integer or string. + +```powershell +$size = [ProjectSize]::new() +"The initial size is: $($size._value), $size" + +$size.Value = 1 +"The defined size is: $($size._value), $size" + +$Size.Value += 1 +"The updated size is: $($size._value), $size" + +$Size.Value = 'Large' +"The final size is: $($size._value), $size" +``` + +```Output +The initial size is: 0, Undefined + +The defined size is: 1, Small + +The updated size is: 2, Medium + +The final size is: 3, Large +``` + +## Limitations + +PowerShell class properties have the following 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 `Add-Member` 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 `Add-Member` 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 + +## See also + +- [about_Classes][09] +- [about_Classes_Constructors][10] +- [about_Classes_Inheritance][11] +- [about_Classes_Methods][12] + +[01]: #hidden-properties +[02]: #static-properties +[03]: #default-property-values +[04]: /dotnet/csharp/language-reference/keywords/reference-types +[05]: /dotnet/csharp/language-reference/builtin-types/value-types +[06]: /dotnet/csharp/language-reference/builtin-types/default-values +[07]: about_Hidden.md +[09]: about_functions_advanced_parameters.md#parameter-and-variable-validation-attributes +[08]: about_Classes_Inheritance.md +[09]: about_Classes.md +[10]: about_Classes_Constructors.md +[11]: about_Classes_Inheritance.md +[12]: about_Classes_Methods.md