3

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)

zett42
  • 25,437
  • 3
  • 35
  • 72
  • 2
    Odd that `$b = [bool][MyBool]::new( $true )` works but `[bool] $b = [MyBool]::new( $true )` doesn't – Santiago Squarzon Mar 26 '22 at 19:43
  • 1
    @SantiagoSquarzon The difference is that the first one is an explicit conversion. An implicit conversion operator can be used for both, but PoSh does not seem to recognize it for the implicit case. – zett42 Mar 26 '22 at 19:49
  • 2
    @SantiagoSquarzon `[bool][MyBool]::new( $false )` -> surprisingly prints `True`! – zett42 Mar 26 '22 at 20:07
  • 2
    @SantiagoSquarzon As always, @mklement0 seems to have [the answer](https://github.com/PowerShell/PowerShell/issues/9435#issuecomment-485580461). In short: conversion to `bool` is entirely handled by PowerShell and ignores any implicit and explicit cast operators (further detailed in [comments of LanguagePrimitives.cs](https://github.com/PowerShell/PowerShell/blob/0bd93777fceab06b0b3d949a5d47cd39cf25d65d/src/System.Management.Automation/engine/LanguagePrimitives.cs#L1638-L1644)). – zett42 Mar 26 '22 at 20:14
  • 1
    btw, `$a = [MyBool]::new(1); $a | gm -force` now `$a` no longer has a value LOL – Santiago Squarzon Mar 26 '22 at 20:14
  • It's normal as you are applying *cast notation*. See [Constrained variables](https://learn.microsoft.com/en-us/powershell/scripting/lang-spec/chapter-05?view=powershell-7.2#53-constrained-variables) and [Types of variables](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_variables?view=powershell-7#types-of-variables). – JosefZ Mar 26 '22 at 22:09
  • @JosefZ, neither of your links explain the behavior in question. – mklement0 Mar 26 '22 at 23:11

1 Answers1

2

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • It would be nice to understand why would `gm` be stripping the value out of the instance too :D (my last comment on zett's question) – Santiago Squarzon Mar 26 '22 at 23:09
  • 2
    @Santiago, that behavior is indeed very curious. Note that it isn't the _value_ that is stripped, but that the for-display formatting suddenly stops working (prints an empty line instead of the value). You'll see that the value is still there with `"$a"`. I encourage you to either ask a new question here, or to create an issue on GitHub. – mklement0 Mar 26 '22 at 23:15
  • Do you think it's a bug? – Santiago Squarzon Mar 26 '22 at 23:16
  • 1
    @Santiago: yes, unquestionably. – mklement0 Mar 26 '22 at 23:16
  • 1
    Ok well, this same behavior will occur in a PS Class with a `hidden` property too. And it's not only `gm` breaking the for-display formatting, `select *` too – Santiago Squarzon Mar 27 '22 at 00:11
  • 1
    For those interested: Santiago posted a follow-up question [here](https://stackoverflow.com/q/71632827/45375). – mklement0 Mar 27 '22 at 01:52
  • 1
    Submitted [GitHub Issue #17071](https://github.com/PowerShell/PowerShell/issues/17071), thanks for the correction on the `for-display formatting` :) I linked your comment so that this Q&A is visible, if you don't mind please don't delete the comment – Santiago Squarzon Mar 27 '22 at 17:37