You can take a look at how compiler lowers this code:
int? a = 3;
Sample<int> sampleA = a;
into this:
int? nullable = 3;
int? nullable2 = nullable;
Sample<int> sample = nullable2.HasValue ? ((Sample<int>)nullable2.GetValueOrDefault()) : null;
Because Sample<int>
is a class its instance can be assigned a null value and with such an implicit operator the underlying type of a nullable object can also be assigned. So assignments like these are valid:
int? a = 3;
int? b = null;
Sample<int> sampleA = a;
Sample<int> sampleB = b;
If Sample<int>
would be a struct
, that of course would give an error.
EDIT:
So why is this possible? I couldn't find it in spec because it's a deliberate spec violation and this is only kept for backwards compatibility. You can read about it in code:
DELIBERATE SPEC VIOLATION:
The native compiler allows for a "lifted" conversion even when the return type of the conversion not a non-nullable value type. For example, if we have a conversion from struct S to string, then a "lifted" conversion from S? to string is considered by the native compiler to exist, with the semantics of "s.HasValue ? (string)s.Value : (string)null". The Roslyn compiler perpetuates this error for the sake of backwards compatibility.
That's how this "error" is implemented in Roslyn:
Otherwise, if the return type of the conversion is a nullable value type, reference type or pointer type P, then we lower this as:
temp = operand
temp.HasValue ? op_Whatever(temp.GetValueOrDefault()) : default(P)
So according to spec for a given user-defined conversion operator T -> U
there exists a lifted operator T? -> U?
where T
and U
are non-nullable value types. However such logic is also implemented for a conversion operator where U
is a reference type because of the above reason.
PART 2 How to prevent the code from compiling in this scenario? Well there is a way. You can define an additional implicit operator specifically for a nullable type and decorate it with an attribute Obsolete
. That would require the type parameter T
to be restricted to struct
:
public class Sample<T> where T : struct
{
...
[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(T? value) => throw new NotImplementedException();
}
This operator will be chosen as a first conversion operator for nullable type because it's more specific.
If you can't make such a restriction you must define each operator for each value type separately (if you are really determined you can take advantage of reflection and generating code using templates):
[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(int? value) => throw new NotImplementedException();
That would give an error if referenced in any place in code:
Error CS0619 'Sample.implicit operator Sample(int?)' is obsolete: 'Some error message'