-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Prototype: Non-null reference types #7445
Comments
I guess that the rules for implicit types are nullable by default because they can't be "used" outside the scope where they are declared and in this scope flow analysis is enough. The benefit is that even when they're initialized to a non-null value (locals and anonymous types have to) they might still be assigned null later on without warning. I'd like to point out that this choice might hurt later, should C# ever allow implicitly typed members. I.e. if you ever allow replacing The same problem will arise if you ever allow to omit the return type of a lambda method or property: Of course, a workaround would be to continue to explicitly declare the return type, but that kind of defeats the feature. It would be nice if there was a "non-null" cast operator. I have no good syntax to propose, so I'll use This is useful when a method returns a nullable type, but you know from the parameters that you passed that the result is actually not null. For example: // Concat returns null if any argument is null
string? Concat(string? a, string? b)
{
if (a == null || b == null) return null;
return a + b;
}
var phrase = Concat("hello", "world");
phrase.Length; // <-- Error because phrase might be null, although we know it's not
// With a "non-null" cast
var phrase = (!)Concat("hello", "world");
phrase.Length; // <-- OK The only possible workaround is to perform a cast to the non-null type, which can be inconvenient if the type name is long or impossible if the type is anonymous and fails to be determined non-null by flow analysis. It could also be used to help with the problem outlined above, to make implicit members non-null: |
@jods4 IIRC there was also a proposal to add a private var cache = new Dictionary<string, int>()!; |
@HaloFour that's cool. I guess it works great when dotting into a ref, e.g. It does the job but is slightly weird at the end of an expression: |
@AlekseyTs Just to mention, |
I think this would be a lot more sensible if you could overload based on nullability. e.g.
I'm not intimately familiar with the runtime and compiler platform, but from the little I do know, I don't think that the current approach would allow for this. Perhaps a |
@yaakov-h Maybe we could get away without a new syntax? |
@jods4 That sounds like a bastardisation of generics to me, and wouldn't necessarily let you do everything you'd want from nullability overloads. |
@yaakov-h I agree that it feels a bit wrong. |
@jods4: Assume the following:
(Assuming the postfix-non-null-cast operator How would you do this with only |
@yaakov-h Ah I see what you mean. The problem is not the signature but implementing the function. On the other hand, overloads by nullability are not allowed either, are they? As they would compile to exactly the same IL method? As long as the CLR doesn't support nullable proper, this is going to be hard APIs to model in C# 😞 |
@jods4 I was under the impression that the CLR can overload by modopt or modreq (e.g. |
@yaakov-h @alrz These are indeed downsides to the design. The reason for it is to allow APIs to add nullability annotations while staying binary compatible. Using Nullable specifically would also cause a perf hit: That is a struct with two members, one of which (the bool tracking whether the content is null) would be redundant for reference types, yet expensive to carry around. |
@yaakov-h You're right about |
@MadsTorgersen For binary compatibility I believe an attribute would just work (as it does today if you are working with an analyzer) but for a type like |
Using modopts will break backward compatibility story. It would mean that, once you annotate types in a library, old clients (previously built consumers of the library) won't be able to use now annotated APIs because modopts change signatures. |
@AlekseyTs Compile-time compat: |
@AlekseyTs Couldn't we have both? I mean, If I want to take advantage of non-nullable reference types and don't break backward compat I'd use an attribute, and otherwise I'd use the type |
@jods4 Binary compat: Runtime doesn't do overload resolution, it looks for an exact signature match. Compiler can ignore modopts for the purpose of the analysis, but it has to spit them out in signatures anyway @alrz Are you saying that anyone going after binary compatibility won't be able to use |
@AlekseyTs Yes, "which becomes rather tricky with arrays and generics" you mean something like array of nullables or nullable array of non-nullables? That seems quite rare (I usually want it all be nullable or non-nullable, so |
@AlekseyTs by runtime I meant JIT of course...
I wouldn't assume that too quickly... I could come up with examples without much trouble. One single example: say you implement |
@jods4 Ok, assuming that non-nullability is the default, when you want to opt out without a breaking change you use the For example, if you want nullable types for a function
whatever type it is. But when you change the types to |
@alrz So, if they're required to use public static List<Dictionary<string, string?>> Foo() { ... } is really [return: Nullable(new bool[] { false, false, false, true })]
public static List<Dictionary<string, string>> Foo() { ... } |
@HaloFour No, when you use |
@alrz And my point is that that is the attribute notation someone would have to know in order to manually decorate a parameter. I recall |
@HaloFour I'm thinking that the |
@alrz I don't see how |
@HaloFour Lots of proposed features already depend on the targeted framework, with separate branch of .NET framework itself (.NET Core 1.0) it makes more sense to rethink the design and don't repeat the same mistakes again, non-nullability seems most fundamental among others and it's really important how we start to support it. As for |
@alrz Yes, but they don't depend on the version of the compiler. I can compile an assembly using Roslyn targeting .NET 2.0 and I can consume it from a project using Visual Studio 2005 without issue. I've not heard of any mention of Roslyn making hard breaks with the full .NET framework and I seriously doubt that a feature such as this would take a hard dependency on such a thing. The attribute-based implementation is the result of a lot of compromises spanning a lot of discussions and I think that everyone is in agreement that it's not a perfect solution but it is a good solution. |
@HaloFour Generics also introduced to the .NET framework on top of non-generic interfaces e.g. |
@alrz Yes, but you could still target .NET 1.1 using C# 2.0, compile an assembly, and consume it using C# 1.x without issue. Anywho, I get what you're trying to do, but if the team is trying to get this feature into the next release (implied by the "Urgency-Soon" tag) I doubt that it will survive any serious consideration for redesign. And, as mentioned, |
I don't think that is the compatibility issue with modopt, is it? The ECMA standard says that a compiler is free to ignore modopt it doesn't understand, and nullable types fit into this model. My understanding is that binary compatibility doesn't work: if Microsoft releases .NET next using nullability annotations everywhere (and they should do that), then existing assemblies built against earlier .net versions will not run against on top of the new framework (because method signatures have changed), which is a no-go. The point about |
Doesn't this only apply if .NET next is an in-place upgrade (e.g. 4.5 over 4.0), and not a separate framework (e.g. .NET 4.0 vs .NET 3.5)? |
They allow more breakage, but generally MS keeps a high compatibility level even between side-by-side major releases. In fact, all of the projects I use or maintain run perfectly on 4.6, even those that were compiled for the 2.0 runtime. Adding |
@jods4 With extremely few exceptions you can expect the same going back to .NET 1.0 assemblies. |
I think an using(var e = list.GetEnumerator()) {
while(true) {
let Some(var item) = e.Next() else break;
// loop body
}
}
using(var e = asyncSeq.GetAsyncEnumerator()) {
while(true) {
let Some(var item) = await e.NextAsync() else break;
// loop body
}
} So |
@alrz Wouldn't that be |
@HaloFour If you agree that |
Work items list (#22152) supersedes this for tracking work. |
Feature Documentation
The text was updated successfully, but these errors were encountered: