Replies: 20 comments 21 replies
-
I guess, that it was already covered in #3297 |
Beta Was this translation helpful? Give feedback.
-
I saw it, it's a related discussion, but it doesn't solve the issue once and for all, which this proposal does. |
Beta Was this translation helpful? Give feedback.
-
I think there's a lot of edge cases which need to be considered here. For example: public interface I
{
string String { get; }
}
#nullable disable
public M1<T>(T t) where T : I => Console.WriteLine(t == null ? "" : t.String);
# nullable enable
public M2<T>(T? t) where T : notnull, I => M1(t); // What happens here? Does this call `M1<UniversalNullable<T>>`? Personally I think there's way too much that can go wrong with these kind of tricks. It just becomes too risky to put into the language. |
Beta Was this translation helpful? Give feedback.
-
Where's the loop? it's calling M1? |
Beta Was this translation helpful? Give feedback.
-
(Sorry I misread your code, didn't notice M1 vs M2) |
Beta Was this translation helpful? Give feedback.
-
What about cases where it's more subtle. For example, if |
Beta Was this translation helpful? Give feedback.
-
Yes, but arguably |
Beta Was this translation helpful? Give feedback.
-
As it happens dictionary throws on a Besides IEquatable is just an example. There are many cases where code dynamically checks for an interface. What if |
Beta Was this translation helpful? Give feedback.
-
Unconstrained |
Beta Was this translation helpful? Give feedback.
-
Actually I just checked and both
The same problems occur with |
Beta Was this translation helpful? Give feedback.
-
I'm not proposing to change I do understand that unconstrained T? was implemented in C# 9, but I think that was a mistake. I wish I could propose this sooner. Is it at all possible to take that implementation out of C# 9 for now and revisit in C#10? |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
True, I retract what I said previously. However, I did point out in my proposal that a lot of these under-the-hood shenanigans can be copied and pasted from |
Beta Was this translation helpful? Give feedback.
-
Again, we considered this. In depth. And we went forward with the conscious decision that we were blocking this feature. |
Beta Was this translation helpful? Give feedback.
-
If you're requiring runtime magic, why not just get this to work as is without introducing a new magic type? The runtime jits separate code for struct |
Beta Was this translation helpful? Give feedback.
-
The magic type is needed to: 1) house the hasValue and the value; 2) allow method binding and overload resolution cleanly. |
Beta Was this translation helpful? Give feedback.
-
Why? Just generate the same IL as you would if T wasn't Nullable, and then the runtime will be responsible for handling the case where T is a struct. |
Beta Was this translation helpful? Give feedback.
-
I thought about that before. The problem is that for nullability to work with struct types, you need a "hasValue". And once you accept that premise, you realize that you end up creating 2 versions of the code for each unconstrained nullable generic parameter. C# needs to support multiple generic parameters, so it becomes 2^N versions of a method; before starting any execution, a special IL needs to be output which checks all the generics and jumps to the correct version. All this is very inefficient, both space and performance wise. I understand the upside of being able to reuse Nullable just for structs in this case, but the downsides are much worse. Furthermore, when the execution leaves the generic-aware context, more difficulties arise - how do we explain to the callers of our generic methods that sometimes they have to unbox Nullable, and sometimes not? In short, just packaging the references into a UniversalNullable, while adding a minor overhead, is much simpler: no multiple versions of the code; standard logic for get/set operations; etc. I think this is the only viable approach. |
Beta Was this translation helpful? Give feedback.
-
So, leaving off the issue compat for a moment, this is only kinda true. In an ideal world, we'd have the runtime niche-fill this. Structs do need a bool for tracking, yes, but reference types do not. They can be represented by the null reference. And if you make the runtime aware of this, and specialize the implementation, then it's totally viable. This is how rust is able to represent |
Beta Was this translation helpful? Give feedback.
-
I swear, I knew this would happen. I ran into a real world problem specifically because of the lack of this feature. Consider the following: class Base<T> where T : notnull
{
public virtual T? Prop { get; set; } // I can't do this, because if T is struct, this becomes just T
public virtual Null<T> Prop { get; set; } // so I had to write my own "UniversalNullable"
}
class ChildClass<T> : Base<T> where T : class
{
}
class ChildStruct<T> : Base<T> where T : struct
{
}
public struct Null<T> where T : notnull
{
public static implicit operator Null<T>(T value) => new Null<T>(value);
public static explicit operator T(Null<T> nullable) => nullable.Value;
public static bool operator ==(Null<T> first, Null<T> second) => first.Equals(second);
public static bool operator !=(Null<T> first, Null<T> second) => !first.Equals(second);
//public static bool operator ==(Null<T> nullable, object? obj) => nullable.Equals(obj);
//public static bool operator !=(Null<T> nullable, object? obj) => !nullable.Equals(obj);
//public static bool operator ==(object? obj, Null<T> nullable) => nullable.Equals(obj);
//public static bool operator !=(object? obj, Null<T> nullable) => !nullable.Equals(obj);
//public static explicit operator Null<T>(object? obj) => default;
public static Null<T> Unbox(object? obj) => obj != null ? new Null<T>((T)obj) : default;
public Null(T value) { this._hasValue = true; this._value = value; }
public bool HasValue => this._hasValue;
public T Value { get { if (!this._hasValue) throw new InvalidOperationException("Nullable object must have a value."); return this._value; } }
public object? Box() => this._hasValue ? this._value : null;
public override bool Equals(object? obj) { ... }
public bool Equals(Null<T> obj) { ... }
public override int GetHashCode() { ... }
public override string ToString() { if (!this._hasValue) return ""; return this._value.ToString(); }
readonly bool _hasValue;
readonly T _value;
}
public static class Null
{
public static Null<T> Create<T>(T value) where T : notnull => new Null<T>(value);
public static Null<T> CreateClass<T>(T? value) where T : class => value != null ? new Null<T>(value) : default;
public static Null<T> Create<T>(T? nullable) where T : struct => nullable.HasValue ? new Null<T>(nullable.Value) : default;
}
public static class _Null_T_Extensions
{
public static T? GetOrNull<T>(this Null<T> This) where T : class => This.HasValue ? This.Value : null;
public static T? GetOrNullStruct<T>(this Null<T> This) where T : struct => This.HasValue ? This.Value : null;
public static TProp? IfNotNull<TValue, TProp>(this Null<TValue> This, Func<TValue, TProp> getProp) where TValue : notnull where TProp : class
{ if (This != default) return getProp(This.Value); return null; }
public static TProp? IfNotNullStruct<TValue, TProp>(this Null<TValue> This, Func<TValue, TProp> getProp) where TValue : notnull where TProp : struct
{ if (This != default) return getProp(This.Value); return null; }
} Basically, almost everything C# and .NET do today behind the scenes for |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Nullable unconstrained generic types
Summary
[summary]: Implement nullable unconstrained generic types, like
class Foo<T> { T? Bar() { ... } }
Motivation
[motivation]: This would get rid of the CS8627, and practically, allow the creation of methods such as FirstOrNull on generic collections, among other things.
Detailed design
[design]:
A new type,
UniversalNullable<T>
, is added to the framework:Whenever the C# compiler encounters any usage of
T?
, and T isn't constrained withclass
,struct
, or a base type, it substitutes the aboveUniversalNullable<T>
type. All "get" operations are wrapped in a.Value
property get (optimizer to inline the getter body); "set" operations are replaced with eithernew UniversalNullable<T>()
ornew UniversalNullable<T>(value)
, depending on whether the value is null. All this can be mostly copied from theNullable<T>
implementation for structs. Then we can write code like this:Drawbacks
[drawbacks]: This obviously introduces a minor performance reduction for reference types, since it has the field referencing overhead. (For struct types, it's a given, similarly to Nullable.)
Alternatives
[alternatives]: Leave things as is, keep generating CS8627. When type safe
FirstOrNull
methods are needed, implement two versions, one for classes and one for structs.Unresolved questions
Design meetings
Beta Was this translation helpful? Give feedback.
All reactions