I'm trying to understand null variable assignment in PowerShell, please see the below code example:
[Bool]$result = $null
Cannot convert value "" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.
This makes sense. Now when using a function:
function Test-Function {
[CmdletBinding()]
Param (
)
[Bool]$result = $null
Write-Output -InputObject "Value of 'result' is: $result"
Write-Output -InputObject "Type is: $($result.GetType())"
}
When running the above function with Test-Function -ErrorAction Stop
the below output is returned:
Value of 'result' is: False
Type is: bool
Why is the behaviour different when assigning $null
to $result
inside the function?
Using PowerShell 5.1 on Windows 11
The issue isn't easy to explain and requires diving deep into PowerShell source code. We can assert this issue only happens on strongly typed conversions a.k.a. variable constraints instead of casting ([bool] $var = $null
vs $var = [bool] $null
).
The simple answer is that there is a clear distinction between how PowerShell invokes a script block when optimized vs unoptimized and this isn't documented or it isn't well documented at least. This is proven by the following test:
$expression = { [bool] $var = $null }
& $expression # Runs optimized, succeeds conversion
. $expression # Runs unoptimized, fails conversion
When the conversion fails, if we inspect $Error[0].Exception.StackTrace
, in the first line we can see that the code path taken is thru ArgumentTypeConverterAttribute.Transform
which attempts a lot of conversions before failing. Trace-Command
can help you see all conversion attempts performed by PowerShell:
Trace-Command TypeConversion { [bool] $var = $null } -PSHost
As for when the conversion succeeds, when running optimized, we can see that a different code path is taken. There is a great cmdlet from the ScriptBlockDisassembler
Module that can help us see what method is called:
{ [bool] $var = $null } | Get-ScriptBlockDisassembly -Minimal
The following outputs:
// ScriptBlock.EndBlock
try
{
locals.Item009 = Fake.Dynamic<Func<CallSite, object, bool>>(
PSConvertBinder.Get(typeof(bool)))(null);
}
catch (FlowControlException)
{
throw;
}
catch (Exception exception)
{
ExceptionHandlingOps.CheckActionPreference(funcContext, exception);
}
Then from here you can see that Powershell uses PSConvertBinder.Get
to perform the optimized conversion. The code for this class isn't easy to follow but what is likely to happen in this case is that the conversion ends up running thru a delegate of LanguagePrimitives
. To put it simple, something like this ends up happening:
[System.Management.Automation.LanguagePrimitives]::ConvertTo($null, [bool])
# doesn't fail, outputs `false`