Replies: 8 comments 11 replies
-
This seems like a niche concern that can easily be solved in existing code; I don't see how it reaches the required threshold for consideration as a language feature. If implemented, I foresee that it would seldom be used because the required behaviour for partially initialized objects would vary from team to team, from project to project, perhaps even from form to form within a specific project. It's therefore highly likely that any specific implementation would only handle a small fraction of cases. Why is a library solution inappropriate? I had a quick play using SharpLab and was able to quickly knock together this example: public class C {
private Optional<int> _age;
public int Age
{
get => _age;
set => _age = value;
}
public bool HasAge => _age.HasValue;
} The definition of public struct Optional<T>
{
private bool _hasValue;
private T _value;
private Optional(T value)
{
_hasValue = true;
_value = value;
}
public bool HasValue => _hasValue;
public static implicit operator T(Optional<T> optional)
{
if (!optional._hasValue)
{
throw new InvalidOperationException();
}
return optional._value;
}
public static implicit operator Optional<T>(T value)
{
return new Optional<T>(value);
}
} FWIW, wrappers like this are one of the very few places where implicit type casting is really useful and seldom causes grief. |
Beta Was this translation helpful? Give feedback.
-
@sjb-sjb How often have you actually needed something like this for it to warrant a language feature? As a senior C# programmer having more stuff that will throw exceptions at runtime is ingredient for bad design. If I would see code like this in our company's codebase it would need a REALLY good reason blow up at runtime if it doesn't it will need to be refactored. |
Beta Was this translation helpful? Give feedback.
-
Well, I need it in most cases where the domain property is not nullable but it needs to be entered on the GUI. This is practically everything (FirstName, LastName) except where the domain design says that the value is optional (MiddleName). As I discussed above, the two obvious alternatives in the language currently for dealing with such a situation are both very unsatisfactory. @theunrepentantgeek interesting idea. A comment though: Optional<T*gt; in your example is similar to Nullable<T>, in other words what you have there is a starting point very similar to the Wheel example that I gave but using a value type (int) instead of a reference type (Wheel). A conversion of a null Nullable<T> to a T would also throw an exception similarly as Optional<T> does. Looking at it, you are proposing a workaround by defining HasAge, similar to the FrontLeftIsInitialized workaround in my discussion. The point of the proposal is that we should not be forced into such workarounds. The invention of non-nullable reference types has enabled us to express nullability in the language in a way that we were not able to before. We need to continue adding language support for using non-nullable types. Specifically in this proposal I am suggesting a way to express the relationship between a non-nullable property and a nullable backing field. It is a fundamental relationship that has hardly any language support at all currently. The proposed 'req' and 'init' keywords do provide some support for this relationship but they focus only on the relationship at construction time (initialization at construction). The 'is' keyword would address the relationship between non-nullable properties and nullable storage at run time (late initialization). Also @wanton7 I know there are various philosophies on how to deal with incorrect calling code, whether to be resilient and respond as best possible or whether to blow up fast and loud so that the caller has to fix their code. Personally I'm in the latter camp. However this is a separate issue from what I am trying to address here. Potentially one could change the proposed language feature so that the "if uninitialized" action was configurable. |
Beta Was this translation helpful? Give feedback.
-
In our company's code bases we also separate models used in API and models used internally or just use API models properties are methods parameters. Actually this is one place #3630 would help by marking properties required if you add new property to class you use internally then compilation would fail if you forgot to copy it. In those API models we define only properties that are optional nullable. Because ASP.NET Core model binding already supports C# 8.0 nullable reference types we don't need to check them for null https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-3.1#required-attribute Non nullable reference types behave like they have [Required] attribute. |
Beta Was this translation helpful? Give feedback.
-
I'm surprised no one's mentioned it yet, but source generators seem like a good fit here? They can easily generate the property if you define the backing field, the getter can throw whatever donain-specific exception you need, and it can generate a suitable There's an example if implementing INPC with a source generator in the cookbook: this would be very similar. |
Beta Was this translation helpful? Give feedback.
-
[MemberNotNullWhen("IsFrontLeftInitialized", true)]
public Wheel? FrontLeft { get; set; }
public bool IsFrontLeftInitialized { get; set; } iirc this will be valid syntax which solves your use case, I think? |
Beta Was this translation helpful? Give feedback.
-
The main thing I'm not understanding is what is so ugly about You want to be able to do the following things -
Therefore you need a:
That seems like really good clear code to me - it provides a clear, straightforward easily understood API which provides exactly the operations you need to do. Now you could provide some language magic to allow you to do 3, and hide the necessary property from you, but why on earth would I prefer magic over clear code? If your complaint is it's too much boilerplate to write, then I think source generators are a great solution to that. |
Beta Was this translation helpful? Give feedback.
-
It’s funny how something that seems like a no-brainer to one person seems opaque and excessive to others. Look at the effort being made, and rightly so, to eliminate the line that declares the backing field by introducing the ‘field’ keyword. To me, if we believe that nullable backing stores for non-nullable properties are a common pattern then this proposal is not very different in terms of the saving on boilerplate and increased encapsulation. When you look at the long discussion about the challenges of having a full type system for non-nullables and the decision to use annotations instead of types, a fair bit of it revolves around the fact that if you have non-nullable reference types then a lot of problems are introduced by the fact that they cannot be allocated in arrays, properties and elsewhere because there is no default value for them. For people who want to use nullables as if they were types to the extent possible, there are only two solutions to such situations. One is you can organize all of your code around the principle of initializing everything at construction time. The proposals for ‘req’ and ‘init’ address this approach. The other is you can let the non-nullable be created with a null backing value and then make sure it is initialized before use. The ‘is’ proposal is meant to address that approach. In other words, unless you want to say that the language should push everyone to initialize every non-nullable property at construction time, you have to accept that many people will want to initialize them after construction time. Then how are we to support that common situation? The ’is’ accessor does so. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
Features are proposed to support nullable backing fields for non-nullable properties ("late initialized properties"). The keyword 'is' would be used in addition to 'get' and 'set' (and the proposed 'init' and 'req') to signal that the backing field should be nullable. There would be appropriate changes to the behavior of 'get' when 'is' is specified.
This proposal follows on discussion of 'req' in #3630 and would co-exist well with other Property-related proposals such as 'init' and the 'field' keyword (#140).
Motivation
The proposed 'req' and 'init' keywords address the need for flexibility in initializing non-nullable properties, especially non-nullable reference properties, during construction and related initialization stages. This provides a compile-time enablement of deferred initialization and required initialization. However, there are many scenarios where initialization cannot occur early in the object lifetime i.e. at or near construction time.
An example is an object whose meaning in the domain / model design involves required properties, but where the operational process for initializing those properties involves a UI / user interface design. The properties need to remain null until they are filled in on the UI. It would not be appropriate to make all properties nullable in the domain model simply to accommodate a UI process. On the other hand it would be too laborious to create a separate object with nullable properties for use in the UI and then have to copy the values into the 'real' object. What makes sense in such scenarios is for the properties to remain uninitialized at construction time and then be initialized (and then validated) during the UI interaction.
Nullable backing fields for non-nullable properties are an excellent way to deal with these "late initialization" scenarios. Here is a simple example of a property we can use for discussion:
So far so good. Once the property is initialized by assigning through 'set', the domain model is complete in the sense that the property can be used "normally" from then on. Prior to the late initialization, the result of 'get' should be undefined and correspondingly this design would throw an exception.
However, there is a significant gap in this design because there is no encapsulated way to test whether or not the property has been initialized. This is a real problem because you cannot call 'get' in order to test whether or not the property has been initialized -- you would get an exception. And you do need to test for initialization as part of validating the object. An incredibly ugly solution would be to add
The purpose of the proposal is to enable better use of such properties before the late initialization has occurred, by adding an 'is' accessor that tests the nullable backing field for null.
Proposal
The 'is' keyword would be introduced in a syntactic position similar to the existing 'get' and 'set' keywords. (This would not conflict with other syntactic uses of the 'is' keyword. }
The body of 'is' would return a boolean value. In custom properties, this would be defined by code provided in a boolean-valued expression or block following the 'is' keyword -- for example this user-provided code could test whether a backing DependencyProperty is null.
In auto-implemented properties where 'is' is present, the backing store would be a nullable field defined as follows:
3a. If the property has a reference type, then the backing store would be the corresponding nullable reference type (in #nullable disable environment, therefore, the backing store would have the same type as the property while in #nullable enable it would have the same type but be marked as nullable).
3b. If the property has a value type T then the backing store type would be Nullable<T>,; except that if T is already Nullable<S> then the backing store would be just T itself. As with reference types, then, the backing store type is a nullable version of the property type, unless the property type is already nullable in which case the backing store type would be the same as the property type. While it would be legal to include 'is' on a property that is already nullable, there would be little reason to do so.
In auto-implemented properties where 'is' is present, the body of 'is' would return true if and only if the backing store is not null. In custom properties where 'is' is present, as noted above the boolean result would be defined by user code.
In auto-implemented properties where 'is' is present, the 'get' method would throw an UninitializedPropertyException if the backing field is null. Note this would apply both for reference types and for value types. In custom properties where 'is' is present, the compiler would check that the value returned by 'get' is definitely assigned, and if the property is a reference type or a nullable value type then run-time behavior of 'get' would be modified to throw an exception if the return value is null. The purpose here is to pretty much require the programmer of the custom property to throw an exception (or have it thrown for them) if the backing store is null.
A new expression would be introduced to execute the 'is' accessor. There are various ways that this could be done, at this time my suggestion would be (in the example above) "car.FrontLeft is null" to test whether the 'is' accessor returned false. Alternatives would be along the lines of either "car.FrontLeft.is" which is not very aesthetic, or "car.FrontLeft != null" which is convenient but introduces obvious problems, or even possibilities like "car.FrontLeft !null" which looks pretty funny at first but actually would be more convenient to write than !(car.FrontLeft is null).
In terms of nullability inferencing, use of the the 'is' accessor described in 6 would not result in the inference that the property is nullable.
If 'is' is specified then a LateInitializedAttribute would be applied to the property by the compiler.
Interaction With Other Features
A. As relates to 'req' and 'init', the 'is' operator could be specified in addition to these or without these. However there would be limited value to specifying both. If you specified both 'is' and 'init', the effect would be that the property could only be initialized in the construction phase; it would be an immutable property so after the construction phase the result of 'is' should not change and 'get' would either always return the immutable value of the property or would always throw. Adding the 'req' keyword would result in 'is' always returning true and 'get' always returning the immutable value.
B. As relates to the proposed 'field' keyword, there would be no special interaction with 'is' other than the fact that 'field' could be accessed within the body of 'is'.
C. Concerning the use case of UI initialization and the need for data validation, I believe the reference implementation of RequiredAttribute uses the 'get' accessor to test whether the value is null or empty. This would not work well with a late-initialized property because the 'get' accessor could throw an exception. The attribute could be modified to first test for the LateInitializedAttribute and if it is present then use the 'is' accessor instead of the 'get' accessor. If this is not deemed sufficiently backwards-compatible then an alternative would be to introduce a new InitializedAttribute derived from ValidationAttribute. However in this case one would also have to do something to guarantee that if both [Initialized] and [Required] are specified then [Initialized] is tested first.
Unresolved Points
Beta Was this translation helpful? Give feedback.
All reactions