5.7 KiB
C# Language Design Notes for Jul 11, 2018
Agenda
- Controlling nullable reference types with feature flags
- Interaction with NonNullTypesAttribute
- Feature flag and 'warning waves'
- How 'oblivious' null types interact with generics
- Nullable and interface generic constraints
Nullable feature flag
A nullable feature flag has been proposed that turns on the new warnings for the nullable reference type feature. A feature flag is proposed because new warnings could be generated for existing code, so the developer must opt-in to the new warnings.
The question is: what does the feature flag do, exactly?
Binding and emit are unaffected by feature flag, so code will not behave any differently with it enabled. If the feature is on you get nullability warnings. If the feature is off and you annotate a reference type with '?', a warning is produced that the feature is disabled, and the IDE should offer to turn the feature on. Without a warning, a developer could think they are using the feature because they've annotated their references, but in fact no warnings are being provided.
Question: What if a developer wants to annotate their public API, but they don't want to consume the feature in their own code?
Answer There are two options:
- Turn the feature on and disable all the nullable warnings
- OR turn feature off and suppress the warning about the feature being off
Interaction with NonNullTypesAttribute
Related to the feature flag, a NonNullTypes attribute is proposed that decides
how unannotated types are interpreted by the compiler. If NonNullTypes(true)
is present, unannotated types are assumed to be non-null. If it is absent
or NonNullTypes(false)
is present, then unannotated types are "oblivious"
and don't produce warnings.
How do the attribute and the feature flag interoperate? Two independently varying variables: - Assembly is annotated - Warnings are produced
Here are the possibilities:
Warning | No warning | |
---|---|---|
Annotated | C# 8 feature on | C# 8 feature off w/ suppress |
Unannotated | N/A | C# 7 |
Each of these seem reasonable.
Q: What about external annotations?
A: Don't know enough about their design yet to decide
Conclusion
Proposal accepted.
For the NonNullTypes
attribute, do we block it for older compilers?
- We usually don't poison new feature attributes for older compilers
(e.g.,
dynamic
,params
). We just provide errors in new compilers. - This has not been a problem in the past
--
Nullable references and warning waves
Q: Do we want to include the nullable references in Warning waves? Would "v8" include all nullable warnings?
- If the feature flag affects the
NonNullTypes
context, it doesn't just impact warnings - The benefit of warning waves is that tooling support wouldn't need to be customized for this feature
Proposal: If you use the feature, it updates the warning wave and sets the
NonNullTypes
setting.
Concern: If so, when you get the new warning wave, now you get nullable and all the other warnings. If you only want one set but not the other, there's no way to split them out.
Conclusion
Don't connect nullable warnings to warning waves
'Oblivious' types and generic parameters
Q: Does M<T?>
require T
to be a non-nullable reference or value type?
A: Yes.
If there's a mix of generic types where T
is oblivious or non-oblivious,
say
Generic substitution
The question is what to do about the following cases:
List<T!> | List<T~> | List<T?> | |
---|---|---|---|
T = string! | C#7: List<T> F<T>(T t); C#8: var s = F(“”); |
List<string?> | |
T = string~ | C#8: List<T> F<T>(T t); C#8: var s = F(obliviousString); |
||
C#8: List<T?> F<T>(T? t); C#8: var s = F(obliviousString); |
|||
T = string? | List<string?> |
C#7: List<T> F<T>(T t); C#8: var s = F(nullString); |
Note that the previous table uses a shorthand that is not meant to be actual C# syntax.
T!
means non-null reference type, T~
means oblivious type, and T?
means nullable type. T?
is the only syntax which is actually expected to appear in valid C# programs.
For
List<T?> F<T>(T? t);
var s = F(obliviousString);
Decision: Type parameter wins. s
is a nullable string.
For
List<T> F<T>(T t)
var s = F(obliviousString)
Is s
oblivious? Does this "flow" oblivious all over?
Proposal: "Collapse" oblivious types as soon as possible
So the previous type substitution, is List<string!>
. Even outside of type
substitution, oblivious types won't be inferred, so in
var s = obliviousString;
, s
would be inferred as string!
, not an
oblivious type.
Conclusion
Proposal accepted.
For
List<T> F<T>(T t)
var s = F(nullString);
The substitution is the argument type. s
is a nullable string.
Nullable and generic constraints
First, it is decided that T?
requires T
be known as a reference type.
Notably, an interface constraint alone is not sufficient.
What does the following do?
void M1<T>(T t) where T : I, T : class?
This is a warning or error, because T : I
implicitly means that I
is
a non-nullable type and class?
is a nullable reference type. The
constraints are contradictory. There are two fixes. Either
void M1<T>(T t) where T : I?, T : class?
to make T
a nullable reference type. Or
void M1<T>(T t) where T : I, T: class
and now T
is a non-nullable reference type.