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?
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