Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compare-Object doesn't compare custom value types (structs) (that don't implement IComparable) correctly #23805

Open
5 tasks done
mklement0 opened this issue May 15, 2024 · 4 comments
Labels
Needs-Triage The issue is new and needs to be triaged by a work group. WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module

Comments

@mklement0
Copy link
Contributor

mklement0 commented May 15, 2024

Prerequisites

Steps to reproduce

Because Compare-Object doesn't respect a false return value when the .Equals() method is called, it falls back to string comparison, which, with the default .ToString() implementation, results in false positives.

However, for instances of value types, a false return value should be trusted, given that a value equality test is performed.
(This only applies if they don't implement IComparable; if they do, the latter is used.)

Add-Type @'
public struct Foo {
  public int Num;
}
'@

$instance1, $instance2 = [Foo]::new(), [Foo]::new()

# Make one instance different
$instance1.Num = 1

# Compare them.
# !! This currently produces NO output, even though the two instances
# !! are clearly different ($instance1 -eq $instance2 yields $false)
Compare-Object $instance1 $instance2

Related:

Expected behavior

The two instances should be considered unequal, and produce something like:

InputObject SideIndicator
----------- -------------
Foo          =>
Foo          <=

Note: With the default .ToString() implementation, both instances stringify to their (full) type name, so the reason for their inequality doesn't show here (and this default .ToString() implementation is also the cause of the false positives).

Actual behavior

No output. That is, due to the inappropriate string-comparison fallback (which results in comparing two 'Foo' strings), Compare-Object unexpectedly considered these value-type instances equal, even though they're clearly not (as $instance1 -eq $instance2 shows).

Error details

No response

Environment data

PowerShell 7.5.0-preview.2

Visuals

No response

@mklement0 mklement0 added the Needs-Triage The issue is new and needs to be triaged by a work group. label May 15, 2024
@scriptingstudio
Copy link

With objects a good practice is using parameter -property

@mklement0
Copy link
Contributor Author

mklement0 commented May 15, 2024

How about the following, then?

Compare-Object @{ Instance = $instance1 } @{ Instance = $instance2 } -Property Instance

Still broken.

The point is that it doesn't matter whether the problem arises in the context of whole-object or property-value comparisons.

Sure, if you know of a distinguishing property - assuming there even is one - you can use it as a workaround
(Compare-Object $instance1 $instance2 -Property Num), but not only should that not be necessary, it changes the output.

@scriptingstudio
Copy link

scriptingstudio commented May 15, 2024

Broken because $instance1 and $instance2 are not scalar types. And argument @{ Instance = $instance1 } is not psobject type as help defines (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/compare-object?view=powershell-7.4#inputs)

"$instance1"
Foo
"$instance2"
Foo

So Compare-Object @{ Instance = 'Foo' } @{ Instance = 'Foo' } -Property Instance has no output.

But

Compare-Object ([pscustomobject]@{ Instance = $instance1 }) ([pscustomobject]@{ Instance = 24545 }) -Property Instance
Instance SideIndicator
-------- -------------
   24545 =>
     Foo <=

works

The Compare-Object cmdlet compares two sets of objects. One set of objects is the reference, and the other set of objects is the difference. Compare-Object checks for available methods of comparing a whole object. If it can't find a suitable method, it calls the ToString() methods of the input objects and compares the string results. You can provide one or more properties to be used for comparison. When properties are provided, the cmdlet compares the values of those properties only. The result of the comparison indicates whether a property value appeared only in the reference object (<=) or only in the difference object (=>). If the IncludeEqual parameter is used, (==) indicates the value is in both objects. If the reference or the difference objects are null ($null), Compare-Object generates a terminating error. Some examples use splatting to reduce the line length of the code samples. For more information, see about_Splatting.

@mklement0
Copy link
Contributor Author

mklement0 commented May 15, 2024

No, it's broken for the reasons stated in the initial post.

$instance1 -eq $instance2 is (meaningfully) $false, so Compare-Object should report a difference too.

The other examples are irrelevant to this discussion, and as for the "If it can't find a suitable method" quote from the docs:

For value types (types derived from System.ValueType) there is a suitable method, by definition, and the fact that it isn't used (and therefore falls back to string comparison) constitutes the bug.

@mklement0 mklement0 changed the title Compare-Object doesn't compare custom value types (structs) that don't implement IComparable correctly Compare-Object doesn't compare custom value types (structs) (that don't implement IComparable) correctly May 15, 2024
@StevenBucher98 StevenBucher98 added the WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module label May 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs-Triage The issue is new and needs to be triaged by a work group. WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module
Projects
None yet
Development

No branches or pull requests

3 participants