Skip to content

Conversation

@stephentoub
Copy link
Member

It currently fails because it uses IndexOf, but there's no fundamental reason this shouldn't work with multidimensional arrays, and it breaks expectations that it doesn't work. We can just fall back to enumerating if the Rank isn't 1.

Fixes #119569

We need to fix the issue in 10, and fixing Array itself seems like the right answer. If there are objections to backporting this fix, please let me know. This is taking something that previously always threw an exception and now makes it work. (There are probably a couple more array interface implementations we could fix subsequently to work with multidimensional arrays, as well, e.g. CopyTo, a problem we've bumped into from LINQ as well in the past.)

It currently fails because it uses IndexOf, but there's no fundamental reason this shouldn't work with multidimensional arrays, and it breaks expectations that it doesn't work. We can just fall back to enumerating if the Rank isn't 1.
@stephentoub stephentoub added this to the 10.0.0 milestone Sep 11, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes the IList.Contains method to work with multidimensional arrays. Previously, Contains always failed for multidimensional arrays because it relied on IndexOf, which only supports single-dimensional arrays. The fix implements a fallback enumeration approach for multidimensional arrays while maintaining the efficient IndexOf-based implementation for single-dimensional arrays.

Key changes:

  • Modified Array.IList.Contains to check array rank and use enumeration for multidimensional arrays
  • Added comprehensive test coverage for both value and reference type multidimensional arrays
  • Added LINQ integration test to verify the fix works with existing LINQ operations

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/libraries/System.Private.CoreLib/src/System/Array.cs Core fix implementing rank check and enumeration fallback for multidimensional arrays
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ArrayTests.cs Enhanced test coverage for multidimensional arrays and IList.Contains behavior
src/libraries/System.Linq/tests/OfTypeTests.cs Integration test verifying LINQ operations work with multidimensional arrays

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Sep 11, 2025
Copy link
Member

@eiriktsarpalis eiriktsarpalis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@stephentoub stephentoub added area-System.Runtime and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Sep 11, 2025
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

bool IList.Contains(object? value)
{
return IndexOf(this, value) >= this.GetLowerBound(0);
// IndexOf only works for single-dimensional arrays.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be fixed in IndexOf instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the output of IndexOf be in the context of MD arrays?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered that, but wasn't sure what semantics we would want for IndexOf in terms of the actual index it computes.

My intent is to backport this change to 10. If we're ok at this point defining what those semantics would be, I could do that instead.

Copy link
Member

@eiriktsarpalis eiriktsarpalis Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the type does implement IList so an MD array index is technically speaking well defined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the type does implement IList so an MD array index is technically speaking well defined.

You mean because of the indexer? That throws for MD arrays, too.

Copy link
Member Author

@stephentoub stephentoub Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was that for 10 we fix Contains in order to fix the LINQ issue, and then for 11 if desired we can look at declaring the ordering and making the other members work... doing so will unblock some other optimizations we previously tried in LINQ but had to back out because the incoming IList wasn't actually usable, and just in general remove unexpected surprises.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean because of the indexer? That throws for MD arrays, too

Ah, ok. It is weird that IList.Count does not throw, but the indexer throws.

Agreed :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't work for all MD arrays as they can have more than Array.MaxLength items and therefore have values outside the bounds of int.MaxValue

Copy link
Member Author

@stephentoub stephentoub Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't work for all MD arrays

What is the "that" in this statement? IndexOf? That's a good point, too, in addition to ordering, you might have indices too large to return.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any of the IList APIs that deal with indexes risk not working with MD arrays, so while we could update them to work in some cases (and likely almost any real world case), it is important to note (at least in the impl, potentially in the documentation) that some MD arrays are "incompatible" with those APIs and so we need to have the fallback paths regardless.

@jkotas
Copy link
Member

jkotas commented Sep 11, 2025

We need to fix the issue in 10

Why do we need to fix this issue in 10? Was there some change in Linq that exposed this bug?

@jkotas
Copy link
Member

jkotas commented Sep 11, 2025

Ok, I read the issue - this was exposed by #119569 (comment) .

Have we looked around for other IList implementations with the same problem?

The safe fix for .NET 10 would be to revert the optimization.

@stephentoub
Copy link
Member Author

Was there some change in Linq that exposed this bug?

Yes, OfType().Contains() uses IList.Contains now. I'd rather fix Array's broken implementation than revert the optimization.

@stephentoub
Copy link
Member Author

Have we looked around for other IList implementations with the same problem?

It's rarely used in LINQ, which is mostly based on IEnumerable<T>; the only two operators that work with the non-generic IEnumerable are Cast and OfType. I searched for other IList uses that would be problematic and didn't see any.

@skyoxZ
Copy link
Contributor

skyoxZ commented Sep 11, 2025

public override bool Contains(TResult value)
{
// Avoid checking for IList when size-optimized because it keeps IList
// implementations which may otherwise be trimmed. Since List<T> implements
// IList and List<T> is popular, this could potentially be a lot of code.
if (!IsSizeOptimized &&
!typeof(TResult).IsValueType && // don't box TResult
_source is IList list)
{
return list.Contains(value);
}

This optimization seems problematic. The code below correctly outputs True in .NET 9 but now it outputs False.

using System;
using System.Collections.Generic;
using System.Linq;

List<D> source = new List<D> { new D(1) };
D d2 = new(2);
Console.WriteLine(source.OfType<S>().Contains(d2));


class S : IEquatable<S>
{
    public bool Equals(S? other) => other is not null;

    public override bool Equals(object? obj) => obj is S;

    public override int GetHashCode() => 0;
}

class D : S, IEquatable<D>
{
    public D(int x) => X = x;

    public int X { get; }

    public bool Equals(D? other) => other is not null && X == other.X;

    public override bool Equals(object? obj) => obj is D d && Equals(d);

    public override int GetHashCode() => X;
}

@tannergooding
Copy link
Member

tannergooding commented Sep 11, 2025

// IndexOf only works for single-dimensional arrays.
if (Rank == 1)
{
return IndexOf(this, value) >= GetLowerBound(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has integer overflow bug.

using System.Collections;

IList a = Array.CreateInstance(typeof(int), [5], [Int32.MinValue]);
Console.WriteLine(a.Contains(42));

prints true, but it should print false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we return GetLowerBound(0) - 1 in the case that it isn't found, so this should likely be the following, which indicates it was found regardless of length (provided it is Rank == 1)

Suggested change
return IndexOf(this, value) >= GetLowerBound(0);
return IndexOf(this, value) != (GetLowerBound(0) - 1);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall I fix that in this PR as well? I didn't introduce this line, I just wrapped it in the if.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. It is part of end-to-end of making IList.Contains work correctly for non-SZ arrays.

@tannergooding
Copy link
Member

tannergooding commented Sep 11, 2025

Violate what?

The issue here is in:

Console.WriteLine(((S)source[0]).Equals(d2));
vs
Console.WriteLine(source[0].Equals(d2));

You've defined two different IEquatable<T> implementations for the same hierarchy and not unified them, which means that x.Equals(y) will return true for two objects that return different hash codes (1 vs 2) and violates the requirements defined.

For IEquatable<T> in particular that is https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=net-9.0#notes-to-implementers, which basically boils down to Equals(object), IEquatable<T>.Equals(T) and GetHashCode() all being part of a unified thing and needing to be self consistent between eachother; otherwise bugs like you've listed can and will occur.

@skyoxZ
Copy link
Contributor

skyoxZ commented Sep 11, 2025

@tannergooding you are right. What about this example:

using System;
using System.Collections.Generic;
using System.Linq;

List<S> source = new List<S> { new S() };
Console.WriteLine(source.OfType<D>().Contains(new D(1)));

class S
{
    public bool Equals(S? other) => other is not null;

    public override bool Equals(object? obj) => obj is S;

    public override int GetHashCode() => 0;
}

class D : S
{
    public D(int x) => X = x;

    public int X { get; }

    public bool Equals(D? other) => other is not null && X == other.X;

    public override bool Equals(object? obj) => obj is D d && Equals(d);

    public override int GetHashCode() => X;
}

@tannergooding
Copy link
Member

tannergooding commented Sep 11, 2025

That correctly reports false. source.OfType<D>() returns 0 items as it only contains a single item of type S and so the contains check fails.

Contains will only use object.Equals or IEquatable<T>.Equals for comparison. It won't use an arbitrary user-defined method named Equals. (excepting a custom IEqualityComparer being passed in, but that then must follow the same rules/guidance).

When implementing either object.Equals or IEquatable<T>.Equals, they are required to be consistent and that includes correctly having the right virtual declarations for any of the APIs that might be used here so they can be overridden by a derived type to remain correct.

You can write all kinds of incorrect code and invalid implementations that violate this, but the bug for that is on the type author not on LINQ or the built-in collection types.

@skyoxZ
Copy link
Contributor

skyoxZ commented Sep 11, 2025

That correctly reports false. source.OfType() returns 0 items as it only contains a single item of type S and so the contains check fails.

The output is True in .NET 10.

@tannergooding
Copy link
Member

The output is True in .NET 10.

I see.

That looks like a bug in OfTypeIterator.Contains then, yes. It's directly grabbing the underlying list and not filtering based on whether the items are of type TResult, which could cause items to unnecessarily be returned.

CC. @stephentoub

@stephentoub
Copy link
Member Author

Replaced by #119600

@stephentoub
Copy link
Member Author

@tannergooding you are right. What about this example:

using System;
using System.Collections.Generic;
using System.Linq;

List<S> source = new List<S> { new S() };
Console.WriteLine(source.OfType<D>().Contains(new D(1)));

class S
{
    public bool Equals(S? other) => other is not null;

    public override bool Equals(object? obj) => obj is S;

    public override int GetHashCode() => 0;
}

class D : S
{
    public D(int x) => X = x;

    public int X { get; }

    public bool Equals(D? other) => other is not null && X == other.X;

    public override bool Equals(object? obj) => obj is D d && Equals(d);

    public override int GetHashCode() => X;
}

That example is also problematic. x.Equals(y) != y.Equals(x)

@github-actions github-actions bot locked and limited conversation to collaborators Oct 12, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LINQ Contains() crashes with System.RankException when used together with a multidimensional array

5 participants