❔ Generics and nullable types
This snippet below results in an error
Cannot convert null to type parameter 'T' because it could be a value type. Consider using 'default(T)' instead.I don't really understand the error. I thought the type of
_foo
was T?
, not T
.37 Replies
ah yes, the classic
You're familiar with Rust, right?
indeed
In fact I'm trying to mimic a Rust pattern, which I imagine is why I'm hitting problems.
To me, the C# code is equivalent (more or less) to
but clearly that's not the case since in the Rust case there are no restrictions on what
T
can beT?
in C# is very different from Option<T>
in Rust. In particular, nullable types in C# are really two different things which use the same T?
syntax: nullable reference types, and nullable value types.
Nullable value types use the Nullable<T>
struct, which is a lot like Option<T>
, but it has a where T : struct
constraint which only permits value types to use Nullable<T>
.
Nullable reference types are nothing more than a compiler annotation (for backcompat reasons), since reference types in C# can always be null, so T?
(where T : class
) is the same as T
but with an annotation saying that it may be null.
The consequence of this is that T?
for an unconstrainted T
is not Nullable<T>
but rather just the annotation, and it doesn't do anything for value types.Thinker
REPL Result: Success
Result: <>f__AnonymousType0#1<string, int>
Compile: 567.011ms | Execution: 77.621ms | React with ❌ to remove this embed.
However if you constraint it to
struct
then it'll actually be null (or None
if you wanna look at it like Option<T>
)Thinker
REPL Result: Success
Result: <>f__AnonymousType0#1<Nullable<int>>
Compile: 558.263ms | Execution: 77.528ms | React with ❌ to remove this embed.
Rust is largely unique in that it got to be designed with option types at its core, but C# wasn't, so what we have now is an absolutely inconsistent mess
Just to make sure I understand correctly: this restriction is artificial, right? Like in theory the code could be valid for both nullable value type and reference types, but the C# compiler just currently doesn't allow one to express this?
Well, it's impossible to express a value type as null without
Nullable<T>
, and it's always possible for reference types to be nullIn my case I don't care what the actual type, as long as I can assign null to it. I thought there might be a way to express this constraint
it is not possible
when the generic type is unconstrained,
T?
really means "maybe default," not "maybe null"right
The common 'solution' for this is to have two different variants of whatever thing you're writing, one which is constrainted to
struct
and one which is not, but for a class like this then it's not really practical.The actual code where I'm hitting this is a generic
Peekable<T>
adapter that allows one to add peeking behavior to any IEnumerable
.
The simple way to implement this is to cache the peeked value. The peeked value can be "None" (or null), so yeah.
Since I can't constrain the generic T
to be "something that can be assigned null" and that there is no Option type I'm not sure how to proceed.
that could be a potential solution I imagineUse a
bool TryPeek(out T value)
method
And besides, T
could naturally be int?
or whatever, so null could be a possible value even if the peek succeededoof :/
yeah
int??
isn't possible
Try
methods are the most common alternative to null/option-returning methods in C#Rust has really spoiled me in a way, I find myself hitting walls and limitations left and right.
I know I just have to readjust how I design my programs, but it's not easy 😅
thanks, that's good to know
Yeah Rust is a ridiculously well-designed language
Rust is love ❤️
The fact C# was more or less designed as a Java derivative has really hurt what it wants to be in modern day
Nullable reference types are a hack, and a precarious one at that
Because it's always possible to use
null!
to just make the compiler shut up about nullability differencesYeah from the little I've explored so far it really seems like a mix of well-designed modern stuff and also very outdated ideas.
I rather like C#, but it's not easy for me to make the switch lol. I guess it'll get better as I learn more.
One thing for sure is that I'm grateful for all the help I'm getting. Makes things so much easier and pleasant ❤️
We still have
System.Collections.ArrayList
from C# 1 before generics were a thing
And MS refuses to remove it because there's probably some company somewhere which has some critical code which relies on it
So uh, we have tons of legacy Yeah that's understandable, C# is a big language (technically speaking and also in the industry), and it has a ton of baggage.
I try to focus on the modern core
but yeah, I'm still having a hard time 🙂
I'll try the
Try
pattern now. Thanks.yw tried Rust pretty recently and I can absolutely agree it's hella nice, and coming back to C# afterwards feels the language is missing half of itself.
Rust was my first programming language and the only one where I'm at a professional level with.
I've learned other languages since then of course, but everything else is so different (or rather, Rust is...) so it's pretty overwhelming.
regardless it is really important to note that
int?
is very different from string?
. just, in general
int?
is an actual option type. it's a syntax shorthand for https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1?view=net-7.0. changing int
to int?
makes a very big difference because they are not actually the same thingnoted!
string?
is identical to string
in every way, except for how it impacts the nullability static analysis done by the C# compiler. the annotations have no impact on the runtime representation or behavior. it is simply input to a flow analysis, which determines the points at which a variable may be null (and whether that is OK for the operations done in the code). the analysis is not perfectright. On the other hand, I imagine that for value types this affects the memory footprint, like how int? could actually be 8 bytes instead of 4 so it contains a flag for null.
or something along those lines
in other words:
it's one bool + the actual value
whereas, for
int?
, there is an explicit unwrapping gesture, because it is really a wrapper with a flagright, but because of alignment requirements I believe this would still be 8 bytes
unless then compiler does some magic with packed representations and whatnot
right,
int?
is eight bytesThanks for the explanations @🌈 Thinker 🌈 @reflectronic, now I understand much better and I'll be able to avoid footguns in the future 😄
Was this issue resolved? If so, run
/close
- otherwise I will mark this as stale and this post will be archived until there is new activity.