Replies: 27 comments
-
I wonder if this proposal could be extended to a more general feature of having any For example: I previously proposed something to work alongside the Nullability Analysis feature where a boolean field or property could indicate whether a different field was I think there may be some value in considering both of these as part of a larger whole. |
Beta Was this translation helpful? Give feedback.
-
According to the spec, out arguments are always definitely assigned after the call, wouldn't it be a breaking change to make that conditional (at least for existing APIs)? I think we could have an analyzer to detect " |
Beta Was this translation helpful? Give feedback.
-
The proposal here is intended to supersede the described behavior. In other words, for any case where you would use a Boolean value to indicate if something is null, you could better represent the case to the compiler using the feature proposed here.
I can think of other features which could benefit from similar expansions to the compiler's understanding of values in code, but none nearly so pressing as the one intended to be addressed by this proposal. This proposal is meant to cleanly address an immediate concern for another highly-anticipated feature (nullable reference types) without limiting the ability to create similar expansions in the future.
The feature would be opt-in, presumably as part of nullable reference types. |
Beta Was this translation helpful? Give feedback.
-
@alrz Thanks for making me think of the implementation. I left that out of the original proposal; it's not updated with examples. |
Beta Was this translation helpful? Give feedback.
-
@MadsTorgersen I believe this proposal (or one like it) would strongly complement #36 for overall usability/expressiveness/program correctness. Since it would be particularly difficult to implement this change independently of the work on that feature, I'd like to bring it up for discussion in that context. |
Beta Was this translation helpful? Give feedback.
-
@stephentoub This feature as currently described takes an all-or-nothing approach. If it were possible to say that one or more return values were not considered "Conditionally-assigned variables", then it would be possible to implement a move method (related to dotnet/roslyn#160) that disallows the use of the source argument until it is reassigned - similar to the |
Beta Was this translation helpful? Give feedback.
-
What you are describing is already handled, far more elegantly, in other languages using discriminated unions: Maybe<int> Example1(object arg) => arg is int x ? x : None;
...
if (Example1(someObj) is int i)
// do stuff with i This "conditional out" solution seems an inelegant fudge in comparison with adding DU support to C#. |
Beta Was this translation helpful? Give feedback.
-
@DavidArno In your example, the |
Beta Was this translation helpful? Give feedback.
-
I disagree. As is noted in the Nullable reference types in C# proposal, "Certain patterns on generic types, such as
With DU's, all problems of not being able to use |
Beta Was this translation helpful? Give feedback.
-
@DavidArno Nevertheless, the fact remains that
|
Beta Was this translation helpful? Give feedback.
-
So you're saying I have to change a simple property-access to a method with an |
Beta Was this translation helpful? Give feedback.
-
@Joe4evr Yes and no. To use this feature, yes, it would need to change. However, this feature is not intended to provide benefits for properties because this case is already fully covered by nullable reference types: instead of returning a type |
Beta Was this translation helpful? Give feedback.
-
I think I didn't understand the proposal as a language feature at the first glance, so having these: [DAV]
static bool Is(this SyntaxNode node, out BlockSyntax result)
=> node.IsKind(SyntaxKind.Block, out result);
[DAV]
static bool IsKind<T>(this SyntaxNode node, SyntaxKind kind, out T result)
where T : SyntaxNode
{
if (!node.IsKind(kind))
return false;
result = (T)node;
return true;
} You'd get def assignment errors. if (!node.Is(out BlockSyntax block)) {
// block is not definitely assigned here
} Pretty neat. The fact that you can leave I wonder if the compiler could actually leave |
Beta Was this translation helpful? Give feedback.
-
@alrz You'd need to write the [DAV]
static bool IsKind<T>(this SyntaxNode? node, SyntaxKind kind, out T result)
where T : SyntaxNode
{
if (!node.IsKind(kind) || !(node is T value))
return false;
result = value;
return true;
} |
Beta Was this translation helpful? Give feedback.
-
No, this would break compatibility with old code and code that does not opt-in to nullable reference type analysis. |
Beta Was this translation helpful? Give feedback.
-
This proposal would never be used on any existing |
Beta Was this translation helpful? Give feedback.
-
@gafter When nullable reference types are enabled via an explicit user option:
Either way you have a breaking change. With this proposal, at least the break leads to a meaningful, usable result with a proper minimizing of the amount of changes users need to make in order to adopt the feature. |
Beta Was this translation helpful? Give feedback.
-
Maybe I'm missing something, but I don't see how this would help nullable reference types as a whole. Definite assignment and nullability are two different concepts. As an
What you appear to be describing by "definitely assigned" is "definitely assigned a non-null value", or "definitely assigned a non-default value". These are two separate things and this is where I start to get confused. Regarding nullability, it's quite complex. Assuming here three things from the nullability proposal / how Swift does it today:
If I have
What should DAV do in this situation? Furthermore, if I have
Given this, the simplest (although still not perfect) solution for me would simply be:
Alternatively:
A completely different options is new API if dictionary is of type
However I believe the problem of how to deal with "when |
Beta Was this translation helpful? Give feedback.
-
Alternatively we could say that once the method returns true, it is non-nullable, for example: object? o = null;
if ( o != null ) M(o); // OK
M(o); // possible null dereference vs if (map.TryGetValue(str, out var value)) {
M(value); // OK
}
M(value); // possible null dereference We should somehow correspond the method return value to out parameter nullability down the rabbit hole. |
Beta Was this translation helpful? Give feedback.
-
As an aside, from the last time I tried, an unconstrained type can't be nullable. TryGetValue would give you var map = new Dicitonary<string, string>(); // both non-nullable
map.TryGetValue(key, out var value); // value is TValue=string but it's actually nullable! |
Beta Was this translation helpful? Give feedback.
-
The proposal works fine with nullable types as well. For this case, it says when
This solution is the simplest for authors of the nullable reference types feature. On the other hand, it is a nightmare for consumers for two reasons:
The check if ((dictionary.TryGetValue(key, out var value) | true) && value != null)
This requires all code using
Are you referring to the difference in meaning of
This is not what I am intending to describe.
This proposal doesn't change whether or not a value is assigned to a parameter at runtime before a method returns. It changes the conditions for use of that value such that it may only be used if the compiler knows the type at runtime is valid according to the statically-declared type of the method. |
Beta Was this translation helpful? Give feedback.
-
This would eliminate the ability to use this feature on |
Beta Was this translation helpful? Give feedback.
-
@sharwell what's the use of DAV on Also, if I'm re-reading this correctly, this suggests a convention-based approach, rather than a decorated (attributes) approach? If so, this would break existing code such as In general, for nullability this is a tricky thing to define well. Even in Code Contracts it's quite cumbersome and hard to read with |
Beta Was this translation helpful? Give feedback.
-
This one is a bit more tricky, which is why I left it out of the original examples. We probably wouldn't decorate this method specifically, but users may find it valuable to decorate deserialization helper methods (wrappers) with this attribute in order to catch cases where code assumes that a value is meaningful, but in reality it was not. This feature is not intended to avoid cases where methods have unspecified/undefined semantics. It is intended to avoid cases where code uses a value which doesn't have the desired meaning in context. For example, while When looking at the nullable reference types feature, it's important to remember that null is not inherently bad. Null reference exceptions are just a symptom of a broader class of programming errors related to the use of variables that do not have meaningful data. Null is nothing more than the most frequent actual value that arises in cases where bugs of this form manifest. Both the feature I am proposing and the nullable reference types feature expand the compiler's understanding of what it means for a variable to hold a "valid value":
At this point, I have not specified the syntax for these decorations, but they would certainly need to be explicit at the point of definition. Also note that I only covered one specific case at this point. Your example of an inverted Boolean is one case that is not currently supported by this feature. Other cases include HRESULT and BOOL in interop signatures. While it is not the intent of this proposal to address those (they are secondary and far behind to the immediate concerns posed by nullable reference types feature), this proposal should not preclude the ability to address those in the future.
I agree. It's an area I've been looking at for a long time and never been particularly happy with the outcome. FWIW, I actually find this proposal to be one of the more elegant solutions. The key insight for me was realizing that the return value isn't indicating "non-null", but in reality was indicating "not meaningful in the context where the method was called". |
Beta Was this translation helpful? Give feedback.
-
I searched the Roslyn code base for tl;dr: For Roslyn.sln, this proposal appears to behave superbly for the practices followed by the development team, minimizing the overall code change requirements of nullable reference types without degrading the ability of the feature to detect bugs related to the use of invalid or uninitialized data. Positive impressionsOverall, the vast majority of code appears to fall into the following "buckets":
Areas of concernHere are some potential cases where the behavior from this proposal would likely not due what a user was hoping for. Assertions in test codeExample: Assert.True(map.TryGetValue(key, out value));
Assert.Equal(10, value); Potential solutions:
Intentional treatment of
|
Beta Was this translation helpful? Give feedback.
-
@sharwell I don't understand this code: if (_typeDefs.TryGetValue(def, out var index))
{
handle = MetadataTokens.TypeDefinitionHandle(index);
return true;
}
else
{
handle = MetadataTokens.TypeDefinitionHandle(index);
return false;
} I thought the idea here is that |
Beta Was this translation helpful? Give feedback.
-
@svick It was a mistake on my part. I meant to use |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Proposal
I propose extending the language support for definite assignment to allow an API to declare a variable is "conditionally assigned". In its simplest form, consider the following expression:
The compiler already understands that
someType
is only definitely assigned on code flow paths where the expression evaluates to true. Now consider the same operation as an API method:C# currently treats
someType
as always assigned in this case. For such an API, the compiler would be better able to detect classes of use errors at compilation time if it knew that thesomeType
instance should only be used ifwasInstance
is true, in the same way the compiler handles theis
operator currently.Definite assignment variable
The definite assignment variable must be a value of type
bool
returned by the method. It may be any of the following:GetResult
method of the awaiterref
out
📝 Currently the compiler is unable to follow the flow of an
is
pattern expression when the result of the type check is stored in an intermediate variable:Until such time as this flow analysis changes, the only usable form of the feature proposed in this issue will be cases where the return value itself is the definite assignment variable.
Conditionally-assigned variable
When a method with a definite assignment variable in the signature returns, all other returnable values are called conditionally-assigned variables. These variables are definitely assigned after the method returns if and only if the compiler can statically determine that the definite assignment variable returned is true on that code path.
Implementing methods with a definite assignment variable
When implementing a method with a definite assignment variable in the signature, at the point of return from the method one of the following must be true:
false
. Any other write-only return value which is not definitely assigned under the old control flow rules is assigned the valuedefault
prior to returning.TryGetValue
.)Examples
Motivation
I believe this feature will provide APIs like
IDictionary<TKey, Value>.TryGetValue
with a semantically precise way to cooperate with nullable reference types. I was particularly unhappy with two of the "obvious" possibilities for the signature:TryGetValue(TKey, out TValue)
:TryGetValue(TKey, out TValue?)
:Ignoring the issues of what happens when
TValue
is unconstrained (and potentially nullable itself), this signature unnecessarily complicates the usage of the API. In particular, the following idiomatic code would no longer compile without warnings:Beta Was this translation helpful? Give feedback.
All reactions