Proposal: allow generic methods to specify T? without constraining to class or struct #2194
Replies: 45 comments 2 replies
-
This isn't currently possible in C# 8?! |
Beta Was this translation helpful? Give feedback.
-
Not yet :) |
Beta Was this translation helpful? Give feedback.
-
How do you propose that actually work? A method can't selectively change its return value. A generic method has the same signature and body regardless of the generic type arguments, it can't return |
Beta Was this translation helpful? Give feedback.
-
@HaloFour Could it possibly generate 2 methods at compile time, one with a Nullable constrained to struct and one with a nullable reference? I know it would not be valid language code to define two different return types with same function name due to ambiguity, but is that a runtime restriction or a compile restriction? (I know its just a compile restriction for Java as I've done it in the past, not sure about .NET). If its just a compile time ambiguity, then it is at least be possible to do this. |
Beta Was this translation helpful? Give feedback.
-
You're right of course - this isn't just a compiler-level feature, it would require deeper changes in other places. For example, the combination of a I do realize that on the implementation level this is a pretty big/complicated ask, I'm only expressing the need at the language level. I'm mostly concerned that nullability is making things significantly more complicated than before. PS If overloading on generic constraints were allowed (#2013) this would be much less of a problem, but that doesn't seem to be adopted in any way either. |
Beta Was this translation helpful? Give feedback.
-
@DanFTRX the problem with generating two methods is that they would collide - same name with the same parameters... |
Beta Was this translation helpful? Give feedback.
-
@roji I'm aware that it creates ambiguity at compile time, but that can be solved with compiler analytics since it would be constrained this specific situation. I do know from when I worked with Java in the past, it is possible to generate valid bytecode directly for 2 methods same names same parameters with the Java runtime, I assumed since C# is similar in regards to being a compiled runtime language, it might be possible to do it here as well. |
Beta Was this translation helpful? Give feedback.
-
I think your only option would be for the compiler to emit two different versions of the method. The CLR does allow overloads based on different returning types, or even just on the application of |
Beta Was this translation helpful? Give feedback.
-
@HaloFour you're right, that sounds like a more viable approach than anything impacting the runtime. So perhaps generating two methods - one for value types and one for reference types - is the right way here. |
Beta Was this translation helpful? Give feedback.
-
I think that this requires a more configurable approach than what is proposed. Consider that there are also various open generic methods that don't return a [return: OpenNullable(ValueTypeHandling.DefaultValue)] //compiler emits one method and in consuming code ignores the nullability flow if invoked with 'T' as a value type
public T? FirstOrDefault<T>(this IEnumerable<T> sequence);
[return: OpenNullable(ValueTypeHandling.Nullable)] //compiler emits two methods as suggested by HaloFour
public T? ReadValue<T>(); |
Beta Was this translation helpful? Give feedback.
-
@Joe4evr what you're thinking about seems to be covered by dotnet/roslyn#30953? Methods like |
Beta Was this translation helpful? Give feedback.
-
I have no idea how practical this actually is to implement, but I think it is extremely necessary if we want to be able to write truly generic code whilst dealing with nullability. |
Beta Was this translation helpful? Give feedback.
-
If it is not a virtual method, this approach could help |
Beta Was this translation helpful? Give feedback.
-
@roji: the name and parameters would collide yes, but what if type parameter constraint also distinguished? That would neatly separate these two cases. The method with |
Beta Was this translation helpful? Give feedback.
-
@JeroenBos that would be a breaking change, since the constraints are not considered as part of the method signature today. Changing this would make the new language spec only available for a new runtime version. |
Beta Was this translation helpful? Give feedback.
-
The current trend is to make construction simpler. Remember patter matching, for example, or the null coalescing assignment operator. I think that having public static void DoWithNullable<T>(T value) where T : class?
{
}
public static void DoWintNonNullable<T>(T value) where T : class
{
}
public static void Main(string[] args)
{
DoWithNullable<string?>(null); // OK
DoWithNullable<string>("value"); // OK
DoWintNonNullable<string?>(null); // Warning CS8634: Nullability of type argument 'string?' doesn't match 'class' constraint.
DoWintNonNullable<string>("value"); // OK
} The problem here is that there is no way to express the same thing for a struct and for any type (either reference or value). So having |
Beta Was this translation helpful? Give feedback.
-
Just for you information, public static void DoWithNullable<[Nullable(2)] T>([Nullable(1)] T value) where T : class
{
}
public static void DoWintNonNullable<[Nullable(1)] T>([Nullable(1)] T value) where T : class
{
} @jcouv Could you explain the meaning of magic values which |
Beta Was this translation helpful? Give feedback.
-
public static void DoWintNonNullable<[Nullable(1)] T>([Nullable(1)] T value) where T : class
{
} This is incorrect. There is no |
Beta Was this translation helpful? Give feedback.
-
To see it you should do a little trick, add |
Beta Was this translation helpful? Give feedback.
-
ah right, without that it gets emitted as oblivious... and therein lies the answer!
|
Beta Was this translation helpful? Give feedback.
-
@yaakov-h Thanks for answering the question to Julien! Out of curiosity, why is |
Beta Was this translation helpful? Give feedback.
-
No idea. At a guess, to reduce the amount of code that needs to be embedded in the target assembly? |
Beta Was this translation helpful? Give feedback.
-
See https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-05-15.md#unconstrained-t |
Beta Was this translation helpful? Give feedback.
-
@jcouv in this section or the recent nullability LDM, it's stated that "There is therefore no real need for the ability to say T? on an unconstrained T However, the point of unconstrained T? (at least in this issue) is to have |
Beta Was this translation helpful? Give feedback.
-
@roji There are two views of what So far in the nullable feature, annotations do not change emitted/runtime types, so we mostly focused on the latter interpretation. |
Beta Was this translation helpful? Give feedback.
-
@jcouv thanks for the clarification. Yeah, this issue is about The "latter interpretation", where |
Beta Was this translation helpful? Give feedback.
-
I want to bring my perspective on nullable return types, especially When writing generic functional code, some language legacy forces us to write duplicate code. Most notorious example is when we have to implement the same code for From what I see, the different handling of And what I find rather ironic, the new language feature doesn't work well with the established Example 1: (Taken from this StackOverflow question.) Let's say we write an implementation of the Option/Maybe monad and want to provide a convenience method to make it integrate better into existing code by implementing a common .Net pattern... public abstract Option<T> {} // exact implementation skipped as irrelevant
public class None<T> : Option<T>
public class Some<T> : Option<T>
public static class OptionExtensions
{
public static bool TryGetValue<T>(this Option<T> option, out T value)
{
if (option is Some<T> some)
{
value = some.Value;
return true;
}
value = default;
return false;
}
} Once we add NotNullWhenAttribute to But we need both of them. We don't want to narrow it down! Thanks to @omariom for bringing up the workaround. Since the issue #1628 was fixed, we now can have multiple extension methods that differ only in For this particular example it looks acceptable. Just two copies... First, we introduced non-trivial code duplication. I suspect there will often be nontrivial code, not shareable between these extension classes, or otherwise there must be a better workaround... Second, what if we have more than one nullable type in the return values? More examples for that: Example 2: Not going too far from our monads. My own issue after which I started to look around and found the above-mentioned SO question - I wanted to do the same thing with the Either monad. public abstract class Either<L, R>
{
// most of the implementation is skipped as irrelevant
public abstract T Match<T>(Func<L, T> f, Func<R, T> g);
private class Left : Either<L, R>{ /* skipped */ }
private class Right : Either<L, R>{ /* skipped */ }
}
public static class EitherExtensions
{
public static bool TryGetValue<L, R>(
this Either<L, R> either,
[NotNullWhen(false)] out L? left,
[NotNullWhen(true)] out R? right
)
where L : class
where R : class
{
L? tempLeft = null;
R? tempRight = null;
bool result = either.Match(
l => { tempLeft = l; return false; },
r => { tempRight = r; return true; }
);
left = tempLeft;
right = tempRight;
return result;
}
/* Three (3) more copies of TryGetValue<L, R> are skipped for brevity.
Thankfully I can do this in an example code.
Although it takes a lot from the impression I hoped to make... */
} I have two independent types - Example 3: I have a bunch of extension methods to work with nullables as poor man's Option... No public static R? MapNullable<T, R>(this T? item, Func<T, R?> fn)
where T : struct
where R : struct
/* !item.HasValue ? null : fn(item.Value) */
=> item == null ? null : fn(item.Value);
public static R? MapNullable<T, R>(this T? item, Func<T, R> fn)
where T : struct
where R : struct
=> item == null ? (R?)null : fn(item.Value);
public static R MapNullable<T, R>(this T? item, Func<T?, R> fn)
where T : struct
=> fn(item);
// ... few more MapNullable<> variations skipped This time, contents of the functions is trivial. But the amount of them makes it hard to maintain them if I want to have them universal across all class and struct combinations. Some of them have to be copied twice, some - four times... Some - precisely, some - not quite. And it all adds up. Granted, I'm not heavily dependent on these methods and can get rid of them, but the same issue will surface in one shape or another for different people. I'm waiting to run into a problem when there will be a need for a function with a combination of multiple different input types, return/out types, collection kinds to see how big of a combinatorial explosion we can realistically get. It seems like commenters in the issue #2309 also had a short discussion on what can be done about this, but also without any conclusion. This is too much of PITA for programmers to handle this manually. If there is a single point to remove the frustration for a lot of people - it must be done. This issue need to have higher priority and worked on more actively. I hope to see more in-depth discussion about implementation possibilities and roadblocks. For input parameters the challenge seems to be to reconcile some existing API's. We are expected to operate with I'm not a fan of an idea to introduce a The limitation for output types seems to be just a collateral damage from that. Correct me if I'm missing something. If the limitation can be removed just for return types at least - it will be really helpful already. I suspect there are also challenges in the inner workings to make that possible. I would like to hear a competent feedback on that. Is it common for C# compiler to emit multiple copies of a function for different underlying types? |
Beta Was this translation helpful? Give feedback.
-
This seems like it's covered by dotnet/roslyn#29146, closing for now. |
Beta Was this translation helpful? Give feedback.
-
Read that too quickly, dotnet/roslyn#29146 is about expressing the default of T for unconstrained generic types. This proposal is about being able to express a single generic method where T? (or some other syntax) express Nullable for value types, and NRTs for reference types. In other words, T? would refer to two different runtime types depending on whether T is a value or reference type (int? for int, string for string). Reopening even though there seems to be little chance this will be implemented (#2194 (comment)). |
Beta Was this translation helpful? Give feedback.
-
So I assume that it's still not possible to represent nullable unconstraint return type in C# (i.e. Are there any other proposals to allow that? I wasn't able to find anything. |
Beta Was this translation helpful? Give feedback.
-
Currently, specifying
T?
in a generic method signature requires it to be constrained to either a struct or class. The proposal is to allow the following without any constraints:For value types, the method would return
Nullable<T>
as usual, and for reference types it would return a nullable reference for the purposes of compiler analysis.As an example, when looking at a better (and more null-friendly) API for fetching database column values on DbDataReader, I am currently blocked by the inability to define a generic method that returns
T?
, and that can work on both value and reference types. The only alternative would be to define two methods - one constrained onclass
and the other onstruct
- but since overloading on generic constraints isn't supported they must have different names. This would force the user to deal with the value/reference type distinction - figuring out which method to call - and seems like a non-starter for the kind of API we're looking for.More generally, it seems that C# 8 nullability introduces more difficulties when intersecting with the value/reference type divide - developers are unfortunately forced to think about this distinction more than before nullability existed. It seems like it's important to smooth that out (see #1865 in general for diminishing the feature gap). As an example, C# already allows one to use (nullable) references and
Nullable<T>
interchangeably in several contexts: the null coalescing and null-conditional operators accept both. It would be good to go further in this direction.This issue may be a dup of #2146, although that issue seems a bit unclear. See also dotnet/roslyn#30953 which is somewhat related, although that issue is about returning default, whereas here the idea is to allow returning null for both reference and value types.
/cc @divega @ajcvickers
Beta Was this translation helpful? Give feedback.
All reactions