From e3b88f2b1033203f4aab43af2c831deb37359613 Mon Sep 17 00:00:00 2001 From: Jared Parsons Date: Sun, 13 Jan 2019 18:16:19 -0800 Subject: [PATCH 1/4] Null arg checking proposal Proposal for simplified null argument validation. --- proposals/null-arg-checking.md | 90 ++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 proposals/null-arg-checking.md diff --git a/proposals/null-arg-checking.md b/proposals/null-arg-checking.md new file mode 100644 index 0000000..19aab56 --- /dev/null +++ b/proposals/null-arg-checking.md @@ -0,0 +1,90 @@ +# Simplified Null Argument Checknig + +## Summary +This proposal provides a simplified syntax for validating method arguments are not `null` and +throwing `ArgumentNullException` appropriately. + +## Motivation +The work on designing nullable reference types has caused us to examine the code necessary for +null argument validation. Given that NRT doesn't affect code execution developers still must +add `if (arg is null) throw` boiler plate code even in projects which are fully null clean. This +gave us the desire to explore a minimal syntax for argument `null` validation in the language. While +anticipated to pair often with NRT the proposal is independent of it. + +## Detailed Design +The bang operator, `!`, can be positioned after any identifier in a parameter list and this will +cause the C# compilet to emit standard `null` checking code for that parameter. For example: + +``` csharp +void M(string name!) { + ... +} +``` + +Will be translated into: + +``` csharp +void M(string name!) { + if (name is null) { + throw new ArgumentNullException(nameof(name)); + } + ... +} +``` + +The generated `null` check will occur before any user authored code in the method. When multiple +parameters contain the `!` operator then the checks will occur in the same order as the parameters +are declared. + +The check will be specifically for reference equality to `null`, it does not invoke `==` or any user +defined operators. This also means the `!` operator can only be added to parameters whose type can +have the value `null`. Value types and type parameters not constrained to `class` or `interface` +cannot be used here. + +``` csharp +// Errro: Cannot use ! on parameter of type T. +void G(T arg!) { + +} +``` + +In the case of a constructor that uses `this` or `base` to chain to another constructor the +`null` validation will occur after such a call. + +``` csharp +class C { + C(string name!) :this(name) { + ... + } +} +``` + +Will be translated into: + +``` csharp +class C { + C(string name!) :this(name) { + if (name is null) { + throw new ArgumentNullException(nameof(name)); + } + ... + } +} +``` + +The `!` operator can only be used for parameter lists which have an associated method body. This +means it cannot be used in an `abstract` method, `interface`, `delegate` or `partial` method +definition. + +## Open Issuess +- There is no way to check the `value` argument of a property setter for `null` using this syntax. + +## Considerations +- In the case a constructor chains to another constructor with `this` or `base` the `null` +validation could occur before the chaining call. This is a stronger guarantee the developer could +desire. However C# code cannot be authored that way today and it seems unlikely that it's a +significant issue. Leaving the check after the chaining call means existing code can be ported +to the new pattern without fear of compat concerns. + +## Future Considerations +None \ No newline at end of file From 94eef2a17295973a20c86be985984e041746407e Mon Sep 17 00:00:00 2001 From: Jared Parsons Date: Mon, 14 Jan 2019 13:15:37 -0800 Subject: [PATCH 2/4] Responding to LDM feedback --- proposals/null-arg-checking.md | 154 ++++++++++++++++++++++++++++++--- 1 file changed, 140 insertions(+), 14 deletions(-) diff --git a/proposals/null-arg-checking.md b/proposals/null-arg-checking.md index 19aab56..fa32f76 100644 --- a/proposals/null-arg-checking.md +++ b/proposals/null-arg-checking.md @@ -1,4 +1,4 @@ -# Simplified Null Argument Checknig +# Simplified Null Argument Checking ## Summary This proposal provides a simplified syntax for validating method arguments are not `null` and @@ -12,6 +12,8 @@ gave us the desire to explore a minimal syntax for argument `null` validation in anticipated to pair often with NRT the proposal is independent of it. ## Detailed Design + +### Null validation The bang operator, `!`, can be positioned after any identifier in a parameter list and this will cause the C# compilet to emit standard `null` checking code for that parameter. For example: @@ -48,43 +50,167 @@ void G(T arg!) { } ``` -In the case of a constructor that uses `this` or `base` to chain to another constructor the -`null` validation will occur after such a call. +In the case of a constructor the `null` validation will occur before any other code in the constructor. That includes: + +- Chaining to other constructors with `this` or `base` +- Field initializers which implicitly occur in the constructor ``` csharp class C { - C(string name!) :this(name) { + string field = GetString(); + C(string name!): this(name) { ... } } ``` -Will be translated into: +Will be roughly translated into the following: ``` csharp class C { - C(string name!) :this(name) { + C(string name) if (name is null) { throw new ArgumentNullException(nameof(name)); } + field = GetString(); + :this(name); ... - } } ``` +Note: this is not legal C# code but instead just an approximation of what the implementation does. + The `!` operator can only be used for parameter lists which have an associated method body. This means it cannot be used in an `abstract` method, `interface`, `delegate` or `partial` method definition. +### Extending is null +The types for which the expression `is null` is valid will be extended to include unconstrained type parameters. This +will allow it to fill the intent of checking for `null` on all types which a `null` check is valid. That is types +which are not definitely known to be value types. Type parameters which are constrained to `struct` cannot be +used with this syntax. + +``` csharp +void NullCheck(T1 p1, T2 p2) where T2 : struct { + if (p1 is null) { + ... + } + + // Error + if (p2 is null) { + ... + } +} +``` + +The behavior of `is null` on a type parameter will be the same as `== null` today. In the cases where the type parameter +is instantiated as a value type the code will be evaluated as `false`. For cases where it is a reference type the +code will do a proper `is null` check. + +### Intersection with Nullable Reference Types +Any parameter which has a `!` operator applied to it's name will start with the nullable state being not `null`. This is +true even if the type of the parameter itself is potentially `null`. That can occur with an explicitly nullable type, +such as say `string?`, or with an unconstrained type parameter. + +When a `!` operator on parameters is combined with an explicitly nullable type on the parameter then a warning will +be issued by the compiler: + +``` csharp +void WarnCase( + string? name!, // Warning: combining explicit null checking with a nullable type + T value1 // Okay +) +``` + ## Open Issuess -- There is no way to check the `value` argument of a property setter for `null` using this syntax. +None ## Considerations -- In the case a constructor chains to another constructor with `this` or `base` the `null` -validation could occur before the chaining call. This is a stronger guarantee the developer could -desire. However C# code cannot be authored that way today and it seems unlikely that it's a -significant issue. Leaving the check after the chaining call means existing code can be ported -to the new pattern without fear of compat concerns. + +### Constructors +The code generation for constructors means there is a small, but observable, behavior change when moving from standard +`null` validation today and the `null` validation parameter syntax (`!`). The `null` check in standard validation +occurs after both field initializers and any `base` or `this` calls. This means a developer can't necessarily migrate +100% of their `null` validation to the new syntax. Constructors at least require some inspection. + +After discussion though it was decided that this is very unlikely to cause any significant adoption issues. It's more +logical that the `null` check run before any logic in the constructor does. Can revisit if significant compat issues +are discovered. + +### Warning when mixing ? and ! +There was a lengthy discussion on whether or not a warning should be issued when the `!` syntax is applied to a +parameter which is explicitly typed to a nullable type. On the surface it seems like a non-sensical declaration by +the developer but there are cases where type hierarchies could force developers into such a situation. + +Consider the following class hierarchy across a series of assemlbies (assuming all are compiled with `null` checking +enabled): + +``` csharp +// Assembly1 +abstract class C1 { + protected abstract void M(object o); +} + +// Assembly2 +abstract class C2 : C1 { + +} + +// Assembly3 +abstract class C3 : C2 { + protected override void M(object o!) { + ... + } +} +``` + +Here the author of `C3` decided to add `null` validation to the parameter `o`. This is completely in line with how the +feature is intended to be used. + +Now imagine at a later date the author of Assembly2 decides to add the following override: + +``` csharp +// Assembly2 +abstract class C2 : C1 { + protected override void M(object? o) { + ... + } +} +``` + +This is allowed by nullable reference types as it's legal to make the contract more flexible for input positions. The +NRT feature in general allows for reasonable co/contravariance on parameter / return nullability. However the language +does the co/contravariance checking based on the most specific override, not the original declaration. This means the +author of Assembly3 will get a warning about the type of `o` not matching and will need to change the signature to the +following to eliminate it: + +``` csharp +// Assembly3 +abstract class C3 : C2 { + protected override void M(object? o!) { + ... + } +} +``` + +At this point the author of Assembly3 has a few choices: + +- They can accept / suppress the warning about `object?` and `object` mismatch. +- They can accept / suppress the warning about `object?` and `!` mismatch. +- They can just remove the `null` validation check (delete `!` and do explicit checking) + +This is a real scenario but for now the idea is to move forward with the warning. If it turns out the warning happens +more frequently than we anticipate then we can remove it later (the reverse is not true). + +### Implicit property setter arguments +The `value` argument of a parameter is implicit and does not appear in any parameter list. That means it cannot be a +target of this feature. The property setter syntax could be extended to include a parameter list to allow the `!` +operator to be applied. But that cuts against the idea of this feature making `null` validation simpler. As such the +implicit `value` argument just won't work with this feature. ## Future Considerations -None \ No newline at end of file +None + +- iterators +- ctors +- lambda parameters \ No newline at end of file From 32ae3b834c0f24b98bd483ee6580d751af752109 Mon Sep 17 00:00:00 2001 From: Jared Parsons Date: Mon, 14 Jan 2019 16:21:39 -0800 Subject: [PATCH 3/4] More work --- proposals/null-arg-checking.md | 35 +++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/proposals/null-arg-checking.md b/proposals/null-arg-checking.md index fa32f76..77c76b4 100644 --- a/proposals/null-arg-checking.md +++ b/proposals/null-arg-checking.md @@ -13,7 +13,7 @@ anticipated to pair often with NRT the proposal is independent of it. ## Detailed Design -### Null validation +### Null validation parameter syntax The bang operator, `!`, can be positioned after any identifier in a parameter list and this will cause the C# compilet to emit standard `null` checking code for that parameter. For example: @@ -80,6 +80,35 @@ class C { Note: this is not legal C# code but instead just an approximation of what the implementation does. +The `null` validation parameter syntax will also be valid on lambda parameter lists. This is valid even in the single +parameter syntax that lacks parens. + +``` csharp +void G() { + // An identity lambda which throws on a null input + Func s = x! => x; +} +``` + +The syntax is also valid on parameters to iterator methods. Unlike other code in the iterator the `null` validation will +occur when the iterator method is invoked, not when the underlying enumerator is walked. This is true for traditional +or `async` iterators. + +``` csharp +class Iterators { + IEnumerable GetCharacters(string s!) { + foreach (var c in s) { + yield return c; + } + } + + void Use() { + // The invocation of GetCharacters will throw + IEnumerable e = GetCharacters(null); + } +} +``` + The `!` operator can only be used for parameter lists which have an associated method body. This means it cannot be used in an `abstract` method, `interface`, `delegate` or `partial` method definition. @@ -210,7 +239,3 @@ implicit `value` argument just won't work with this feature. ## Future Considerations None - -- iterators -- ctors -- lambda parameters \ No newline at end of file From dc6d70e02cec856bddf3f4ecc3d6024015fd6647 Mon Sep 17 00:00:00 2001 From: Jared Parsons Date: Mon, 14 Jan 2019 16:32:00 -0800 Subject: [PATCH 4/4] Cleanup --- proposals/null-arg-checking.md | 64 +++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/proposals/null-arg-checking.md b/proposals/null-arg-checking.md index 77c76b4..0adabb1 100644 --- a/proposals/null-arg-checking.md +++ b/proposals/null-arg-checking.md @@ -1,21 +1,24 @@ # Simplified Null Argument Checking ## Summary -This proposal provides a simplified syntax for validating method arguments are not `null` and -throwing `ArgumentNullException` appropriately. +This proposal provides a simplified syntax for validating method arguments are not `null` and throwing +`ArgumentNullException` appropriately. ## Motivation -The work on designing nullable reference types has caused us to examine the code necessary for -null argument validation. Given that NRT doesn't affect code execution developers still must -add `if (arg is null) throw` boiler plate code even in projects which are fully null clean. This -gave us the desire to explore a minimal syntax for argument `null` validation in the language. While -anticipated to pair often with NRT the proposal is independent of it. +The work on designing nullable reference types has caused us to examine the code necessary for `null` argument +validation. Given that NRT doesn't affect code execution developers still must add `if (arg is null) throw` boiler +plate code even in projects which are fully `null` clean. This gave us the desire to explore a minimal syntax for +argument `null` validation in the language. + +While this `null` parameter validation syntax is expected to pair frequently with NRT the proposal is fully independent +of it. The syntax can be used independent of `#nullable` directives. ## Detailed Design ### Null validation parameter syntax -The bang operator, `!`, can be positioned after any identifier in a parameter list and this will -cause the C# compilet to emit standard `null` checking code for that parameter. For example: +The bang operator, `!`, can be positioned after a parameter name in a parameter list and this will cause the C# +compiler to emit standard `null` checking code for that parameter. This is referred to as `null` validation parameter +syntax. For example: ``` csharp void M(string name!) { @@ -26,7 +29,7 @@ void M(string name!) { Will be translated into: ``` csharp -void M(string name!) { +void M(string name) { if (name is null) { throw new ArgumentNullException(nameof(name)); } @@ -34,18 +37,28 @@ void M(string name!) { } ``` -The generated `null` check will occur before any user authored code in the method. When multiple -parameters contain the `!` operator then the checks will occur in the same order as the parameters -are declared. - -The check will be specifically for reference equality to `null`, it does not invoke `==` or any user -defined operators. This also means the `!` operator can only be added to parameters whose type can -have the value `null`. Value types and type parameters not constrained to `class` or `interface` -cannot be used here. +The generated `null` check will occur before any developer authored code in the method. When multiple parameters contain +the `!` operator then the checks will occur in the same order as the parameters are declared. ``` csharp -// Errro: Cannot use ! on parameter of type T. -void G(T arg!) { +void M(string p1, string p2) { + if (p1 is null) { + throw new ArgumentNullException(nameof(p1)); + } + if (p2 is null) { + throw new ArgumentNullException(nameof(p2)); + } + ... +} +``` + +The check will be specifically for reference equality to `null`, it does not invoke `==` or any user defined operators. +This also means the `!` operator can only be added to parameters whose type can be tested for equality against `null`. +This means it can't be used on a parameter whose type is known to be a value type. + +``` csharp +// Errro: Cannot use ! on parameters who types derive from System.ValueType +void G(T arg!) where T : struct { } ``` @@ -55,6 +68,8 @@ In the case of a constructor the `null` validation will occur before any other c - Chaining to other constructors with `this` or `base` - Field initializers which implicitly occur in the constructor +For example: + ``` csharp class C { string field = GetString(); @@ -115,12 +130,13 @@ definition. ### Extending is null The types for which the expression `is null` is valid will be extended to include unconstrained type parameters. This -will allow it to fill the intent of checking for `null` on all types which a `null` check is valid. That is types -which are not definitely known to be value types. Type parameters which are constrained to `struct` cannot be -used with this syntax. +will allow it to fill the intent of checking for `null` on all types which a `null` check is valid. Specifically that +is types which are not definitely known to be value types. For example Type parameters which are constrained to +`struct` cannot be used with this syntax. ``` csharp void NullCheck(T1 p1, T2 p2) where T2 : struct { + // Okay: T1 could be a class or struct here. if (p1 is null) { ... } @@ -141,7 +157,7 @@ Any parameter which has a `!` operator applied to it's name will start with the true even if the type of the parameter itself is potentially `null`. That can occur with an explicitly nullable type, such as say `string?`, or with an unconstrained type parameter. -When a `!` operator on parameters is combined with an explicitly nullable type on the parameter then a warning will +When a `!` syntax on parameters is combined with an explicitly nullable type on the parameter then a warning will be issued by the compiler: ``` csharp