Open redgoldlace opened 3 months ago
Tagging subscribers to this area: @dotnet/area-meta See info in area-owners.md if you want to be subscribed.
Related to https://github.com/dotnet/csharplang/discussions/5337 . ref
field isn't that special comparing to fields of reference types.
This has a particular impact on the newer
Span
andReadOnlySpan
APIs, especially in the context of writing parsing code, where representing the lack of a value is important, and negatively harms the ergonomics and expressiveness of these APIs, making them more difficult to adopt when writing high-performance code.
Really not seeing that, considering you can use the Try
pattern with out
parameter to surface both a ReadOnlySpan<char>
and a bool
indicating whether a token was succesfully extracted during a parse operation. In cases where it was not, you just return default
- aka ReadOnlySpan<char>.Empty
for the out
parameter.
This scenario is only relevant in those situations where you're predisposed to overloading the meaning of null
to be false
, 'error', etc. and the solution in that case is simply to stop doing that.
Background and motivation
Prior to C# 13, the lack of the
allows ref struct
generic bound and the inability to useref struct
s as generic type arguments made it impossible to represent a nullableref struct
.This has a particular impact on the newer
Span
andReadOnlySpan
APIs, especially in the context of writing parsing code, where representing the lack of a value is important, and negatively harms the ergonomics and expressiveness of these APIs, making them more difficult to adopt when writing high-performance code.As well as making it impossible to represent nullable
Span
s andReadOnlySpan
s directly, this also harms the ergonomics of types that containSpan
s,ReadOnlySpan
s or otherref struct
-like types, by indirectly making it impossible to represent a lack of a value. While - in the real world - you would want to simply use aRange
instead, this problem is extremely apparent in the case of a hypotheticalRegexMatch
struct containing aReadOnlySpan
- you cannot have aRegexMatch
-returning method that returnsnull
if no match is found.Ultimately, the arguments for nullable
ref struct
s are the same as the arguments for nullablestruct
s and nullable reference types in the first place; representing a possible lack of a value without using a type-specific "sentinel" value, providing type-safety to downstream code, and catching bugs at compile time rather than at runtime. Since C# supports nullable values in all of these scenarios, a lack of support forref struct
s feels like a hole that should be addressed.As mentioned above, this was broadly impossible to support prior to C# 13. With C# 13 supporting the
allows ref struct
generic bound, there should no longer be anything at the language/runtime level blocking the implementation of this feature.API Proposal
From what I can see, there are effectively two options here.
Option 1: Annotate
System.Nullable<T>
such thatT : allows ref struct
. From everything I understand of the implementation ofallows ref struct
, this would be a breaking change, as it would make all instantiations ofSystem.Nullable<T>
behave according to the restrictions imposed onref struct
s, regardless of whether a specificT
was/was not aref struct
. For this reason, it's likely that this is not a practical implementation choice.Option 2: Implement a new
System.Nullable<T>
-like type forref struct
s. The API for this would be extremely similar to the existingSystem.Nullable<T>
API, and could look vaguely similar to the below:The type
T?
could then be expanded to this hypotheticalSystem.NullableRef<T>
class in the case of aref struct
, andSystem.Nullable<T>
otherwise. Since using aref struct
as a generic type argument requires an explicitallows ref struct
bound, this should not affect existing code that deals with nullable generic types.For usage in pattern matching, it's likely that this would require runtime/language support of some kind, but I'm not entirely sure where. With the API surface and general implementation being so similar, this doesn't seem like it would be a particularly difficult change, and rather just an expansion of existing functionality. That said, looks can be deceiving, so it's possible this is more involved than I realize!
API Usage
Most of the examples here would apply to
System.Nullable<T>
as well - the idea is for things to be as ergonomic as possible, and mirrorSystem.Nullable<T>
where possible.Alternative Designs
See the first option mentioned in the API design section above. This is the most obvious alternative, though the actual implementation of a hypothetical
System.NullableRef<T>
could also differ in some way. A lack of API symmetry would likely be harmful to ergonomics and the ability to work with nullableref struct
s, however.Another notable alternative - though one that's likely far more work! - is adjusting the
allows ref struct
bound such that theref struct
rules are only imposed on genericallows ref struct
types when specifically instantiated with aref struct
, or another generic parameter that isallows ref struct
. As my knowledge of the current behavior is unclear, it's possible that this is already the case - meaning that option 1 would suffice, and was just something that was missed in the initial round ofallows ref struct
additions.Another alternative, though it goes without saying that it would be the least preferable to me, is to simply not support nullable
ref struct
s in the first place.Risks
The risks of adjusting
System.Nullable<T>
were mentioned earlier, but there are some other risks that could arise in downstream code.What immediately comes to mind is the following:
System.NullableRef<T>
, and might not know when to use it overSystem.Nullable<T>
.System.NullableRef<T>
or usingSystem.NullableRef<T>
with a non-ref struct
type. These warnings could suggest usingT?
instead of naming the type directly.ref struct
s, creating friction.This is not an exhaustive list, and just what comes to mind presently. It's possible that a feature like this would have other risks I'm not aware of.