Search code examples
c#powershellimplicit-conversionpowershell-7.0powershell-7.2

Implicit conversion to bool of struct defined in C# fails in PowerShell


Why does an implicit conversion to [byte] work, but when replacing byte by bool it no longer works?

I. e. the following works...

Add-Type -TypeDefinition @'
public readonly struct MyByte
{
    private readonly byte value;

    public MyByte( byte b ) => this.value = b;

    public static implicit operator byte( MyByte b ) => b.value;
    public static explicit operator MyByte( byte b ) => new MyByte( b );

    public override string ToString() => $"{value}";
}
'@

[byte] $d = [MyByte]::new( 1 )    # OK

...while this very similar code does not:

Add-Type -TypeDefinition @'
public readonly struct MyBool
{
    private readonly bool value;

    public MyBool( bool b ) => this.value = b;

    public static implicit operator bool( MyBool b ) => b.value;
    public static explicit operator MyBool( bool b ) => new MyBool( b );

    public override string ToString() => $"{value}";
}
'@

[bool] $b = [MyBool]::new( $true )    # Error

This produces the following error:

Cannot convert value "MyBool" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.

Note that in C# the implicit conversion to bool works as expected:

public class MyBoolTest {
    public static void Test() {
        bool b = new MyBool( true );    // OK
    }
}

So this seems to be a PowerShell issue only.

(PSVersion: 7.2.2)


Solution

  • You've done most of the discovery yourself already, assisted by Santiago Squarzon, but let me try to summarize:

    You're seeing two separate problematic PowerShell behaviors:

    • Problematic behavior A: PowerShell has its own, built in to-Boolean conversion logic, which, unfortunately, does not honor implicit or explicit .NET conversion operators.

      • The bottom section of this answer summarizes the rules of this built-in logic, which explains why it considers any instance of your [MyBool] type - even [MyBool]::new($false) - $true, unfortunately.

      • Only in operations where an instance isn't coerced to a Boolean first are the conversion operators honored, which for most operators means using the instance on the LHS:

        [MyBool]::new($false) -eq $false # -> $true
        
        [MyBool]::new($false), 'other' -contains $false # -> $true
        
        # With -in, it is the *RHS* that matters 
        $false -in [MyBool]::new($false), 'other' # -> $true
        
      • By contrast, if you force a Boolean context - either by using a Boolean on the (typically) LHS or with implicit to-Boolean coercion - PowerShell's built-in logic - which doesn't honor conversion operators - kicks in:

        $false -eq [MyBool]::new($false) # -> !! $false
        
        $false, 'other' -contains [MyBool]::new($false) # -> !! $false
        
        # With -in, it is the *RHS* that matters 
        [MyBool]::new($false) -in $false, 'other' # -> !! $false
        
        # Most insidiously, with *implicit* coercion.
        if ([MyBool]::new($false)) { 'what?' } # -> !! 'what?'
        
    • Problematic behavior B: When you type-constrain a variable with [bool], i.e. when you place the type literal to the left of the variable being assigned (e.g, [bool] $b = ..., as opposed to $b = [bool] (...),[1] the rules for binding a [bool] parameter - unexpectedly and inappropriately - kick in, which - unlike the any-type-accepted built-in to-Boolean conversion - are quite restrictive, as the error message indicates.

      • That is, only $true, $false and numbers (with zero mapping to $false and any nonzero value to $true) may be passed to a parameter typed [bool].

        • Note that [bool] parameters themselves are rare, because Boolean logic is PowerShell-idiomatically expressed with a [switch] parameter instead, which, when it is (non-typically) given an explicit argument, is even more restrictive and accepts $true and $false only.
      • This problematic behavior - inappropriately applying parameter logic to (non-parameter) variables - is the subject of GitHub issue #10426.


    [1] The difference between the two is that type-constraining - [bool] $b = ... - effectively locks in the data type of variable $b, so that latter attempts to assign new values are coerced to the same type. By contrast, $b = [bool] (...) merely applies an ad hoc cast to force a conversion, without preventing later assignments from assigning values with different data types.