-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Use Span<T>.Fill
implementation for Array.Fill
#52590
Conversation
Tagging subscribers to this area: @tannergooding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a Span<T>
wrapped around a T[]
in this manner is illegal. (It doesn't account for array variance and could lead to type-safety violations.) You need to have logic similar to what's already in the Span<T>
ctor:
runtime/src/libraries/System.Private.CoreLib/src/System/Span.cs
Lines 44 to 45 in b0c7f04
if (!typeof(T).IsValueType && array.GetType() != typeof(T[])) | |
ThrowHelper.ThrowArrayTypeMismatchException(); |
Basically, if the span ctor would've succeeded, then turn it into a span and call fill. Otherwise loop and set each array element individually.
I will make the suggested changes. However. I see the same pattern used elsewhere, without the type check, e.g.:
|
@gfoidl Could we combine conditions and just throw runtime/src/libraries/System.Private.CoreLib/src/System/Span.cs Lines 406 to 418 in fbd3b98
|
I like the idea, but I fear this a breaking change as a different exception will be thrown. In the span-code it's just the ArgumentOutOfRangeException, whilst the array-code has a distinction between index and count. |
Correct - it's generally not considered an acceptable breaking change to change or combine exceptions like that. The reason we could get away with it in |
We should add extra unit tests for this:
|
{ | ||
array[i] = value; | ||
var span = new Span<T>(ref MemoryMarshal.GetArrayDataReference(array), array.Length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't need this; should be able to do new Span<T>(array).Fill(value);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, we've already checked array
for null
so the null check in Span(T[]? array)
should be eliminated by the jit. I don't think we can do similar in same Fill<T>(T[] array, T value, int startIndex, int count)
because the argument validation is more complex.
public static void Fill_Casting() | ||
{ | ||
Bar[] barArray = new Bar[2]; | ||
Array.Fill<object>(barArray, new Bar()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please validate that Array.Fill
actually populated the array. Something like:
public class Bar
{
public string Value;
}
Bar[] barArray = new Bar[]
{
new Bar() { Value = "1" },
new Bar() { Value = "2" },
};
Array.Fill<object>(barArray, new Bar() { Value = "x" });
Assert.Equal("x", barArray[0].Value);
Assert.Equal("x", barArray[1].Value);
This method should also test the other Fill
overload. For instance: new Bar[5]
, with Fill(theArray, 1, 2)
, ensuring that only theArray[1] and theArray[2] were changed. There's no need to unit test that the bounds check is correct; I think there's already a unit test for that.
And we should have a test between uint[]
<-> int[]
as well, to ensure that we didn't inadvertently break that logic. For instance:
int[] ints = new int[] { 10, 20, 30, 40, 50 };
uint[] uints = (uint[])(object)ints;
// do work via Array.Fill(uints, ...);
Assert.Equal(new int[] { /* whatever the expected sequence is */ }, ints);
[Fact] | ||
public static void Fill_Casting_Invalid() | ||
{ | ||
Bar[] barArray = new Bar[2]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to above, but after the call to Assert.Throws
this unit test should ensure that the contents of the array remain unchanged.
} | ||
|
||
[Fact] | ||
public static void Fill_Casting() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add similar tests coverage for the other Array.Fill overload. If I am reading the code correctly, it has a bug that you should be able to find by adding the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bug fixedup in 07dd088
@xtqqczze Do you think you'll be able to address the remaining feedback? |
I'll be able to continue working on this in the w/c 2021-06-20. |
Cool; thanks! If it's merged in by July 12, it'll make .NET 6.0. |
Assert.Throws<ArrayTypeMismatchException>(() => Array.Fill<object>(barArray1, new Foo())); | ||
|
||
Bar[] barArray2 = CreateBarArray(); | ||
try |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use try / catch in these tests, as it won't properly handle the failure condition. Instead, use:
Assert.Throws<ArrayTypeMismatchException>(() => ...);
Assert.Equal(...);
These two lines will check that the callback throws, and it will also check that the array has not been mutated, as you are doing here.
See near the top of this file for some other examples of using Assert.Throws
.
{ | ||
Bar[] barArray1 = CreateBarArray(); | ||
Array.Fill<object>(barArray1, new Bar() { Value = "x" }); | ||
Assert.Equal(Enumerable.Repeat("x", barArray1.Length), barArray1.Select(e => e.Value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using new string[] { 'x', 'x', 'x', 'x' }
here to match the pattern you have in line 4285 below. Removing the call to Enumerable.Repeat
uncomplicates the test.
(Same comment for line 4289.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was originally considering parameterised the tests, this would require the second Fill_Casting
test to be written something like:
runtime/src/libraries/System.Runtime/tests/System/ArrayTests.cs
Lines 4216 to 4223 in 8c90687
T[] before = source.Take(startIndex).ToArray(); | |
T[] after = source.Skip(startIndex + count).ToArray(); | |
Array.Fill(array, value, startIndex, count); | |
Assert.Equal(before, array.Take(startIndex)); | |
Assert.Equal(Enumerable.Repeat(value, count), array.Skip(startIndex).Take(count)); | |
Assert.Equal(after, array.Skip(startIndex + count)); |
But this seemed unnecessarily complicated 😋
|
FYI - there's no need to force push. In fact, doing so makes the PR more difficult for us to review. |
Also, the PR as of commit c48c69c seemed fine. Why change it to df45239e19ff03b43175bc24ebc0fbfc543682ce? Seems like that change introduced compilation errors and reverted some of the test cleanup you did. |
To clarify here, df45239e19ff03b43175bc24ebc0fbfc543682ce was the commit you approved (which contained compilation errors), c48c69c is the current revision (which was force pushed). Sorry for the confusion, in future, I shall avoid force push. If it helps I can rebase back onto df45239e19ff03b43175bc24ebc0fbfc543682ce to present a linear history. |
Derp, swapping the two was my mistake. But it's a good case study of how force pushing makes reviewing more difficult: because we need to fetch the two commits individually and perform a manual diff against them offline, which presents opportunity for reviewers to err (as I had done here). In the future, if you stick to simple pushes, it'll mean that we can review using just the web UI here, which makes looking at diffs much simpler. Thanks! :) |
CI failures are caused by https://status.dev.azure.com/_event/246897938. We'll sit tight and wait for this to resolve itself. |
@xtqqczze Looks like 2 categories of errors: The first is that c48c69c should be reverted, and those lines should instead read The second is that I think you need to implement I think we're getting close here! :) |
This reverts commit c48c69c.
PeriodTimer failures are a known issue (also tracked via #54543). |
Test failures:
These appear to be tracked by #54778. |
I kicked off a new manual run: https://dev.azure.com/dnceng/public/_build/results?buildId=1212225&view=results |
Thanks @xtqqczze! |
@GrabYourPitchforks We got there in the end! Thanks for all your help with this. |
Fix #7049
Follow-up #51365
cc: @GrabYourPitchforks