Should non defaultable value types be per instance rather than per type? #3322
Replies: 6 comments
-
My proposal in #99 (comment) covers these cases. |
Beta Was this translation helpful? Give feedback.
-
@sharwell |
Beta Was this translation helpful? Give feedback.
-
I'm not currently convinced that use case 2 is a use case. An analyzer that requires the explicit initialization of fields to their default value results in unnecessary runtime overhead for the type because the runtime provides this initialization for you. |
Beta Was this translation helpful? Give feedback.
-
It requires an explicit initialisation to some value. Not the default value. |
Beta Was this translation helpful? Give feedback.
-
You could use a wrapper struct to force this: [NonDefaultable]
public struct NonDefaultable<T>
{
public T Value;
public NonDefaultable(T value)
{
Value = value;
}
} |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Current discussions about non-defaultable value types imply that they will be on the type level. Certain structs will be marked as being non-defaultable, and it will not be allowed to leave them in an uninitialized state. See #146.
However that's not the way NRTs work. For example the
string
class can always be null. Whenever you use the type you indicate whether you want to allow nulls or not by using?
. e.g.string
is not nullable, andstring?
is nullable. There's no such thing as a class which never allows null instances.Perhaps non-defaultable value types should work the same way?
Strawman
A usage of struct type can be marked as non-defaultable by using the
!
postfix. E.g.public int! P { get; };
.Just like with NRTs, flow analysis will guarantee that an uninitialized struct, or a struct initialized with default, cannot be assigned to a non-defaultable struct.
In order to aid the flow analysis, a new pattern will be added
someStruct is default
. This will be lowered to a runtime helper which simply checks whether the struct is completely zeroed out.This will allow flow analysis along the lines of:
Discussion
Part of the difficulty with this design is there's two different use cases for non-defaultable value types.
Some structs don't 'work' or are special cased when they are in their default state. For example structs which wrap a reference type, will wrap a null reference in their default state. Thus you want to indicate that this particular struct cannot be left in a default state.
For many structs the default case has a perfectly valid value - e.g 0 for numeric types. However you want to make sure you set the value of the struct explicitly as 0, rather than it being left as 0 because you didn't set it in the constructor. I.e. the problem isn't a struct being in the default state, it's that the concept of default exists at all! Many FP languages have no concept of default values. Instead every single instance must be set explicitly, thus avoiding a whole class of bugs.
previous proposal
The previous proposal just tried to address use case 1. You explicitly mark a struct as non-defaultable, and from then on, just like NRTs, if you want to allow a a default instance in some location you explicitly mark it as defaultable in some way.
This had the advantage of working very well for such structs, since now by default they are non-defaultable, and you have to explicitly opt-in to defaultability which is the right way round for such use cases.
On the other hand is suffers from two issues:
Backwards compatability.
ImmutableArray
would be a perfect candidate for a struct where the default case is special cased, but making it non-defaultable now would break a lot of code. Warning waves might help with this.There's no good syntax for a defaultable instance of a non-defaultable value type.
?
already means something else, and I think it would be a terrible idea to special case it for these structs - that would make it completely impossible to change a type to non-defaultable without almost guaranteed source breaking changes.current proposal
My Strawman proposal here tries to balance both use cases but unfortunately doesn't do either brilliantly.
use case 1
To deal with use case 1, you would mark a usage of a struct as non-defaultable. Then whoever called your method would have to check whether their instance of the struct was defaultable before passing it to you.
E.g.
This suffers from two disadvantages:
you have to opt in to non-defaultability, when it should be the other way round. In 90% of cases you want your array to wrap a real struct, not a null instance of one.
This doesn't help with instance methods. There'll be nothing to warn you if you call array.Length from a defaultable arrray. Perhaps attributes could help there, but again that's opt in rather than opt out.
use case 2
To deal with use case 2 you would mark a usage of a struct as non-defaultable, to make sure that someone initializes it with some value before passing it to you.
E,g,
This again suffers from 2 issues:
Again this is probably the wrong way round. There's very few cases where you really really cannot accept something that's in an uninitialized state. Rather in almost all cases, an uninitialized variable is a bug waiting to happen. So you would almost never explicitly mark a variable as non-defaultable, but if every variable was implicitly non-defaultable, we would probably catch a lot of bugs.
There's no way to convert from a maybe-defaultable value to a non-defaultable one. In the example above how could you safely pass x to Foo? Checking
x is default
wont help, because there's no way to distinguish between, "x was explicitly initialized to 0", and "x was never initialized". Instead you have no choice but to pass the buck upwards.Conclusion
This strawman clearly isn't going to cut it as it is. But I think there's definitely more to discuss here, before we settle with the previous proposal.
Beta Was this translation helpful? Give feedback.
All reactions