Powershell: [AllowNull()] should be usable with value-type parameters too

Created on 17 Oct 2019  路  8Comments  路  Source: PowerShell/PowerShell

When you declare a non-mandatory parameter and do not pass an argument on invocation, the parameter variable is _implicitly_ $null - even if its declared type is a (non-numeric) _value type_ (instances of which cannot contain $null).

PS> & { param([datetime] $dt) $null -eq $dt } # no value is passed to -dt

True  # $dt was $null, even though a [datetime] instance cannot be $null

Note: By contrast, _numeric_ value types default to 0, i.e. the type's default value. This inconsistency is problematic in itself, but it's presumably too late to change that.

However, if you want to allow _explicit_ passing of $null, the same approach does _not_ work:

PS> & { param([AllowNull()] [datetime] $dt) $null -eq $dt } -dt $null # !! FAILS

Cannot process argument transformation on parameter 'dt'. Cannot convert null to type "System.DateTime".

Similarly, _numeric_ value-type parameters effectively coerce the $null to the type's default value, 0, meaning that $null as a "signal value" is lost:

PS> & { param([AllowNull()] [int] $i) $null -eq $i } -i $null

False # !! $false, because $i becomes 0

The - cumbersome - workaround is to use [Nullable[datetime]] as the parameter type instead - and note that if the parameter is _mandatory_, you _also_ need AllowNull().

Explicit use of nullable types in the context of parameter declarations shouldn't be necessary in PowerShell; I suspect that many PowerShell users may not even be aware of the distinction between reference types and value types.

As for a real-world use case: It's conceivable to have a _mandatory_ parameter - one you want to force the users to pass a value _intentionally_ to - that still supports $null - even with value types - as an explicit signal that a default value should apply.

Steps to reproduce

{ & { param([AllowNull()] [datetime] $dt) $null -eq $dt } -dt $null } | Should -Not -Throw

& { param([AllowNull()] [int] $i) $null -eq $i } -i $null | Should -Be $true

Expected behavior

The tests should pass.

Actual behavior

The 1st test fails:

Expected no exception to be thrown, but an exception "Cannot process argument transformation on parameter 'dt'. Cannot convert null to type "System.DateTime"." was thrown ...

The 2nd test fails:

Expected $true, but got $false.

That is, $i became 0 rather than being retained as $null.

Environment data

PowerShell Core 7.0.0-preview.4
Issue-Question WG-Engine

All 8 comments

So, to summarize in brief -- you're proposing that [AllowNull()][int] automatically implies we should transform that parameter into [Nullable[int]]?

I mean, that seems fairly sensible. 馃憤

@vexx32:

_Conceptually_ that is a good summary.

However, I don't think that it is _technically_ necessary:

What seems to be happening in the _implicit_ case is that the parameter variable is simply not _initialized_, which is what makes its value $null by default.

However, the type constraint _is_ in effect, and _is_ enforced if you later try to assign to that variable (although you typically wouldn't do that with a _parameter_ variable):

PS> & { param([datetime] $dt) $dt = $null } # FAILS, because $dt, despite being initially $null, IS type-constrained

Cannot convert null to type "System.DateTime".

While I'm not familiar with the plumbing, my guess is that we can apply this non-initialization technique in the explicit case too - without needing to involve Nullable<T>.

@vexx32: I've since realized that the non-initialization to $null only applies to _non-numeric_ value types. With _numeric_ ones you get 0, which presents a different problem with [AllowNull()]: passing $null quietly converts to 0 - I've updated the OP accordingly.

Hmm, interesting. Given that [nullable[T]] doesn't let Mandatory parameters accept null values, and with the odd behaviour with numeric inputs there...

I'm not really sure what the optimal solution is there. I think I'm still partial to having [AllowNull()] make it a [nullable[T]] implicitly, I think, as that would probably be a fairly minimal and least-likely-to-break-something change, and neatly skirts the slightly awkward syntax for [nullable[int]]$Count etc.

But yes, it's not necessarily the idea solution. I'm also not sure simply "not initializing" the value is something we can do without taking some extra care, because I wouldn't be surprised if trying to do that excludes it from $PSBoundParameters; and for those parameters where you want to be able to detect the difference between "not specified" and "null", that is a fairly critical piece of plumbing.

@vexx32:

First, as an aside: that non-bound value-type parameters behave differently depending on whether they're numeric (default to 0) or not (default to $null) is problematic in itself, but it's probably too late to fix that inconsistency.

[nullable[T]] doesn't let Mandatory parameters accept null values

Maybe I'm misreading your comment, but [Nullable[T]] _does_ work as a workaround even with numeric value types:

PS> & { param([Parameter(Mandatory)] [AllowNull()] [nullable[int]] $i) $null -eq $i } -i $null

True

I'm also not sure simply "not initializing" the value is something we can do without taking some extra care

Fair point - we certainly want $PSBoundParameters to continue to tell the truth.

I don't know enough about PowerShell's variable plumbing to be of help there, but I do see that trying to initialize a [datetime] variable with $null does _not_ work:

# Borrow a [datetime] argument-transformation attribute from a regular variable.
[datetime] $dt = 0; $ta = (Get-Variable dt).Attributes[0]

# !! FAILS
[psvariable]::new(
  'foo', 
  $null,  # initial value
 'None', 
 [System.Collections.ObjectModel.Collection[System.Attribute]] $ta
) 

That said, the .Value property of System.Management.Automation.PSVariable instances is object-typed, so I think it's at least _possible_ to initialize to $null.

But perhaps [Nullable[T]] is indeed the answer. If it is, we should make sure it doesn't peek from behind the curtain or at least doesn't cause obscure variations in behavior.

@mklement0 I mean that [nullable[T]] _without_ using [AllowNull()] doesn't work. But yes, that is a currently available workaround, if one that feels very redundant. 馃檪

Your example with datetime there makes me wonder if we may simply are missing some checks for the [allownull()] attribute when setting variables, though, if we can't even initialize them like that. Note that this also fails:

[AllowNull()][datetime]$value = $null

Potentially we need to simply add a check for whether the variable has the [AllowNull()] attribute before we try to create an instance of [datetime] (or anything else) with a null value...

Yes, I've noticed that [AllowNull()][datetime]$value = $null fails, and so does later assigning $null to a parameter variable declared this way.

So it sounds like in a manner of speaking we could roll our own nullable types, without actually needing to use [Nullable[T]], correct?

That is, if the [AllowNull()] attribute is set on a value-type-constrained variable, allow assigning $null as-is (make .Value return null) rather than attempting to convert $null to the type.

Aye, I think either we need to do that, or if that is unfeasible or ends up being more misleading, we may want to make [AllowNull()] imply the use of [nullable[T]] instead.

Was this page helpful?
0 / 5 - 0 ratings