I found out DateTime data types cannot be null.
Your questions show that you have a pretty vague idea about how the whole system works. Let's start by picking apart what you mean by "DateTime data types cannot be null". I think what you mean here is that the null literal cannot be implicitly converted to a value of DateTime, and hence the null literal cannot be assigned to a variable of type DateTime.
That is:
DateTime d = null;
is not legal in C#. The null literal cannot be converted to DateTime, and so the initialization must fail at compile time.
I was told this is because they are value types...
That is almost correct.
There are two kinds of value types: nullable value types and non-nullable value types. A nullable value type always has an "underlying" type. The underlying type is always a non-nullable value type.
It is always legal to implicitly convert a null literal to a nullable value type.
It is almost always illegal to implicitly convert a null literal to a non-nullable value type; the only time this happens is if someone creates a user-defined conversion operator on the non-nullable value type, and that user-defined conversion operator takes an argument to which the null literal is implicitly convertible.
... and placed on the stack (not on the heap).
This is completely and utterly wrong. Whomever told you that does not know what they are talking about. This makes no sense whatsoever. Whether a null literal is convertible to a type or not at compile time has nothing whatsoever to do with where in memory the data is going to be stored at runtime.
I can understand why the stack is quicker and why value types need to (mostly) be placed on the stack
Then you are misunderstanding something. Values of value types need not be "mostly" placed on the stack.
Values of value type, and references to values of reference type, can sometimes be put on the short term allocation pool when it is known at compile time that the lifetimes of the storage for those values is going to be short. At runtime, that short term pool might be realized as stack locations or registers.
Whether data goes on the short term pool or the long term pool has nothing whatsoever to do with whether the data is a value of value type or a reference. They are not called the "value pool" and the "reference pool". They are the short term pool and the long term pool. Obviously which pool is used has only to do with the answer to the question "do we know ahead of time when we are going to be done using this value or reference?" If the answer is "yes", then the value or reference can go on the short term pool. If the answer is "no", then it has to go on the long term pool.
Is it true the real limitation of why a value type cannot be null is because the stack cannot handle null values?
No, absolutely not. That makes no sense.
What is the real limitation?
Let's look at an easier example than DateTime. Let's look at int. An int is four bytes, no more, no less. Suppose you have a variable of type int. It takes up four bytes. It might take up four bytes on the heap, four bytes on the stack, four bytes in a register -- doesn't matter. It is four bytes of storage, somewhere.
There are 232 possible values you can put into an integer: 0, 1, -1, 2, -2, ... and so on. When you say:
int x = null;
what value do you suppose is going to go into those four bytes of storage? There are only 232 possible values of four bytes, and they are already taken up by the numbers! You can't put six pigeons into five pigeonholes and still have only one pigeon in every pigeonhole. You can't put 232 + 1 values into 32 bits; you need at least 33 bits, and that's one more bit than we've got available.
Non-nullable value types cannot be null because there are exactly as many possible values of a value type as their bits are capable of holding. A distinct "null" value requires one more value than the maximum number the bits are capable of holding.
So how then do we do it with references? Simple; one of the references is reserved as "special"; the reference whose bits are all zero is set aside as never a valid reference. With ints we have a requirement that all 232 values be valid; with references implemented, say, as 32 bit pointers, we have no requirement whatsoever that all 232 possible pointers be valid; the vast majority of them are invalid.
Why don't we do that with value types? Because then:
int x = null;
is exactly the same as
int x = 0;
and it seems bogus to say that "null" just means "the zero of this type". Null should be a distinct value from every other value. (If you want to get the "zero" of a value type you can always say default(T)
where T is the type. So int x = default(int);
is the same as int x = 0;
)
So how then do we do it with nullable value types? We add more bits. When you say:
int? x = null;
int? y = 123;
that is exactly the same as saying:
int x = 0;
bool xHasValue = false;
int y = 123;
bool yHasValue = true;
When you use a nullable value type, you are doing nothing more than passing around a non-nullable value type along with a flag that says whether it is null or not.
I understand in .net we often don't care about exact memory allocation, yet it is important in performance applications to take the stack and heap into consideration, so I'm trying to wrap my head around this.
That is important in some scenarios, yes. But it has nothing whatsoever to do with the question of why the null literal cannot be assigned to a non-nullable value type.
are nullable types hence placed on the heap or stack?
Values of nullable value types are placed wherever the compiler thinks best; they are not different from any other value of any other type.
If the compiler knows that a value will never be used again after a short period then the compiler generates code to place the value on the short-term storage. At runtime, this will be realized as a location on the stack or a register. If the compiler does not know that for sure then it generates code to place the value on the long-term storage heap.
Again, where the storage for a value is located has nothing whatsoever to do with the type of that value. It has only to do with the answer to the question "do I know for sure how long this value needs to hang around?" If the answer is "yes, and it is a short time", then the storage goes on the short-term pool. If the answer is "yes, and it is a long time" or "no, I don't know", then the storage goes on the long-term pool.