-
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
Fix nullable annotations on generic math interfaces #74025
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @dotnet/area-system-numerics Issue Details
cc: @jeffhandley, @tannergooding, @drewnoakes, @jaredpar, @RikkiGibson, @MadsTorgersen
|
I agree with all the changes except the "oblivious TOther" change—but that's only because I haven't paged in that particular problem yet. I did want to raise some additional potential usability issues that maybe we can reason through together: If someone decides to implement Conversely if someone was trying to ensure their caller provides an operator which can accept null, they might use a constraint like where It feels like the most "robust" pattern is for type authors to implement In general it seems like it would make sense to sprinkle |
Interesting. Earlier today I actually asked @jaredpar whether he thought there were any uses for us properly annotating the interfaces for co/contravariance, as I couldn't think of any. I guess there is one. I'll push an additional commit with what that would look like and we can decide whether it makes sense. |
d976d00
to
197d96f
Compare
I pushed a commit with what I think the co/contravariance would look like. Two oddities are IAdditiveIdentity and IMultiplicativeIdentity, in which TSelf isn’t used in any of the members, so I left it as invariant even though it could be either covariant or contravariant. |
cc: @terrajobst, @bartonjs |
What's the null state for the out parameter in |
Depends on the T. If T is non-nullable, then the null state is not-null. If T is nullable, then the null state is maybe-null. We use |
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.
First commit LGTM, besides a couple clarifying questions.
For the The intent here is that I know there are also various edge cases with It might overall be safer if we say that |
I'm fine removing the variance commit; I added it in response to Rikki's comments, and I didn't have any scenarios for it. His scenario doesn't actually involve anything other than TSelf from a runtime perspective, but the possibility of TSelf vs TSelf?, and using variance to allow for that distinction. But that scenario also requires it on TSelf, so if we're talking about reverting the variance changes, I'd be inclined to just revert them all-up, and revisit them in .NET 8 (though Jared raised the possibility that adding variance later could be a breaking change in its impact on overload resolution). |
I don't have a strong preference here. I'm also unsure on how impactful such a break will be in practice considering the expected usage for most of these types between .NET 7 and .NET 8. For
I know that we have For the other operators they need at least one parameter to be the implementing type which should always be |
Yup. But, we also have |
- All `where TSelf : ...` constraints become `where TSelf : ...?`. Without this, trying to define a type like `Matrix<T> where T : INumber<T>?` in order to support nullable T types warns because `INumber<T>` constrains its `T` (`TSelf`) to be non-nullable. - All `where TOther : ...` constraints are changed to be oblivious. They can't be correctly annotated as there's no way to express the nullability relationship with the nullability of TSelf. - Use `[MaybeNullWhen(false)] out T` instead of `[NotNullWhen(true)] out T?`, as we do with other generics, since if the instantiation of `T` is nullable, we can't guarantee `NotNullWhen(true)`. - Make `IEqualityOperators` `==` and `!=` accept `TSelf?`. This keeps it consistent with `IEquatable<T>.Equals(T?)`, `IEqualityComparer<in T>.Equals(T?, T?)`, `IEqualityComparer.Equals(object?, object?)`, `IStructuralEquatable.Equals(object?, IEqualityComparer)`, and `object.Equals(object?)` which all allow null even if generic and the generic is non-null. It in turn enables checks like `T.Zero == default` without nullability warnings.
197d96f
to
56aa719
Compare
Removed the extraneous @RikkiGibson, are you ok with this? Any other feedback? |
Top line: yeah, I think it is fine to keep these type parameters invariant for .NET 7. IEquatable is invariant somewhat because of compat and it is justified somewhat in terms of design (maybe a comparer written for Base is not going to do the right thing on Derived if Derived introduces additional state). To "make up" for this the compiler special cases IEquatable to make it "nullable-variant". SharpLab. I actually wonder to what extent being able to do this explicitly on interfaces like
This actually does seem suspect. Why would it make sense to convert from
Yeah, it means that some conversions that used to be invalid would become valid, and that would affect the viability/betterness of overloads, etc. My gut feeling is that is likely to be a low-impact break. Would it be helpful to "protect" this space by constraining some of the type parameters to |
Blocking classes from participating in generic math is a gigantic hammer, one I'm certainly not going to wield while @tannergooding is on vacation. I'd like to get these NRT changes in for RC1, and I'll open a separate issue for the remaining things to be investigated when Tanner is back online. |
Agreed. Let's not exclude classes. |
/backport to release/7.0-rc1 |
Started backporting to release/7.0-rc1: https://github.com/dotnet/runtime/actions/runs/2879292036 |
where TSelf : ...
constraints becomewhere TSelf : ...?
. Without this, trying to define a type likeMatrix<T> where T : INumber<T>?
in order to support nullable T types warns becauseINumber<T>
constrains itsT
(TSelf
) to be non-nullable.where TOther : ...
constraints are changed to be oblivious. They can't be correctly annotated as there's no way to express the nullability relationship with the nullability of TSelf.[MaybeNullWhen(false)] out T
instead of[NotNullWhen(true)] out T?
, as we do with other generics, since if the instantiation ofT
is nullable, we can't guaranteeNotNullWhen(true)
.IEqualityOperators
==
and!=
acceptTSelf?
. This keeps it consistent withIEquatable<T>.Equals(T?)
,IEqualityComparer<in T>.Equals(T?, T?)
,IEqualityComparer.Equals(object?, object?)
,IStructuralEquatable.Equals(object?, IEqualityComparer)
, andobject.Equals(object?)
which all allow null even if generic and the generic is non-null. It in turn enables checks likeT.Zero == default
without nullability warnings.cc: @jeffhandley, @tannergooding, @dakersnar, @jaredpar, @RikkiGibson, @MadsTorgersen
Fixes #73855