PowerShell Classes: Is it possible to make a constructors parameter optional?
Asked Answered
R

4

5

I am trying to build a "type", for the Timecode convention used in Film/Animation.

I want to accommodate use cases where a user may want to initialise the object with a Timecode string gotten from elsewhere:

$MyTimeCode = [TimeCode]::new("08:06:04.10", 12)

and in other cases, directly with parameters belonging to the classes constructor:

$MyTimeCode = [TimeCode]::new(, 8, 6, 4, 10, 12)

#If I had constructor such as the foloowing in my class definition:
#TimeCode([String]$TimeStamp, [Int]$Hours, [Int]$Minutes, [Int]$Seconds, [Int]$Frames, [Int]$FrameRate)
`

The following Class as an example:

class HelloWorld {
    [String]$Hello
    [String]$World

    HelloWorld([String]$Hello, [String]$World) {
        $This.Hello = $Hello
        $This.World = $World
        }
    }

Attempting to initialise it [HelloWorld]::new("Hello", "World") works fine but with [HelloWorld]::new("Hello") I get an error:

MethodException: Cannot find an overload for "new" and the argument count: "1".

I have been watching some lectures here and there but they are not structured at all and they quickly advance.

The official docs are usually my go to but the "About_Classes" page quickly advances to "inheriting" and "subclasses" etc with the "Rack" and "Devices" examples and its not clear to me how one begins to work with just a single class.

My goal is to familiarise myself with classes and what they can do. The Timecode is secondary.

Recusancy answered 5/9, 2023 at 19:30 Comment(0)
C
8

A constructor that takes a single argument is needed. There can be as many constructors as needed as long as the signature of the parameters is unique for each. This code also creates a default constructor. The members of the default constructor will contain $null when instantiated.

class HelloWorld {
    [String]$Hello
    [String]$World

    HelloWorld([String]$Hello, [String]$World) {
        $This.Hello = $Hello
        $This.World = $World
    }
    HelloWorld([String]$Hello) {
        $This.Hello = $Hello
        $This.World = 'default'
    }
    HelloWorld() {}     # default constructor
}

$x1 = [HelloWorld]::new('hello', 'world')
$x1
$x2 = [HelloWorld]::new('hello')
$x2
$x3 = [HelloWorld]::new()
$x3
Cynosure answered 5/9, 2023 at 19:38 Comment(3)
Very nice. You should also include a default parameterless constructor when defining custom constructors. Without it you won't see New() in intellisense. imgur.com/vxvYheM vs imgur.com/UUBvGhCKutzer
ok intellisense was a bad argument for providing a default constructor. A better argument is when using custom constructors, the default constructor disappears. This means you can't instantiate an instance without parameters. This may be confusing for users, so if you would make a note about this. 4sysops.com/archives/powershell-classes-part-4-constructors mentions this possibly confusing fact.Kutzer
I think it's better than omitting. I appreciate you.Kutzer
W
5

To add some background information to lit's effective solution:

What you're looking for requires one of the following features, neither of which are available in PowerShell, neither in Windows PowerShell (which is no longer active developed), nor in the actively developed PowerShell (Core) (as of v7.3.x, the stable version as of this writing):

  • Optional method/constructor parameters, ideally in combination with named parameters.

    • Optional parameters aren't supported, even though PowerShell lets you define them:

       # !! Does NOT work as of PowerShell 7.3.x
       # !! The default value is QUIETLY ACCEPTED, but IGNORED. 
       class Foo { [void] Bar($baz, $quux = 42) {} }
      
       # !! FAILS, because you must supply arguments for BOTH parameters.
       # !! -> 'Cannot find an overload for "Bar" and the argument count: "1"'
       [Foo]::new().Bar('hi')
      
      • GitHub issue #9701 is a feature request that asks that optional parameters be implemented.
    • Named parameters are fundamentally unsupported.

  • Constructor chaining, i.e. to have multiple constructor overloads that can call each other with default values, which PowerShell doesn't support as of v7.3.x:

       class Foo { 
         # Two-parameter constructor
         Foo($bar, $baz) { <# ... #> } 
         # Single-parameter constructor
         Foo($bar) { 
           # !! Constructor chaining Does NOT work as of PowerShell 7.3.x:
           # !! That is, the attempt to call the two-parameter constructor
           # !! (with a default value for the second parameter) causes a SYNTAX ERROR.
           $this($bar, 'default for $baz') 
         } 
       }
    
    • For a workaround via hidden helper methods, see this answer.

      • In simple cases, if code duplication isn't a concern, you can simply create separate, independent constructor overloads, as shown in lit's answer.
    • GitHub issue #19969 is a feature request asking that constructor chaining be implemented (among other features).

Wort answered 5/9, 2023 at 21:0 Comment(0)
D
2

This is something a C# class would allow you to do, i.e.:

public class HelloWorld
{
    public string? Hello { get; set; }
    public string? World { get; set; }

    public HelloWorld(string? hello = null, string? world = null)
    {
        Hello = hello;
        World = world;
    }
}

Would allow the 3 possibilities (no arguments, 1 argument or 2 arguments) for instantiation in a single constructor. PowerShell classes are far from there... pretty disappointing. On the bright side, Add-Type can be used for inline C#.

Other alternatives to add to the other answers:

class HelloWorld {
    [string] $Hello
    [string] $World
}

[HelloWorld]@{ Hello = 'foo' }
[HelloWorld]@{ World = 'bar' }
class HelloWorld {
    [string] $Hello
    [string] $World

    HelloWorld([string] $Hello, [string] $World) {
        $this.Hello = $Hello
        $this.World = $World
    }

    # targets a `$hello` only explicit casting
    static hidden [HelloWorld] op_Explicit([string] $Hello) {
        return [HelloWorld]::new($Hello, 'myDefaultValue')
    }
}

[HelloWorld] 'foo'

Following the C# example at the beginning, this is also shown in Mathias's helpful answer.

NOTE: PowerShell 7+ is needed to compile, this will fail in Windows PowerShell 5.1.

Add-Type '
public class HelloWorld
{
    public string? Hello { get; set; }
    public string? World { get; set; }

    public HelloWorld() : this("hey", "there")
    { }

    public HelloWorld(string? hello = "hey", string? world = "there")
    {
        Hello = hello;
        World = world;
    }
}' -IgnoreWarnings -WarningAction Ignore

[HelloWorld]::new()

# Hello World
# ----- -----
# hey   there

[HelloWorld]::new('hi')

# Hello World
# ----- -----
# hi    there

[HelloWorld]@{ world = 'you' }

# Hello World
# ----- -----
# hey   you
Deal answered 5/9, 2023 at 22:40 Comment(0)
B
1

If I understood you correctly, you need to be able to pass optional parameters to your class, if this is correct, take a look at the code below.

The idea is to use only one parameter of type [HashTable] for the class, this way you avoid the need to create a separate constructor for each possible combinations of parameters you need to pass to your class. You basically just need 1 default constructor which has no parameters, 1 constructor that takes 1 parameter of type [HashTable], and 1 function that will be called from both the constructors just mentioned, in my example, I called the function Initialize but you can give it any name you like.

You can copy the code to a file and run it as is or paste it in VS Code or PowerShell ISE. I tested it with PowerShell 5.1 and 7.4.2.

Class HelloWorld
{
 [String] $Para1 = 'Hello' # This is the default.
 [String] $Para2 = 'world' # This is the default.

 # Default Constructor.
 HelloWorld() {$This.Initialize(@{})}

 # Constructor with one parameter of type hashtable.
 HelloWorld([HashTable] $Parameters) {$This.Initialize($Parameters)}

 # This is actually the code that runs when you create a new instance of this class.
 Hidden [Void] Initialize([HashTable] $Parameters)
 {
  ForEach ($Parameter In $Parameters.Keys)
  {
   Switch ($Parameter)
   {
    'Para1' {$This.$Parameter = $Parameters.$Parameter; Break} # You can also add code here between the {} to validate the parameter value for example.
    'Para2' {$This.$Parameter = $Parameters.$Parameter; Break} # You can also add code here between the {} to validate the parameter value for example.
    Default {} # Ignores invalid parameters or you can choose to handle them here between the {}.
   }
  }
 }

 [Void] Greetings() {Write-Host $This.Para1 $This.Para2 -ForeGroundColor Yellow}

}

cls
[HelloWorld]::New()
[HelloWorld]::New(@{Para2='MyName'})
[HelloWorld]::New(@{Para2='MyName'; Para1='Hi'})
[HelloWorld]::New(@{Para1='Hi'})
[HelloWorld]::New(@{Para3='IgnoreMe'})
([HelloWorld]::New(@{Para2='MyName'})).Greetings()

"Another way to create a new instance of your class."
New-Object HelloWorld
New-Object HelloWorld @{Para2='MyName'}
New-Object HelloWorld @{Para2='MyName'; Para1='Hi'}
New-Object HelloWorld @{Para1='Hi'}
New-Object HelloWorld @{Para3='IgnoreMe'}
(New-Object HelloWorld).Greetings()

# Or you can assign any of the above statments to a variable, for example:
$Test = New-Object HelloWorld
$Test.Greetings()

Update:

The above class works just fine if the order of the parameters do not matter, but I came across a situation where the order mattered. When populating the [HashTable] with parameters, it does not retain the order of the keys as you add them. To work around this issue, use the class below, it is a bit more work but gets the job done.

See the following 2 links for additional information:

powershell - Hashtables and key order - Stack Overflow

powershell - Passing an ordered hashtable to a function - Stack Overflow

Class HelloWorld
{
 [String] $Para1 = 'Hello' # This is the default value.
 [String] $Para2 = 'world' # This is the default value.

 # Default Constructor.
 HelloWorld() {$This.Initialize(@{})}

 # Constructor with one parameter of type hashtable.
 HelloWorld([HashTable] $Parameters) {$This.Initialize($Parameters)}

 # This is actually the code that runs when you create a new instance of this class.
 Hidden [Void] Initialize([HashTable] $Parameters)
 {
  $OrderedParameters = @{}
  ForEach ($Parameter In $Parameters.Keys)
  {
   Switch ($Parameter)
   {
    'Para1'  {$OrderedParameters[0] = @($Parameter, $Parameters.$Parameter); Break}
    'Para2'  {$OrderedParameters[1] = @($Parameter, $Parameters.$Parameter); Break}
     Default {} # Ignores invalid parameters or you can choose to handle them here between the {}.
    }
   }

  ForEach($Parameter In $OrderedParameters.GetEnumerator() | Sort-Object Key)
  {
   Switch ($Parameter.Value[0])
   {
    'Para1' {$This.Para1 = $Parameter.Value[1]; Break} # You can also add code here between the {} to validate the parameter value for example.
    'Para2' {$This.Para2 = $Parameter.Value[1]; Break} # You can also add code here between the {} to validate the parameter value for example.
   }
  }
 }

 [Void] Greetings() {Write-Host $This.Para1 $This.Para2 -ForeGroundColor Yellow}

}
Beaufort answered 29/7, 2024 at 9:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.