- Ambiguous implementations/overrides with generic methods and NRTs
- NRT and
dynamic
interface I
{
void Foo<T>(T value) where T : class;
void Foo<T>(T? value) where T : struct;
}
class C : I
{
void I.Foo<T>(T? value) { }
}
This was valid code in C# 7 and would always match the Nullable
overload because T? could only mean Nullable<T>. Now, however, T?
is ambiguous. The ambiguity also cannot be fixed because we do not
allow you to write the constraints.
Possible fixes:
-
The existing code means the same thing as before, namely that
T?
always meansNullable<T>
. If you would like to use the nullable class form, we allow exactly one constrain on the override/implementation:where T : class
. -
Same as (1), but also allow the constraint
where T : struct
. -
No new syntax, do matching in two passes. First, look if the old form (
where T : struct
) is present and use it if present. Second, expand to look for both forms if no match was found.
These fixes would address the problem where we currently just choose the "first" one. Note that this is not the only situation where you observe ordering dependence for OHI, but it also appears that that ordering dependence appears in the CLR, so we're not considering this fix to address all of the complexity here, just to not make things worse.
Option 4: When you are trying to match an implementation against two signatures, you prefer the
one that, for each type parameter, if it has any T?
instantiations, each of those type
parameters has struct
constraints. In other words, for each place where the overriding member
has a ?
, we look for a signature that has a struct type parameter in at least the same places
as every other candidate.
Follow-on question: why not allow implementations and overrides to specify constraints? The biggest problem is that C# does not allow you to specify certain constraints when you actually need to in order to correctly override. VB solves this problem by allowing anything, specifically for OHI (overriding, hiding, and interface implementation). We could also allow a subset of constraints to be specified, but we would have to define what we mean by subset.
The main problem we have with (4) is we're worried that the preference rule would be confusing.
Fundamentally, allowing a bare T?
to mean either Nullable<T>
or nullable reference based on
context of what is defined in the base is worrying.
Conclusion
We like option 2. This would maintain backwards compatibility and is a
simple explanation for what T?
means in OHI.
Do we want to report or track nullability for values of type dynamic
?
In some dynamic scenarios we don't know the nullability, but the simple
assignment of null to a local of dynamic type must be null.
It seems like there could be value in providing at least some null tracking.
For a method which simply returns a dynamic?
we could produce a maybe null
dynamic value. For invocations, we could always produce a dynamic!
that
is not maybe null. This seems like a reasonable middle ground.
Conclusion
Let's track nullability for dynamic
. Tracking dynamic values seems somewhat useful. We would do
the same value-based flow analysis as we do for other types. Some scenarios with dynamic
would
not produce warnings, but if a warning is produced it seems likely to indicate a real problem.
For static methods, we currently do no type checking for the dynamic arguments, but we do check if any of the candidates are possible. Would we provide null checking in addition to this? For all argument types, including the dynamic arguments?
Conclusion
Since we already do some validation for static invocations, and we can provide some useful static information here, it sounds good to do it if possible.
We currently do no checking on the return type (the return type is always dynamic, even if all the candidates return void). Adding nullability does not seem useful given the lack of checking that we already do.
Conclusion
The return type is always dynamic!
.