diff --git a/reference/5.1/Microsoft.PowerShell.Core/About/About.md b/reference/5.1/Microsoft.PowerShell.Core/About/About.md index 4bc837399d9b..8b8ab76b402e 100644 --- a/reference/5.1/Microsoft.PowerShell.Core/About/About.md +++ b/reference/5.1/Microsoft.PowerShell.Core/About/About.md @@ -2,7 +2,7 @@ description: About topics cover a range of concepts about PowerShell. Help Version: 6.0 Locale: en-US -ms.date: 03/18/2022 +ms.date: 11/10/2023 title: About topics --- # About topics @@ -58,6 +58,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/5.1/Microsoft.PowerShell.Core/About/about_Classes.md b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes.md index 467dc66f7be7..640c860832f3 100644 --- a/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes.md +++ b/reference/5.1/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/10/2023 online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-5.1&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,329 @@ 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.PageCount -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.PageCount * 2 + return [timespan]::new(0, $Minutes, 0) + } + # Method to calculate how long ago a book was published + [timespan] GetPublishedAge() { + if ( + $null -eq $this.PublishDate -or + $this.PublishDate -eq [datetime]::MinValue + ) { throw 'PublishDate not defined' } + + return (Get-Date) - $this.PublishDate + } + # Method to return a string representation of the book + [string] ToString() { + return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" + } } - -$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. +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. -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. +```powershell +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 + } -### Example simple class with properties and methods + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() -Extending the **Rack** class to add and remove devices -to or from it. + return $true + } + # Ensure a book is valid for the list. + static [void] Validate([book]$Book) { + $Prefix = @( + 'Book validation failed: Book must be defined with the Title,' + 'Author, and PublishDate properties, but' + ) -join ' ' + if ($null -eq $Book) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Book.Title)) { + throw "$Prefix Title wasn't defined" + } + if ([string]::IsNullOrEmpty($Book.Author)) { + throw "$Prefix Author wasn't defined" + } + if ([datetime]::MinValue -eq $Book.PublishDate) { + throw "$Prefix PublishDate wasn't defined" + } + } + # Static methods to manage the list of books. + # Add a book if it's not already in the list. + static [void] Add([Book]$Book) { + [BookList]::Initialize() + [BookList]::Validate($Book) + if ([BookList]::Books.Contains($Book)) { + throw "Book '$Book' already in list" + } -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku + $FindPredicate = { + param([Book]$b) - [string]ToString(){ - return ('{0}|{1}|{2}' -f $this.Brand, $this.Model, $this.VendorSku) - } -} + $b.Title -eq $Book.Title -and + $b.Author -eq $Book.Author -and + $b.PublishDate -eq $Book.PublishDate + }.GetNewClosure() + if ([BookList]::Books.Find($FindPredicate)) { + throw "Book '$Book' already in list" + } -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 + [BookList]::Books.Add($Book) } - - [void]RemoveDevice([int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $null + # Clear the list of books. + static [void] Clear() { + [BookList]::Initialize() + [BookList]::Books.Clear() } - - [int[]] GetAvailableSlots(){ - [int]$i = 0 - return @($this.Devices.foreach{ if($_ -eq $null){$i}; $i++}) + # 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. +```Output +The Fellowship of the Ring -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku +The Hobbit +The Fellowship of the Ring - Device(){ - $this.Brand = 'Undefined' - } +The Fellowship of the Ring - Device( - [string]$b, - [string]$m, - [string]$vsk - ){ - $this.Brand = $b - $this.Model = $m - $this.VendorSku = $vsk - } -} +Titles: -[Device]$someDevice = [Device]::new() -[Device]$server = [Device]::new( - "Fabrikam, Inc.", - "Fbk5040", - "5072641000" -) - -$someDevice, $server +Book 'The Hobbit by J.R.R. Tolkien (1937)' already in list +At C:\code\classes.examples.ps1:114 char:13 ++ throw "Book '$Book' already in list" ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : OperationStopped: (Book 'The Hobbi...alread + y in list:String) [], RuntimeException + + FullyQualifiedErrorId : 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 +431,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 - -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 +The `static` keyword only applies to class members, not a class itself. - ## 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. - -PowerShell doesn't support multiple inheritance. Classes can't inherit from -more than one class. However, you can use interfaces for that purpose. - -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 {...} -``` +class. The derived class inherits the properties and methods of the base class. +You can add or override the base class members as required. -This example shows inheritance with an interface declaration coming after the -base class. +PowerShell doesn't support multiple inheritance. Classes can't inherit directly +from more than one 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/5.1/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md new file mode 100644 index 000000000000..391d14cd80fb --- /dev/null +++ b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md @@ -0,0 +1,541 @@ +--- +description: Describes how to define constructors for PowerShell classes. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_constructors?view=powershell-5.1&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') +[ExampleBook3]::new() +``` + +```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 + +Cannot find an overload for "new" and the argument count: "0". +At C:\code\classes.examples.ps1:42 char:1 ++ [ExampleBook3]::new() ++ ~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (:) [], MethodException + + FullyQualifiedErrorId : MethodCountCouldNotFindBes +``` + +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/5.1/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md new file mode 100644 index 000000000000..8c0610d461f9 --- /dev/null +++ b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md @@ -0,0 +1,1614 @@ +--- +description: Describes how you can define classes that extend other types. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_inheritance?view=powershell-5.1&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 + +Cannot compare "Temperature" because it is not IComparable. +At line:1 char:1 ++ $Celsius -gt $Kelvin ++ ~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : InvalidOperation: (:) [], RuntimeException + + FullyQualifiedErrorId : NotIcomparable +``` + +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() + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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()) + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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 +An error occurred while creating the pipeline. + + CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRe + cordException + + FullyQualifiedErrorId : RuntimeException +``` + +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] {} + +# Define the types to export with type accelerators. +$ExportableTypes =@( + [InventoryItem] + [Inventory] +) +# Get the internal TypeAccelerators class to use its static methods. +$TypeAcceleratorsClass = [psobject].Assembly.GetType( + 'System.Management.Automation.TypeAccelerators' +) +# Ensure none of the types would clobber an existing type accelerator. +# If a type accelerator with the same name exists, throw an exception. +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +foreach ($Type in $ExportableTypes) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + $Message = @( + "Unable to register type accelerator '$($Type.FullName)'" + 'Accelerator already exists.' + ) -join ' - ' + + throw [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($Message), + 'TypeAcceleratorAlreadyExists', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $Type.FullName + ) + } +} +# Add type accelerators for every exportable type. +foreach ($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Add($Type.FullName, $Type) +} +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() +``` + +> [!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() | Format-List -Property Name, BaseType + +$Inventory.Add([InventoryItem]::new('Bucket', 2)) +$Inventory.Add([InventoryItem]::new('Mop')) +$Inventory.Add([InventoryItem]@{ Name = 'Broom' ; Count = 4 }) +$Inventory +``` + +```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/5.1/Microsoft.PowerShell.Core/About/about_Classes_Methods.md b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Methods.md new file mode 100644 index 000000000000..ac84bda241ca --- /dev/null +++ b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Methods.md @@ -0,0 +1,756 @@ +--- +description: Describes how to define methods for PowerShell classes. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_methods?view=powershell-5.1&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 + +Invalid cube properties ('Height', 'Width'): Cube dimensions must all be +positive numbers. +At C:\code\classes.examples.ps1:26 char:13 ++ throw $Message ++ ~~~~~~~~~~~~~~ + + CategoryInfo : OperationStopped: (Invalid cube pr...sitive + numbers.:String) [], RuntimeException + + FullyQualifiedErrorId : 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) + +The variable cannot be validated because the value 0 is not a valid value +for the Width variable. +At C:\code\classes.examples.ps1:19 char:13 ++ [ValidateScript({$_ -gt 0 })]$Width = $Width ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : MetadataError: (:) [], ValidationMetadataEx + ception + + FullyQualifiedErrorId : ValidateSetFailure +``` + +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. + +Exception calling "Eat" with "1" argument(s): "The attribute cannot be +added because variable Count with value 20 would no longer be valid." +At line:1 char:1 ++ $Jar.Eat(20) ++ ~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (:) [], MethodInvocationExcep + tion + + FullyQualifiedErrorId : ScriptMethodRuntimeException + +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/5.1/Microsoft.PowerShell.Core/About/about_Classes_Properties.md b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Properties.md new file mode 100644 index 000000000000..e00460269b6a --- /dev/null +++ b/reference/5.1/Microsoft.PowerShell.Core/About/about_Classes_Properties.md @@ -0,0 +1,963 @@ +--- +description: Describes how to define properties for PowerShell classes. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_properties?view=powershell-5.1&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 +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." +At line:1 char:1 ++ $project.Size = 32 ++ ~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (:) [], SetValueInvocationExc + eption + + FullyQualifiedErrorId : ExceptionWhenSetting + +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." +At line:1 char:1 ++ $project.Size = -1 ++ ~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (:) [], SetValueInvocationExc + eption + + FullyQualifiedErrorId : ExceptionWhenSetting + +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 = @( + @{ + MemberName = '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() +``` + +```Output +Name Description Guid +---- ----------- ---- + aa96350c-358d-465c-96d1-a49949219eec +``` + +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 diff --git a/reference/7.2/Microsoft.PowerShell.Core/About/About.md b/reference/7.2/Microsoft.PowerShell.Core/About/About.md index 411e0cb6996c..11011db8f6ad 100644 --- a/reference/7.2/Microsoft.PowerShell.Core/About/About.md +++ b/reference/7.2/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/10/2023 title: About topics --- # About topics @@ -58,6 +58,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.2/Microsoft.PowerShell.Core/About/about_Classes.md b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes.md index 0fd5a3e6c3bc..38e1ffaa99b6 100644 --- a/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes.md +++ b/reference/7.2/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/10/2023 online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.2&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,326 @@ 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.PageCount -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.PageCount * 2 + return [timespan]::new(0, $Minutes, 0) + } + # Method to calculate how long ago a book was published + [timespan] GetPublishedAge() { + if ( + $null -eq $this.PublishDate -or + $this.PublishDate -eq [datetime]::MinValue + ) { throw 'PublishDate not defined' } + + return (Get-Date) - $this.PublishDate + } + # Method to return a string representation of the book + [string] ToString() { + return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" + } } - -$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. +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. -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. +```powershell +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 + } -### Example simple class with properties and methods + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() -Extending the **Rack** class to add and remove devices -to or from it. + return $true + } + # Ensure a book is valid for the list. + static [void] Validate([book]$Book) { + $Prefix = @( + 'Book validation failed: Book must be defined with the Title,' + 'Author, and PublishDate properties, but' + ) -join ' ' + if ($null -eq $Book) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Book.Title)) { + throw "$Prefix Title wasn't defined" + } + if ([string]::IsNullOrEmpty($Book.Author)) { + throw "$Prefix Author wasn't defined" + } + if ([datetime]::MinValue -eq $Book.PublishDate) { + throw "$Prefix PublishDate wasn't defined" + } + } + # Static methods to manage the list of books. + # Add a book if it's not already in the list. + static [void] Add([Book]$Book) { + [BookList]::Initialize() + [BookList]::Validate($Book) + if ([BookList]::Books.Contains($Book)) { + throw "Book '$Book' already in list" + } -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku + $FindPredicate = { + param([Book]$b) - [string]ToString(){ - return ('{0}|{1}|{2}' -f $this.Brand, $this.Model, $this.VendorSku) - } -} + $b.Title -eq $Book.Title -and + $b.Author -eq $Book.Author -and + $b.PublishDate -eq $Book.PublishDate + }.GetNewClosure() + if ([BookList]::Books.Find($FindPredicate)) { + throw "Book '$Book' already in list" + } -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 + [BookList]::Books.Add($Book) } - - [void]RemoveDevice([int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $null + # Clear the list of books. + static [void] Clear() { + [BookList]::Initialize() + [BookList]::Books.Clear() } - - [int[]] GetAvailableSlots(){ - [int]$i = 0 - return @($this.Devices.foreach{ if($_ -eq $null){$i}; $i++}) + # 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. +```Output +The Fellowship of the Ring -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku +The Hobbit +The Fellowship of the Ring - Device(){ - $this.Brand = 'Undefined' - } +The Fellowship of the Ring - Device( - [string]$b, - [string]$m, - [string]$vsk - ){ - $this.Brand = $b - $this.Model = $m - $this.VendorSku = $vsk - } -} +Titles: -[Device]$someDevice = [Device]::new() -[Device]$server = [Device]::new( - "Fabrikam, Inc.", - "Fbk5040", - "5072641000" -) - -$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 +428,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 - -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 +The `static` keyword only applies to class members, not a class itself. - ## 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. - -PowerShell doesn't support multiple inheritance. Classes can't inherit from -more than one class. However, you can use interfaces for that purpose. - -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 {...} -``` +class. The derived class inherits the properties and methods of the base class. +You can add or override the base class members as required. -This example shows inheritance with an interface declaration coming after the -base class. +PowerShell doesn't support multiple inheritance. Classes can't inherit directly +from more than one 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.2/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md new file mode 100644 index 000000000000..d9c509b15383 --- /dev/null +++ b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md @@ -0,0 +1,540 @@ +--- +description: Describes how to define constructors for PowerShell classes. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_constructors?view=powershell-7.2&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') +[ExampleBook3]::new() +``` + +```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.2/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md new file mode 100644 index 000000000000..91cbbfb2320d --- /dev/null +++ b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md @@ -0,0 +1,1610 @@ +--- +description: Describes how you can define classes that extend other types. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_inheritance?view=powershell-7.2&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() + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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()) + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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] {} + +# Define the types to export with type accelerators. +$ExportableTypes =@( + [InventoryItem] + [Inventory] +) +# Get the internal TypeAccelerators class to use its static methods. +$TypeAcceleratorsClass = [psobject].Assembly.GetType( + 'System.Management.Automation.TypeAccelerators' +) +# Ensure none of the types would clobber an existing type accelerator. +# If a type accelerator with the same name exists, throw an exception. +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +foreach ($Type in $ExportableTypes) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + $Message = @( + "Unable to register type accelerator '$($Type.FullName)'" + 'Accelerator already exists.' + ) -join ' - ' + + throw [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($Message), + 'TypeAcceleratorAlreadyExists', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $Type.FullName + ) + } +} +# Add type accelerators for every exportable type. +foreach ($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Add($Type.FullName, $Type) +} +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() +``` + +> [!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() | Format-List -Property Name, BaseType + +$Inventory.Add([InventoryItem]::new('Bucket', 2)) +$Inventory.Add([InventoryItem]::new('Mop')) +$Inventory.Add([InventoryItem]@{ Name = 'Broom' ; Count = 4 }) +$Inventory +``` + +```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.2/Microsoft.PowerShell.Core/About/about_Classes_Methods.md b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes_Methods.md new file mode 100644 index 000000000000..6858857df272 --- /dev/null +++ b/reference/7.2/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/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_methods?view=powershell-7.2&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.2/Microsoft.PowerShell.Core/About/about_Classes_Properties.md b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes_Properties.md new file mode 100644 index 000000000000..563475b6b13e --- /dev/null +++ b/reference/7.2/Microsoft.PowerShell.Core/About/about_Classes_Properties.md @@ -0,0 +1,959 @@ +--- +description: Describes how to define properties for PowerShell classes. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_properties?view=powershell-7.2&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 = @( + @{ + MemberName = '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() +``` + +```Output +Name Description Guid +---- ----------- ---- + aa96350c-358d-465c-96d1-a49949219eec +``` + +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 diff --git a/reference/7.3/Microsoft.PowerShell.Core/About/About.md b/reference/7.3/Microsoft.PowerShell.Core/About/About.md index f6970fb9ac22..0e865533f388 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/10/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..cb445ae578a7 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/10/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,326 @@ 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.PageCount -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.PageCount * 2 + return [timespan]::new(0, $Minutes, 0) + } + # Method to calculate how long ago a book was published + [timespan] GetPublishedAge() { + if ( + $null -eq $this.PublishDate -or + $this.PublishDate -eq [datetime]::MinValue + ) { throw 'PublishDate not defined' } + + return (Get-Date) - $this.PublishDate + } + # Method to return a string representation of the book + [string] ToString() { + return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" + } } - -$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. +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. -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. +```powershell +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 + } -### Example simple class with properties and methods + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() -Extending the **Rack** class to add and remove devices -to or from it. + return $true + } + # Ensure a book is valid for the list. + static [void] Validate([book]$Book) { + $Prefix = @( + 'Book validation failed: Book must be defined with the Title,' + 'Author, and PublishDate properties, but' + ) -join ' ' + if ($null -eq $Book) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Book.Title)) { + throw "$Prefix Title wasn't defined" + } + if ([string]::IsNullOrEmpty($Book.Author)) { + throw "$Prefix Author wasn't defined" + } + if ([datetime]::MinValue -eq $Book.PublishDate) { + throw "$Prefix PublishDate wasn't defined" + } + } + # Static methods to manage the list of books. + # Add a book if it's not already in the list. + static [void] Add([Book]$Book) { + [BookList]::Initialize() + [BookList]::Validate($Book) + if ([BookList]::Books.Contains($Book)) { + throw "Book '$Book' already in list" + } -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku + $FindPredicate = { + param([Book]$b) - [string]ToString(){ - return ('{0}|{1}|{2}' -f $this.Brand, $this.Model, $this.VendorSku) - } -} + $b.Title -eq $Book.Title -and + $b.Author -eq $Book.Author -and + $b.PublishDate -eq $Book.PublishDate + }.GetNewClosure() + if ([BookList]::Books.Find($FindPredicate)) { + throw "Book '$Book' already in list" + } -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 + [BookList]::Books.Add($Book) } - - [void]RemoveDevice([int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $null + # Clear the list of books. + static [void] Clear() { + [BookList]::Initialize() + [BookList]::Books.Clear() } - - [int[]] GetAvailableSlots(){ - [int]$i = 0 - return @($this.Devices.foreach{ if($_ -eq $null){$i}; $i++}) + # 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. +```Output +The Fellowship of the Ring -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku +The Hobbit +The Fellowship of the Ring - Device(){ - $this.Brand = 'Undefined' - } +The Fellowship of the Ring - Device( - [string]$b, - [string]$m, - [string]$vsk - ){ - $this.Brand = $b - $this.Model = $m - $this.VendorSku = $vsk - } -} +Titles: -[Device]$someDevice = [Device]::new() -[Device]$server = [Device]::new( - "Fabrikam, Inc.", - "Fbk5040", - "5072641000" -) - -$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 +428,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 - -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 +The `static` keyword only applies to class members, not a class itself. - ## 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. - -PowerShell doesn't support multiple inheritance. Classes can't inherit from -more than one class. However, you can use interfaces for that purpose. - -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 {...} -``` +class. The derived class inherits the properties and methods of the base class. +You can add or override the base class members as required. -This example shows inheritance with an interface declaration coming after the -base class. +PowerShell doesn't support multiple inheritance. Classes can't inherit directly +from more than one 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..e26272375dde --- /dev/null +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md @@ -0,0 +1,540 @@ +--- +description: Describes how to define constructors for PowerShell classes. +Locale: en-US +ms.date: 11/10/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') +[ExampleBook3]::new() +``` + +```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..e7d9f2b9995c --- /dev/null +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md @@ -0,0 +1,1610 @@ +--- +description: Describes how you can define classes that extend other types. +Locale: en-US +ms.date: 11/10/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() + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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()) + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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] {} + +# Define the types to export with type accelerators. +$ExportableTypes =@( + [InventoryItem] + [Inventory] +) +# Get the internal TypeAccelerators class to use its static methods. +$TypeAcceleratorsClass = [psobject].Assembly.GetType( + 'System.Management.Automation.TypeAccelerators' +) +# Ensure none of the types would clobber an existing type accelerator. +# If a type accelerator with the same name exists, throw an exception. +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +foreach ($Type in $ExportableTypes) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + $Message = @( + "Unable to register type accelerator '$($Type.FullName)'" + 'Accelerator already exists.' + ) -join ' - ' + + throw [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($Message), + 'TypeAcceleratorAlreadyExists', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $Type.FullName + ) + } +} +# Add type accelerators for every exportable type. +foreach ($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Add($Type.FullName, $Type) +} +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() +``` + +> [!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() | Format-List -Property Name, BaseType + +$Inventory.Add([InventoryItem]::new('Bucket', 2)) +$Inventory.Add([InventoryItem]::new('Mop')) +$Inventory.Add([InventoryItem]@{ Name = 'Broom' ; Count = 4 }) +$Inventory +``` + +```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..eb4bd80c02bc --- /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/10/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..07b22d6cb6a9 --- /dev/null +++ b/reference/7.3/Microsoft.PowerShell.Core/About/about_Classes_Properties.md @@ -0,0 +1,959 @@ +--- +description: Describes how to define properties for PowerShell classes. +Locale: en-US +ms.date: 11/10/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 = @( + @{ + MemberName = '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() +``` + +```Output +Name Description Guid +---- ----------- ---- + aa96350c-358d-465c-96d1-a49949219eec +``` + +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 diff --git a/reference/7.4/Microsoft.PowerShell.Core/About/About.md b/reference/7.4/Microsoft.PowerShell.Core/About/About.md index f6970fb9ac22..0e865533f388 100644 --- a/reference/7.4/Microsoft.PowerShell.Core/About/About.md +++ b/reference/7.4/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/10/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.4/Microsoft.PowerShell.Core/About/about_Classes.md b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes.md index 150151315e0d..0525657b26e5 100644 --- a/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes.md +++ b/reference/7.4/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/10/2023 online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.4&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. + +## Examples -### Example syntax and usage +### Example 1 - Minimal definition This example shows the minimum syntax needed to create a usable class. @@ -82,375 +98,326 @@ 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.PageCount -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.PageCount * 2 + return [timespan]::new(0, $Minutes, 0) + } + # Method to calculate how long ago a book was published + [timespan] GetPublishedAge() { + if ( + $null -eq $this.PublishDate -or + $this.PublishDate -eq [datetime]::MinValue + ) { throw 'PublishDate not defined' } + + return (Get-Date) - $this.PublishDate + } + # Method to return a string representation of the book + [string] ToString() { + return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" + } } - -$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 -} +$Book = [Book]::new(@{ + Title = 'The Hobbit' + Author = 'J.R.R. Tolkien' + Publisher = 'George Allen & Unwin' + PublishDate = '1937-09-21' + PageCount = 310 + Tags = @('Fantasy', 'Adventure') +}) -class Rack { - [string]$Brand - [string]$Model - [string]$VendorSku - [string]$AssetId - [Device[]]$Devices = [Device[]]::new(8) +$Book +$Time = $Book.GetReadingTime() +$Time = @($Time.Hours, 'hours and', $Time.Minutes, 'minutes') -join ' ' +$Age = [Math]::Floor($Book.GetPublishedAge().TotalDays / 365.25) -} - -$rack = [Rack]::new() - -$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 + +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. -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. +```powershell +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 + } -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. + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() -### Example simple class with properties and methods + return $true + } + # Ensure a book is valid for the list. + static [void] Validate([book]$Book) { + $Prefix = @( + 'Book validation failed: Book must be defined with the Title,' + 'Author, and PublishDate properties, but' + ) -join ' ' + if ($null -eq $Book) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Book.Title)) { + throw "$Prefix Title wasn't defined" + } + if ([string]::IsNullOrEmpty($Book.Author)) { + throw "$Prefix Author wasn't defined" + } + if ([datetime]::MinValue -eq $Book.PublishDate) { + throw "$Prefix PublishDate wasn't defined" + } + } + # Static methods to manage the list of books. + # Add a book if it's not already in the list. + static [void] Add([Book]$Book) { + [BookList]::Initialize() + [BookList]::Validate($Book) + if ([BookList]::Books.Contains($Book)) { + throw "Book '$Book' already in list" + } -Extending the **Rack** class to add and remove devices -to or from it. + $FindPredicate = { + param([Book]$b) -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku + $b.Title -eq $Book.Title -and + $b.Author -eq $Book.Author -and + $b.PublishDate -eq $Book.PublishDate + }.GetNewClosure() + if ([BookList]::Books.Find($FindPredicate)) { + throw "Book '$Book' already in list" + } - [string]ToString(){ - return ('{0}|{1}|{2}' -f $this.Brand, $this.Model, $this.VendorSku) + [BookList]::Books.Add($Book) } -} - -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 + # Clear the list of books. + static [void] Clear() { + [BookList]::Initialize() + [BookList]::Books.Clear() } - - [void]RemoveDevice([int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $null + # Find a specific book using a filtering scriptblock. + static [Book] Find([scriptblock]$Predicate) { + [BookList]::Initialize() + return [BookList]::Books.Find($Predicate) } - - [int[]] GetAvailableSlots(){ - [int]$i = 0 - return @($this.Devices.foreach{ if($_ -eq $null){$i}; $i++}) + # 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}) - } +$null -eq [BookList]::Books - [string]SayHello(){ - # this following line doesn't go to the pipeline - "Good Morning" +[BookList]::Add($Book) - # this line goes to the pipeline - return "Hello World" - } -} - -$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. +```Output +The Fellowship of the Ring -The default constructor sets the **brand** to **Undefined**, and leaves -**model** and **vendor-sku** with null values. +The Hobbit +The Fellowship of the Ring -```powershell -class Device { - [string]$Brand - [string]$Model - [string]$VendorSku +The Fellowship of the Ring - Device(){ - $this.Brand = 'Undefined' - } +Titles: - Device( - [string]$b, - [string]$m, - [string]$vsk - ){ - $this.Brand = $b - $this.Model = $m - $this.VendorSku = $vsk - } -} - -[Device]$someDevice = [Device]::new() -[Device]$server = [Device]::new( - "Fabrikam, Inc.", - "Fbk5040", - "5072641000" -) - -$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,513 +428,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 - -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. +The `static` keyword only applies to class members, not a class itself. -## 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. +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. -This example shows the basic PowerShell class inheritance syntax. +For more information about deriving classes that inherit from a base class or +implement interfaces, see +[about_Classes_Inheritance][11]. -```powershell -Class Derived : Base {...} -``` +## Exporting classes with type accelerators -This example shows inheritance with an interface declaration coming after the -base class. +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 -Class Derived : Base, Interface {...} -``` +However, if a module adds type accelerators, those type accelerators are +immediately available in the session after users import the module. -### Example of inheritance in PowerShell classes - -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. - -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]. - -```powershell -enum DeviceType { - Undefined = 0 - Compute = 1 - Storage = 2 - Networking = 4 - Communications = 8 - Power = 16 - Rack = 32 -} -``` - -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) } - -class Rack : Device { - hidden [DeviceType]$devtype = [DeviceType]::Rack - hidden [int]$Slots = 8 - - [string]$Datacenter - [string]$Location - [Device[]]$Devices = [Device[]]::new($this.Slots) - - Rack (){ - ## Just create the default rack with 8 slots - } - - Rack ([int]$s){ - ## Add argument validation logic here - $this.Devices = [Device[]]::new($s) - } - - [void] AddDevice([Device]$dev, [int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $dev +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Remove($Type.FullName) } - - [void] RemoveDevice([int]$slot){ - ## Add argument validation logic here - $this.Devices[$slot] = $null - } -} - -$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 +}.GetNewClosure() ``` -```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 -``` - -### Calling base class constructors - -To invoke a base class constructor from a subclass, add the `base` keyword. - -```powershell -class Person { - [int]$Age - - Person([int]$a) - { - $this.Age = $a - } -} +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. -class Child : Person -{ - [string]$School +## Manually importing classes from a PowerShell module - Child([int]$a, [string]$s ) : base($a) { - $this.School = $s - } -} - -[Child]$littleOne = [Child]::new(10, "Silver Fir Elementary School") - -$littleOne.Age -``` +`Import-Module` and the `#requires` statement only import the module functions, +aliases, and variables, as defined by the module. Classes aren't imported. -```Output +If a module defines classes and enumerations but doesn't add type accelerators +for those types, use a `using module` statement to import them. -10 -``` +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. -### Invoke base class methods +For more information about the `using` statement, see [about_Using][12]. -To override existing methods in subclasses, declare methods using the same name -and signature. +## Loading newly changed code during development -```powershell -class BaseClass -{ - [int]days() {return 1} -} -class ChildClass1 : BaseClass -{ - [int]days () {return 2} -} +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. -[ChildClass1]::new().days() -``` +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. -```Output +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. -2 -``` +## The PSReference type isn't supported with class members -To call base class methods from overridden implementations, cast to the base -class (`[baseclass]$this`) on invocation. +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. -```powershell -class BaseClass -{ - [int]days() {return 1} -} -class ChildClass1 : BaseClass -{ - [int]days () {return 2} - [int]basedays() {return ([BaseClass]$this).days()} -} +For more information, see [PSReference Class][13]. -[ChildClass1]::new().days() -[ChildClass1]::new().basedays() -``` +## Limitations -```Output +The following lists include limitations for defining PowerShell classes and +workaround for those limitations, if any. -2 -1 -``` +### General limitations -### Inheriting from interfaces +- Class members can't use **PSReference** as their type. -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: None. +- PowerShell classes can't be unloaded or reloaded in a session. -> [!NOTE] -> PowerShell doesn't support declaring new interfaces in PowerShell script. + Workaround: Start a new session. +- PowerShell classes defined in a module aren't automatically imported. -```powershell -class MyComparable : System.IComparable -{ - [int] CompareTo([object] $obj) - { - return 0; - } -} + 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. -class MyComparableBar : bar, System.IComparable -{ - [int] CompareTo([object] $obj) - { - return 0; - } -} -``` + Workaround: None. -## NoRunspaceAffinity attribute +### Constructor limitations -A runspace is the operating environment for the commands invoked by PowerShell. -This environment includes the commands and data that are currently present, and -any language restrictions that currently apply. +- Constructor chaining isn't implemented. -By default, a PowerShell class is affiliated with the **Runspace** where it's -created. Using a PowerShell class in `ForEach-Object -Parallel` is not safe. -Method invocations on the class are marshalled back to the **Runspace** where -it was created, which can corrupt the state of the **Runspace** or cause a -deadlock. + Workaround: Define hidden `Init()` methods and call them from within the + constructors. +- Constructor parameters can't use any attributes, including validation + attributes. -Adding the `NoRunspaceAffinity` attribute to the class definition ensures that -the PowerShell class is not affiliated with a particular runspace. Method -invocations, both instance and static, use the **Runspace** of the running -thread and the thread's current session state. + Workaround: Reassign the parameters in the constructor body with the + validation attribute. +- Constructor parameters can't define default values. The parameters are + always mandatory. -The attribute was added in PowerShell 7.4. + Workaround: None. +- If any overload of a constructor is hidden, every overload for the + constructor is treated as hidden too. -### Example - Class definition with Runspace affinity + Workaround: None. -The `ShowRunspaceId()` method of `[UnsafeClass]` reports different thread Ids -but the same runspace Id. Eventually, the session state is corrupted causing -an error, such as `Global scope cannot be removed`. +### Method limitations -```powershell -# Class definition with Runspace affinity (default behavior) -class UnsafeClass { - static [object] ShowRunspaceId($val) { - return [PSCustomObject]@{ - ThreadId = [Threading.Thread]::CurrentThread.ManagedThreadId - RunspaceId = [runspace]::DefaultRunspace.Id - } - } -} -$unsafe = [UnsafeClass]::new() -while ($true) { - 1..10 | ForEach-Object -Parallel { - Start-Sleep -ms 100 - ($using:unsafe)::ShowRunspaceId($_) - } -} -``` +- Method parameters can't use any attributes, including validation + attributes. -> [!NOTE] -> This example runs in an infinite loop. Enter Ctrl+C to -> stop the execution. + 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. -### Example - Class definition with NoRunspaceAffinity + 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. -The `ShowRunspaceId()` method of `[SafeClass]` reports different thread and -Runspace ids. + Workaround: None. +- If any overload of a method is hidden, every overload for that method is + treated as hidden too. -```powershell -# Class definition with NoRunspaceAffinity attribute -[NoRunspaceAffinity()] -class SafeClass { - static [object] ShowRunspaceId($val) { - return [PSCustomObject]@{ - ThreadId = [Threading.Thread]::CurrentThread.ManagedThreadId - RunspaceId = [runspace]::DefaultRunspace.Id - } - } -} -$safe = [SafeClass]::new() -while ($true) { - 1..10 | ForEach-Object -Parallel { - Start-Sleep -ms 100 - ($using:safe)::ShowRunspaceId($_) - } -} -``` + Workaround: None. -> [!NOTE] -> This example runs in an infinite loop. Enter Ctrl+C to -> stop the execution. +### 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.4/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md new file mode 100644 index 000000000000..8095ee20bf2b --- /dev/null +++ b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes_Constructors.md @@ -0,0 +1,540 @@ +--- +description: Describes how to define constructors for PowerShell classes. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_constructors?view=powershell-7.4&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') +[ExampleBook3]::new() +``` + +```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.4/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md new file mode 100644 index 000000000000..1b0a27aad896 --- /dev/null +++ b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes_Inheritance.md @@ -0,0 +1,1610 @@ +--- +description: Describes how you can define classes that extend other types. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_inheritance?view=powershell-7.4&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() + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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()) + } +} + +enum TemperatureScale { + Celsius = 0 + Fahrenheit = 1 + Kelvin = 2 +} +``` + +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] {} + +# Define the types to export with type accelerators. +$ExportableTypes =@( + [InventoryItem] + [Inventory] +) +# Get the internal TypeAccelerators class to use its static methods. +$TypeAcceleratorsClass = [psobject].Assembly.GetType( + 'System.Management.Automation.TypeAccelerators' +) +# Ensure none of the types would clobber an existing type accelerator. +# If a type accelerator with the same name exists, throw an exception. +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +foreach ($Type in $ExportableTypes) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + $Message = @( + "Unable to register type accelerator '$($Type.FullName)'" + 'Accelerator already exists.' + ) -join ' - ' + + throw [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($Message), + 'TypeAcceleratorAlreadyExists', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $Type.FullName + ) + } +} +# Add type accelerators for every exportable type. +foreach ($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Add($Type.FullName, $Type) +} +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach($Type in $ExportableTypes) { + $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() +``` + +> [!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() | Format-List -Property Name, BaseType + +$Inventory.Add([InventoryItem]::new('Bucket', 2)) +$Inventory.Add([InventoryItem]::new('Mop')) +$Inventory.Add([InventoryItem]@{ Name = 'Broom' ; Count = 4 }) +$Inventory +``` + +```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.4/Microsoft.PowerShell.Core/About/about_Classes_Methods.md b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes_Methods.md new file mode 100644 index 000000000000..d82a303b8128 --- /dev/null +++ b/reference/7.4/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/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_methods?view=powershell-7.4&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.4/Microsoft.PowerShell.Core/About/about_Classes_Properties.md b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes_Properties.md new file mode 100644 index 000000000000..ea32e6078c8c --- /dev/null +++ b/reference/7.4/Microsoft.PowerShell.Core/About/about_Classes_Properties.md @@ -0,0 +1,959 @@ +--- +description: Describes how to define properties for PowerShell classes. +Locale: en-US +ms.date: 11/10/2023 +online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes_properties?view=powershell-7.4&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 = @( + @{ + MemberName = '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() +``` + +```Output +Name Description Guid +---- ----------- ---- + aa96350c-358d-465c-96d1-a49949219eec +``` + +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