Skip to content
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

C# Design Notes for Mar 4, 2015 #1303

Closed
MadsTorgersen opened this issue Mar 16, 2015 · 15 comments
Closed

C# Design Notes for Mar 4, 2015 #1303

MadsTorgersen opened this issue Mar 16, 2015 · 15 comments

Comments

@MadsTorgersen
Copy link
Contributor

C# Design Meeting Notes for Mar 4, 2015

The design notes can be found at https://github.com/dotnet/roslyn/blob/master/docs/designNotes/2015-03-04%20C%23%20Design%20Meeting.md

Discussion on these design notes are in this issue.

@MgSam
Copy link

MgSam commented Mar 17, 2015

So if you guys aren't adding InternalImplementationOnly, what is your plan for the Roslyn interfaces that you don't want people to implement? Perhaps marking them as Obsolete might work, as this will always generate a compiler warning (even though they're not actually obsolete).

@Miista
Copy link

Miista commented Mar 17, 2015

About the whole "what is the default value of a non-nullable reference type". Maybe say that only types that provide a default no-arg constructor can be used as non-nullable reference types. That would get around the problem of default value (if the no-arg constructor initialized the object correct).

I know that this can make it confusing to know whether a reference type is in fact non-nullable but I believe we all use an IDE that can help with that.

@SolalPirelli
Copy link

I think you mixed up n and s in part of the nullable/non-nullable code example; shouldn't it be like this?

WriteLine(n.Length); // Error!
WriteLine(s.Length); // Sure; it can't be null so no harm
if (n is string! ns) WriteLine(ns.Length); // Sure; you checked and dug out the value

@GeirGrusom
Copy link

@Miista what does that solve? The only sane thing would be to require that all non-nullable fields are assigned in every constructor. If you have a non-nullable field the compiler cannot generate a default constructor, and it shouldn't. It has to require that the type has at least one defined constructor. If it is parameterless or not doesn't matter.

In my opinion these should be the rules:

  • default(T!) is considered nonsensical
  • Passing T! to MyGeneric<T> where T : class should be a compile-time error.
  • Passing T! to MyGeneric<T> where T : class! should be OK.
  • Passing T to MyGeneric<T> where T : class! should be a compile-time error.
  • Using T! fields in a struct is considered a compile-time error (nothing good can come of it)
  • Using T! properties in a struct that are not auto-properties is OK.
  • A class containing T! fields require at least one defined constructor, and that constructor must assign all T! fields (on its own level) directly or invoke another constructor that does.
  • Casting T to T! is a compile time error unless it is provable to the compiler that T is not null.

One advantage of a T! when people are actually cheating the system by for example using a compiler that doesn't support it is that it will fail on assignment instead of dereference. Casting null to T! will fail at the call site instead of on dereference, so it will fail earlier, and very likely in a place where it will be easier to debug. So I think even without CLR support it will still improve debugging a lot even if a null were to slip into the system.

@Miista
Copy link

Miista commented Mar 17, 2015

@GeirGrusom

If you have a non-nullable field the compiler cannot generate a default constructor, and it shouldn't. It has to require that the type has at least one defined constructor. If it is parameterless or not doesn't matter.

I agree about the requirement for at least one defined constructor (which also initializes all non-nullable fields). However if it's not parameterless what is the result of default(T)?

  • default(T!) is considered nonsensical

Unless there is some requirement that non-nullable types declare a constructor that initializes all non-nullable fields.

  • Passing T! to MyGeneric where T : class should be a compile-time error.

I think this would be okay since the contract for MyGeneric<T> only requires that T! is a class. A non-nullable reference is still a reference. If you can't pass T! how are you going to store the non-nullable reference types in a collection?

  • A class containing T! fields require at least one defined constructor, and that constructor must assign all T! fields (on its own level) directly or invoke another constructor that does.

This was what I meant.

@GeirGrusom
Copy link

@Miista

I think this would be okay since the contract for MyGeneric only requires that T! is a class. A non-nullable reference is still a reference. If you can't pass T! how are you going to store the non-nullable reference types in a collection?

Generics that has a class constraint can return null, which will fail in run-time at assignment, but they can also return default(T) which will return a non-nullable containing a null value which will end up in a null dereference when the value is used.

T ReturnDefault<T>() where T : class
{
  return default(T);
}

void Foo()
{
  string! f = ReturnDefault<string!>();

  Console.WriteLine(f.Length); // NRE
}

Generics will have to be programmed with non-nullable types in mind.

edit: There is probably a way to solve this issue for generic collections. How about this:

public class List<T> where T : T!

In this manner the compiler will actually enforce class! behavior, but still allow nullable types to be used as a generic argument. In other words default(T) is not allowed and the code cannot explicitly use null as an asignment for T, but string for example is still an acceptable generic argument.

So class! is a constraint for nullable types, and T! is a constraint for telling that null or default(T) are not acceptable values. Perhaps there is some better name for this.

@Miista
Copy link

Miista commented Mar 17, 2015

Since the constraint is where T : class and not where T : class! the expression string! f = ReturnDefault<string!>(); should be string f = ReturnDefault<strong!>();

You can assign a non-nullable reference to a nullable reference but the reverse is not possible. The same is true here. The non-nullable reference satisfies the generic constraint however the constraint only promises that any output is most definitely a class. Nothing more.


Søren Palmund

Den 17/03/2015 kl. 13.32 skrev Henning Moe [email protected]:

@Miista

I think this would be okay since the contract for MyGeneric only requires that T! is a class. A non-nullable reference is still a reference. If you can't pass T! how are you going to store the non-nullable reference types in a collection?

Generics that has a class constraint can return null, which will fail in run-time at assignment, but they can also return default(T) which will return a non-nullable containing a null value which will end up in a null dereference when the value is used.

T ReturnDefault() where T : class
{
return default(T);
}

void Foo()
{
string! f = ReturnDefault<string!>();

Console.WriteLine(f.Length); // NRE
}

Reply to this email directly or view it on GitHub.

@Miista
Copy link

Miista commented Mar 17, 2015

@GeirGrusom

Generics will have to be programmed with non-nullable types in mind.

I don't agree with this.

public class List where T : T!

It isn't immediately visible that non-nullable types are also acceptable. What this says when I read it is that no nullable reference types can be used.

@HaloFour
Copy link

"The perfect is the enemy of the good" is absolutely right.

Generics seem like a can of worms with non-nullability, especially with any existing generic type. The runtime simply could not enforce that List<string!> doesn't contain a null string, especially since that instance could be passed anywhere.

For your own generic types how would T! be emitted or enforced? You'd probably need a marker generic type, like NonNullable<T> to serve as a wrapper for the compiler.

@MgSam
Copy link

MgSam commented Mar 17, 2015

I think the team should have a wait-and-see attitude with respect to non-nullability in the language because its such a difficult problem. The ?. operator as part of C# 6 hasn't even reached RTM yet. I think that as usage of it becomes more common, the amount of NullReferenceExceptions you'll see in practice will decrease, and so too will the demand for this feature.

(Not that I don't think non-nullability has a lot of value, its just so difficult to add it to the language at this point.)

@gafter
Copy link
Member

gafter commented Mar 17, 2015

@MgSam We will have an analyzer that goes with the Roslyn nuget packages that checks for clients attempting to extend or implement ISymbol. If you create a project that references the Roslyn types then you'll automatically get the analyzers run on your code.

@GeirGrusom
Copy link

@Miista

I don't agree with this.

A common source of NullReferenceException is IEnumerable<T>.FirstOrDefault() which a lot of developers use over First<T>() for no reason what so ever. If we don't implement something in generics they will still be able to get a null result here * even* when the type is non-nullable. Should they? Should the code even compile? It is obviously wrong, and with a type constraint the compiler could point it out.

edit: if not-null in generics isn't implemented the code would still be wrong, but it wouldn't be wrong because the compiler allowed obviously wrong code.

@MadsTorgersen
Copy link
Contributor Author

@SolalPirelli: I did indeed mix up s and n in the example. Fixed. Thanks!

@gzak
Copy link

gzak commented Mar 28, 2015

Not sure what has been discussed about records here so far, but this seems like exactly the sort of problem that "type providers" (a la F#) can address. I'm not saying C# should be exactly compatible with F# type providers, but that's generally the gist.

It's also useful to keep in mind why people want records in the first place: typically, there is some service endpoint which defines some contract/schema (a la WSDL or OData), and it often feels like boilerplate to have to then go and write a class to represent this schema manually. The lighter weight, the better, hence the desire for records. Luckily there's great support for WSDL in VS, but it's relatively heavy weight, and WSDLs aren't the only interfaces out there for clients to work against. Enter type providers.

Given this other dimension of the problem, revisiting the idea of type providers for C# really kills two birds with one stone, and it would be a breakthrough language feature on the order of LINQ or async/await. Personally, this is what I'm rooting for.

@gafter
Copy link
Member

gafter commented Nov 21, 2015

The design notes have been moved to https://github.com/dotnet/roslyn/blob/master/docs/designNotes/2015-03-04%20C%23%20Design%20Meeting.md

Discussion on these design notes are in this issue.

@gafter gafter closed this as completed Nov 21, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants