We started this discussion with an email with a proposal based on follow-up research from the previous meeting.
Allowed inputs and outputs:
First of all, I’ll try to make rules based only on “allowed inputs” and “allowed outputs” of a property. I’ll use shorthands ?, ! and T for “nullable”, “nonnullable” and “unknown – depends on T” respectively.
For all the different sensible combinations of attributes, here are the “allowed inputs” and “allowed outputs”:
Allowed input Allowed output string ! ! [AllowNull]string ? ! [NotNull]string? ? ! [MaybeNull]string ! ? [DisallowNull]string? ! ? string? ? ? [DisallowNull][NotNull]T ! ! [NotNull]T T ! [AllowNull][NotNull]T ? ! [DisallowNull]T ! T T T T [AllowNull]T ? T [DisallowNull][MaybeNull]T ! ? [MaybeNull]T T ? [AllowNull][MaybeNull]T ? ?
There should be no surprises to anyone there. Now let’s use these to define the different behaviors around properties: Ordering of states: T is stricter than ? and ! is stricter than both T and ?. This is a measure of relative permissiveness of states.
Initial state: The initial state of a property is its allowed output. This corresponds to us knowing nothing about the property yet, beyond what it tells us through a combination of its type and its postconditions.
State after null check:
- On the non-null branch of a null check the state of the property is !.
- On the null branch of a pure null check the state of the property is ?.
- Elsewhere the state of the property is unchanged.
These rules reflect the general benefit of a null check, as well as the overriding effect of a pure null check even of a nonnull property.
Note that this rules out "dangerous" properties, meaning properties that may change outside the scope of the nullable analysis, as in fields which may be changed by a different thread, and thus the null check is unreliable. We don't consider this scenario to be in scope of our current design and if we decide to address this, we must create a new attribute or some other mechanism.
There's also some problem with the state after null checks, namely that the type may not support
the ?
state. For instance, in the following unconstrained generic,
T M<T>(T t)
{
if (t != null)
t.ToString();
return t;
}
we should not produce a warning on the return, since the state should match the legal state of
T
, which is T
, not ?
. Similarly, for non-Nullable value types, the state cannot be ?
after a null check, since the value cannot be null. The rule should use the T
state.
State after assignment:
The state of the property after an assignment is
- Its initial state if the state of the assigned value is at least as strict as the allowed input, but no stricter than the allowed output
- The state of the assigned value otherwise
This rule reflects that a property is expected to “take care of things” when the state of an assigned value is valid as input but not as output. It does so by assuming that the resulting state in such situations is something that’s valid as output.
This is probably the only rule that would differ from the rules for fields, which would continue to always use the state of the assigned value.
Warnings on assignment: A warning is yielded if the state of the assigned value is less strict than the allowed input. This is the same rule as all other input positions.
We like this new "state after assignment" rule and think it can be implemented now.
However, when looking into the solution, we found that this is the current behavior for properties when annotated:
using System;
using System.Diagnostics.CodeAnalysis;
#nullable enable
class C<T> where T : class?
{
public C(T x) => f = x;
T f;
T P1 { get => f; set => f = value; }
[AllowNull] T P2 { get => f; set => f = value ?? throw new ArgumentNullException(); }
[MaybeNull] T P3 { get => default!; set => f = value; }
void M()
{
P1 = null; // Warning
P2 = null; // No warning
P3 = null; // Warning
f = P1; // No warning
f = P2; // No warning
f = P3; // BUG?: No warning!
}
}
That last line does not look right. Similar to default(T)
, you could be producing a
potentially nullable value, when the substituted type may not permit it. We think
the three state domain outlined above will solve the problem, but that would
be too extensive to change to perform in such a short period of time.
Alternative: whenever you introduce a value (by calling
a property or a method), that produces a generic type annotated with [MaybeNull]
in a substituted generic method, that would produce a warning. This
matches our current behavior for default(T)
.
For example,
T M<T>()
{
_ = (new List<T>).FirstOrDefault(); // this would now produce a warning
}
Conclusion
Let's implement the "state after assignment" rule as defined and implement the "Alternative" proposal outlined above. We will consider updating to use the "three state domain" above later, which may have some further changes. A sample of the expected behavior follows:
Non-Generic
using System;
using System.Diagnostics.CodeAnalysis;
class Widget {
string _description = string.Empty;
[AllowNull]
string Description {
get => _description;
set => _description = value ?? string.Empty;
}
static void Test(Widget w) {
w.Description = null; // ok
Console.WriteLine(w.Description.ToUpper()); // ok
if (w.Description == null) {
Console.WriteLine(w.Description.ToUpper()); // warning
}
}
}
Generic
using System;
using System.Diagnostics.CodeAnalysis;
class Box<T> {
T _value;
[AllowNull]
T Value {
get => _value;
set {
if (value != null) {
_value = value;
}
}
}
static void TestConstrained<U>(Box<U> box) where U : class {
box.Value = null; // ok
Console.WriteLine(box.Value.ToString()); // ok
if (box.Value == null) {
Console.WriteLine(box.Value.ToString()); // warning
}
}
static void TestUnconstrained<U>(Box<U> box, U value) {
box.Value = default(U); // 'default(U)' always produces a warning when U could be a non-nullable reference type
Console.WriteLine(box.Value.ToString()); // ok
box.Value = value; // ok
Console.WriteLine(box.Value.ToString()); // ok
if (box.Value == null) {
Console.WriteLine(box.Value.ToString()); // warning
}
}
}
We have a variety of different use cases and experimental products (C# Interactive Window, Jupyter projects, try.net, etc) that use the current "C# scripting" language, which is already effectively a dialect of C#. There's a fair amount of concern that if adoption continues, we may produce a fracturing of the C# language.
However, adding top-level statements and reconciling scripting in C# proper would be an expensive feature, in both design and implementation. It also doesn't directly impact many of the designs we're currently considering.
But there is also significant cost to doing nothing. We have not considered the semantic for many
features in C# 8, or even if they should work in scripting (using
declarations, notably). There
is a significant ongoing cost here, either in considering all our designs for the scripting
dialect, or in risk that not doing design/implementation work will cause bad experiences for
products using the C# scripting code.
Conclusion
We'll schedule this for 9.0, to at least examine options.
This occupies the same design space as records, which is scheduled for 9.0, so we at least need to consider this feature while implementing records.
Conclusion
Moving to 9.0.
Issue #882
This overlaps significantly with a "is not" pattern. We're not confident this feature has significant value, after the "is not" pattern is implemented.
Conclusion
Move to X.X to consider after "is not" has shipped and see if there are significant use cases that are not addressed by the "is not" pattern.
Issue #1394
The primary use case is (x, y, z) = default;
instead of naming each variable
individually. There are some issues around the written specification, specifically
on what target typing default
has.
Conclusion
From a consistency perspective it seems like this should work, regardless of the complexity in details of the specification. We'll take this Any Time whenever we have a solid specification and implementation.
Issue #1349
Nothing that's related to type inference is a tiny feature, but this is pretty small as type inference changes are concerned. We think the hardest problem will be agreeing on the syntax. Agreed that it could be useful, though.
Conclusion
We'll take this Any Time.
Issue #973
Somewhat related is "sequence expressions". This is useful for declaring variables inline in an expression. There are places where statements are not possible, and this requires refactoring.
Conclusion
We don't think there's value in half measures here. We think going all the way to sequence expressions may have value, but then declaration expressions do not.