Search code examples
powershelloopoperator-overloading

Why customed "-eq" do twice in Powershell?


I meet a weird issue when I was doing some OOP programming in PowerShell. To be specific, the code is as below:

class x {
    [int]$v1
    x([int]$a1) {
        $this.v1 = $a1
    }

    [bool] Equals([object]$b) {
        Write-Host "123"
        if ($b -is [int]) {
            return $this.v1 -eq $b
        }
        return $false
    }
}

$xi1 = [x]::new(2)
$bv = $xi1 -eq 3

The code contains a simple custom class x, and implement constructor and an Equals method, which overloads the -eq operator. However, the code will print "123" twice, why does it happen?

In my trials, if I change the last line to "... -eq 2", only one "123" would be printed. It seems if the first comparison be $false, it will try again, and convert the type of "$b" automatically (I find it via printing $b.GetType() as well).

I guess the powershell just do the auto-type-conversion and try again if the first comparison fails. But I cannot find this grammar in the official guide. Also, how to avoid this condition?


Solution

  • I guess the [P]ower[S]hell just do the auto-type-conversion and try again if the first comparison fails.

    That is exactly right! When $xi1 -eq 3 fails, $xi1 -eq [x]3 is attempted.

    You can observe the resulting type conversion operation with Trace-Command:

    PS ~> Trace-Command -Expression { $xi1 -eq 3 } -PSHost -Name TypeConversion
    123
    DEBUG: 2024-03-27 17:44:55.8141 TypeConversion Information: 0 : Constructor result: "x".
    123
    False
    

    This behavior is encoded in the default equality comparison runtime binder, which can be found in Binders.cc:

    var conversion = LanguagePrimitives.FigureConversion(arg.Value, targetType, out debase);
    if (conversion.Rank == ConversionRank.Identity || conversion.Rank == ConversionRank.Assignable
        || (conversion.Rank == ConversionRank.NullToRef && targetType != typeof(PSReference)))
    {
        // In these cases, no actual conversion is happening, and conversion.Converter will just return
        // the value to be converted. So there is no need to convert the value and compare again.
        return new DynamicMetaObject(toResult(objectEqualsCall).Cast(typeof(object)), target.CombineRestrictions(arg));
    }
    
    BindingRestrictions bindingRestrictions = target.CombineRestrictions(arg);
    bindingRestrictions = bindingRestrictions.Merge(BinderUtils.GetOptionalVersionAndLanguageCheckForType(this, targetType, _version));
    
    // If there is no conversion, then just rely on 'objectEqualsCall' which most likely will return false. If we attempted the
    // conversion, we'd need extra code to catch an exception we know will happen just to return false.
    if (conversion.Rank == ConversionRank.None)
    {
        return new DynamicMetaObject(toResult(objectEqualsCall).Cast(typeof(object)), bindingRestrictions);
    }
    
    // A conversion exists.  Generate:
    //    tmp = target.Equals(arg)
    //    try {
    //        if (!tmp) { tmp = target.Equals(Convert(arg, target.GetType())) }
    //    } catch (InvalidCastException) { tmp = false }
    //    return (operator is -eq/-ceq/-ieq) ? tmp : !tmp
    var resultTmp = Expression.Parameter(typeof(bool));
    
    Expression secondEqualsCall =
        Expression.Call(target.Expression.Cast(typeof(object)),
                        CachedReflectionInfo.Object_Equals,
                        PSConvertBinder.InvokeConverter(conversion, arg.Expression, targetType, debase, ExpressionCache.InvariantCulture).Cast(typeof(object)));
    

    Here we can see that if PowerShell can decide on a meaningful conversion (eg. by casting [x]$b), then it'll try that and do a second comparison attempt against the results of that