From 6c7900d133ad3c217a9869bee46857f3c7afdc1b Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 3 Sep 2025 19:37:40 -0400 Subject: [PATCH 1/2] Nameof may reference set-only property --- standard/classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/classes.md b/standard/classes.md index b46746511..afaf25e32 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -3475,7 +3475,7 @@ Based on the presence or absence of the get and set accessors, a property is cla - A property that includes both a get accessor and a set accessor is said to be a ***read-write property***. - A property that has only a get accessor is said to be a ***read-only property***. It is a compile-time error for a read-only property to be the target of an assignment. -- A property that has only a set accessor is said to be a ***write-only property***. Except as the target of an assignment, it is a compile-time error to reference a write-only property in an expression. +- A property that has only a set accessor is said to be a ***write-only property***. Except as the target of an assignment or as an argument to the `nameof` operator ([§12.8.23](expressions.md#12823-the-nameof-operator)), it is a compile-time error to reference a write-only property in an expression. > *Note*: The pre- and postfix `++` and `--` operators and compound assignment operators cannot be applied to write-only properties, since these operators read the old value of their operand before they write the new one. *end note* From 8eeb9f46da95b3ef8ebe3c3c6ab351fba53aaf60 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 3 Sep 2025 20:43:29 -0400 Subject: [PATCH 2/2] Update more places with respect to ref-valued properties and indexers --- standard/classes.md | 2 +- standard/expressions.md | 40 +++++++++++++++++++++------------------- standard/structs.md | 2 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/standard/classes.md b/standard/classes.md index afaf25e32..3150a475b 100644 --- a/standard/classes.md +++ b/standard/classes.md @@ -3474,7 +3474,7 @@ A set accessor corresponds to a method with a single value parameter of the prop Based on the presence or absence of the get and set accessors, a property is classified as follows: - A property that includes both a get accessor and a set accessor is said to be a ***read-write property***. -- A property that has only a get accessor is said to be a ***read-only property***. It is a compile-time error for a read-only property to be the target of an assignment. +- A property that has only a get accessor is said to be a ***read-only property***. It is a compile-time error for a read-only property to be the target of an assignment unless the property is ref-valued and returns a writeable reference. - A property that has only a set accessor is said to be a ***write-only property***. Except as the target of an assignment or as an argument to the `nameof` operator ([§12.8.23](expressions.md#12823-the-nameof-operator)), it is a compile-time error to reference a write-only property in an expression. > *Note*: The pre- and postfix `++` and `--` operators and compound assignment operators cannot be applied to write-only properties, since these operators read the old value of their operand before they write the new one. *end note* diff --git a/standard/expressions.md b/standard/expressions.md index ac6b861e0..6b7a3d03b 100644 --- a/standard/expressions.md +++ b/standard/expressions.md @@ -27,7 +27,9 @@ For expressions which occur as subexpressions of larger expressions, with the no - An event access. Every event access has an associated type, namely the type of the event. Furthermore, an event access may have an associated instance expression. An event access may appear as the left operand of the `+=` and `-=` operators ([§12.21.5](expressions.md#12215-event-assignment)). In any other context, an expression classified as an event access causes a compile-time error. When an accessor of an instance event access is invoked, the result of evaluating the instance expression becomes the instance represented by `this` ([§12.8.14](expressions.md#12814-this-access)). - A throw expression, which may be used in several contexts to throw an exception in an expression. A throw expression may be converted by an implicit conversion to any type. -A property access or indexer access is always reclassified as a value by performing an invocation of the get accessor or the set accessor. The particular accessor is determined by the context of the property or indexer access: If the access is the target of an assignment, the set accessor is invoked to assign a new value ([§12.21.2](expressions.md#12212-simple-assignment)). Otherwise, the get accessor is invoked to obtain the current value ([§12.2.2](expressions.md#1222-values-of-expressions)). +A non-ref-valued property access or non-ref-valued indexer access is always reclassified as a value by performing an invocation of the get accessor or the set accessor. The particular accessor is determined by the context of the property or indexer access: If the access is the target of an assignment, the set accessor is invoked to assign a new value ([§12.21.2](expressions.md#12212-simple-assignment)). Otherwise, the get accessor is invoked to obtain the current value ([§12.2.2](expressions.md#1222-values-of-expressions)). + +A ref-valued property access or non-ref-valued indexer access is always reclassified as a variable by performing an invocation of the get accessor ([§15.7.3](classes.md#1573-accessors)). An ***instance accessor*** is a property access on an instance, an event access on an instance, or an indexer access. @@ -36,8 +38,8 @@ An ***instance accessor*** is a property access on an instance, an event access Most of the constructs that involve an expression ultimately require the expression to denote a ***value***. In such cases, if the actual expression denotes a namespace, a type, a method group, or nothing, a compile-time error occurs. However, if the expression denotes a property access, an indexer access, or a variable, the value of the property, indexer, or variable is implicitly substituted: - The value of a variable is simply the value currently stored in the storage location identified by the variable. A variable shall be considered definitely assigned ([§9.4](variables.md#94-definite-assignment)) before its value can be obtained, or otherwise a compile-time error occurs. -- The value of a property access expression is obtained by invoking the get accessor of the property. If the property has no get accessor, a compile-time error occurs. Otherwise, a function member invocation ([§12.6.6](expressions.md#1266-function-member-invocation)) is performed, and the result of the invocation becomes the value of the property access expression. -- The value of an indexer access expression is obtained by invoking the get accessor of the indexer. If the indexer has no get accessor, a compile-time error occurs. Otherwise, a function member invocation ([§12.6.6](expressions.md#1266-function-member-invocation)) is performed with the argument list associated with the indexer access expression, and the result of the invocation becomes the value of the indexer access expression. +- The value of a property access expression is obtained by invoking the get accessor of the property. If the property has no get accessor, a compile-time error occurs. Otherwise, a function member invocation ([§12.6.6](expressions.md#1266-function-member-invocation)) is performed. For non-ref-valued properties, the result of the invocation becomes the value of the property access expression. For ref-valued properties, the result of the invocation is a variable and the value of the variable becomes the value of the property access expression. +- The value of an indexer access expression is obtained by invoking the get accessor of the indexer. If the indexer has no get accessor, a compile-time error occurs. Otherwise, a function member invocation ([§12.6.6](expressions.md#1266-function-member-invocation)) is performed with the argument list associated with the indexer access expression. For non-ref-valued indexers, the result of the invocation becomes the value of the indexer access expression. For ref-valued indexers, the result of the invocation is a variable and the value of the variable becomes the value of the indexer access expression. - The value of a tuple expression is obtained by applying an implicit tuple conversion ([§10.2.13](conversions.md#10213-implicit-tuple-conversions)) to the type of the tuple expression. It is an error to obtain the value of a tuple expression that does not have a type. ## 12.3 Static and Dynamic Binding @@ -359,7 +361,7 @@ In both of the above cases, a cast expression can be used to explicitly convert A member lookup is the process whereby the meaning of a name in the context of a type is determined. A member lookup can occur as part of evaluating a *simple_name* ([§12.8.4](expressions.md#1284-simple-names)) or a *member_access* ([§12.8.7](expressions.md#1287-member-access)) in an expression. If the *simple_name* or *member_access* occurs as the *primary_expression* of an *invocation_expression* ([§12.8.10.2](expressions.md#128102-method-invocations)), the member is said to be *invoked*. -If a member is a method or event, or if it is a constant, field or property of either a delegate type ([§20](delegates.md#20-delegates)) or the type `dynamic` ([§8.2.4](types.md#824-the-dynamic-type)), then the member is said to be *invocable.* +If a member is a method or event, or if it is a constant, field or property whose evaluated value is either of a delegate type ([§20](delegates.md#20-delegates)) or the type `dynamic` ([§8.2.4](types.md#824-the-dynamic-type)), then the member is said to be *invocable.* Member lookup considers not only the name of a member but also the number of type parameters the member has and whether the member is accessible. For the purposes of member lookup, generic methods and nested generic types have the number of type parameters indicated in their respective declarations and all other members have zero type parameters. @@ -454,7 +456,7 @@ Once a particular function member has been identified at binding-time, possibly > > > P = value -> The set accessor of the property P in the containing class or struct is invoked with the argument list (value). A compile-time error occurs if P is read-only. If P is not static, the instance expression is this. +> If P is non-ref-valued, the set accessor of the property P in the containing class or struct is invoked with the argument list (value). If P is ref-valued, the get accessor of the property P in the containing class or struct is invoked. A compile-time error occurs if P is non-ref-valued and read-only or if P is ref-valued and returns a readonly reference. If P is not static, the instance expression is this. > > > T.P @@ -462,7 +464,7 @@ Once a particular function member has been identified at binding-time, possibly > > > T.P = value -> The set accessor of the property P in the class or struct T is invoked with the argument list (value). A compile-time error occurs if P is not static or if P is read-only. +> If P is non-ref-valued, the set accessor of the property P in the class or struct T is invoked with the argument list (value). If P is ref-valued, the get accessor of the property P in the containing class or struct is invoked. A compile-time error occurs if P is not static, if P is non-ref-valued and read-only, or if P is ref-valued and returns a readonly reference. > > > e.P @@ -470,7 +472,7 @@ Once a particular function member has been identified at binding-time, possibly > > > e.P = value -> The set accessor of the property P in the class, struct, or interface given by the type of E is invoked with the instance expression e and the argument list (value). A binding-time error occurs if P is static or if P is read-only. +> If P is non-ref-valued, the set accessor of the property P in the class, struct, or interface given by the type of E is invoked with the instance expression e and the argument list (value). If P is ref-valued, the get accessor of the property P in the containing class, struct, or interface given by the type of E is invoked is invoked with the instance expression e. A binding-time error occurs if P is static, if P is non-ref-valued and read-only, or if P is ref-valued and returns a readonly reference. > > > Event access @@ -504,7 +506,7 @@ Once a particular function member has been identified at binding-time, possibly > > > e[x, y] = value -> Overload resolution is applied to select the best indexer in the class, struct, or interface given by the type of e. The set accessor of the indexer is invoked with the instance expression e and the argument list (x, y, value). A binding-time error occurs if the indexer is read-only. +> Overload resolution is applied to select the best indexer in the class, struct, or interface given by the type of e. If the indexer is non-ref-valued, the set accessor of the indexer is invoked with the instance expression e and the argument list (x, y, value). If the indexer is ref-valued, the get accessor of the indexer is invoked with the instance expression e and the argument list (x, y). A binding-time error occurs if the indexer is non-ref valued and read-only or if the indexer is ref-valued and returns a readonly reference. > > > @@ -1715,7 +1717,7 @@ The *member_access* is evaluated and classified as follows: - If `I` identifies an enumeration member, then the result is a value, namely the value of that enumeration member. - Otherwise, `E.I` is an invalid member reference, and a compile-time error occurs. - If `E` is a property access, indexer access, variable, or value, the type of which is `T`, and a member lookup ([§12.5](expressions.md#125-member-lookup)) of `I` in `T` with `K` type arguments produces a match, then `E.I` is evaluated and classified as follows: - - First, if `E` is a property or indexer access, then the value of the property or indexer access is obtained ([§12.2.2](expressions.md#1222-values-of-expressions)) and E is reclassified as a value. + - First, if `E` is a non-ref-valued property or non-ref-valued indexer access, then the value of the property or indexer access is obtained ([§12.2.2](expressions.md#1222-values-of-expressions)) and E is reclassified as a value. If `E` is a ref-valued property or ref-valued indexer access, then a variable is obtained by invoking the get accessor ([§15.7.3](classes.md#1573-accessors)) and E is reclassified as a variable. - If `I` identifies one or more methods, then the result is a method group with an associated instance expression of `E`. - If `I` identifies an instance property, then the result is a property access with an associated instance expression of `E` and an associated type that is the type of the property. If `T` is a class type, the associated type is picked from the first declaration or override of the property found when starting with `T`, and searching through its base classes. - If `T` is a *class_type* and `I` identifies an instance field of that *class_type*: @@ -2293,7 +2295,7 @@ The binding-time processing of an indexer access of the form `P[A]`, where `P` i - The best indexer of the set of candidate indexers is identified using the overload resolution rules of [§12.6.4](expressions.md#1264-overload-resolution). If a single best indexer cannot be identified, the indexer access is ambiguous, and a binding-time error occurs. - The index expressions of the *argument_list* are evaluated in order, from left to right. The result of processing the indexer access is an expression classified as an indexer access. The indexer access expression references the indexer determined in the step above, and has an associated instance expression of `P` and an associated argument list of `A`, and an associated type that is the type of the indexer. If `T` is a class type, the associated type is picked from the first declaration or override of the indexer found when starting with `T` and searching through its base classes. -Depending on the context in which it is used, an indexer access causes invocation of either the get accessor or the set accessor of the indexer. If the indexer access is the target of an assignment, the set accessor is invoked to assign a new value ([§12.21.2](expressions.md#12212-simple-assignment)). In all other cases, the get accessor is invoked to obtain the current value ([§12.2.2](expressions.md#1222-values-of-expressions)). +Depending on the context in which it is used, an indexer access causes invocation of either the get accessor or the set accessor of the indexer. If the indexer access is the target of an assignment and the indexer is non-ref-valued, the set accessor is invoked to assign a new value ([§12.21.2](expressions.md#12212-simple-assignment)). In all other cases, the get accessor is invoked; for a non-ref-valued indexer, to obtain the current value ([§12.2.2](expressions.md#1222-values-of-expressions)), and for a ref-valued indexer, to obtain a variable ([§15.7.3](classes.md#1573-accessors)). ### 12.8.13 Null Conditional Element Access @@ -2426,7 +2428,7 @@ The operand of a postfix increment or decrement operation shall be an expression If the *primary_expression* has the compile-time type `dynamic` then the operator is dynamically bound ([§12.3.3](expressions.md#1233-dynamic-binding)), the *post_increment_expression* or *post_decrement_expression* has the compile-time type `dynamic` and the following rules are applied at run-time using the run-time type of the *primary_expression*. -If the operand of a postfix increment or decrement operation is a property or indexer access, the property or indexer shall have both a get and a set accessor. If this is not the case, a binding-time error occurs. +If the operand of a postfix increment or decrement operation is a property or indexer access, the property or indexer either shall have both a get and a set accessor, in the case of a non-ref-valued property or indexer, or shall return a writeable reference, in the case of a ref-valued property or indexer. If this is not the case, a binding-time error occurs. Unary operator overload resolution ([§12.4.4](expressions.md#1244-unary-operator-overload-resolution)) is applied to select a specific operator implementation. Predefined `++` and `--` operators exist for the following types: `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `char`, `float`, `double`, `decimal`, and any enum type. The predefined `++` operators return the value produced by adding `1` to the operand, and the predefined `--` operators return the value produced by subtracting `1` from the operand. In a checked context, if the result of this addition or subtraction is outside the range of the result type and the result type is an integral type or enum type, a `System.OverflowException` is thrown. @@ -2440,7 +2442,7 @@ The run-time processing of a postfix increment or decrement operation of the for - The saved value of `x` is converted to the operand type of the selected operator and the operator is invoked with this value as its argument. - The value returned by the operator is converted to the type of `x` and stored in the location given by the earlier evaluation of `x`. - The saved value of `x` becomes the result of the operation. -- If `x` is classified as a property or indexer access: +- Otherwise, if `x` is classified as a property or indexer access: - The instance expression (if `x` is not `static`) and the argument list (if `x` is an indexer access) associated with `x` are evaluated, and the results are used in the subsequent get and set accessor invocations. - The get accessor of `x` is invoked and the returned value is saved. - The saved value of `x` is converted to the operand type of the selected operator and the operator is invoked with this value as its argument. @@ -2557,9 +2559,9 @@ Each *initializer_target* is followed by an equals sign and either an expression A member initializer that specifies an expression after the equals sign is processed in the same way as an assignment ([§12.21.2](expressions.md#12212-simple-assignment)) to the target. -A member initializer that specifies an object initializer after the equals sign is a ***nested object initializer***, i.e., an initialization of an embedded object. Instead of assigning a new value to the field or property, the assignments in the nested object initializer are treated as assignments to members of the field or property. Nested object initializers cannot be applied to properties with a value type, or to read-only fields with a value type. +A member initializer that specifies an object initializer after the equals sign is a ***nested object initializer***, i.e., an initialization of an embedded object. Instead of assigning a new value to the field or property, the assignments in the nested object initializer are treated as assignments to members of the field or property. Nested object initializers cannot be applied to properties or indexers with a value type, to ref-valued properties or indexers whose type is a reference to a value type, or to read-only fields with a value type. -A member initializer that specifies a collection initializer after the equals sign is an initialization of an embedded collection. Instead of assigning a new collection to the target field, property, or indexer, the elements given in the initializer are added to the collection referenced by the target. The target shall be of a collection type that satisfies the requirements specified in [§12.8.17.2.3](expressions.md#1281723-collection-initializers). +A member initializer that specifies a collection initializer after the equals sign is an initialization of an embedded collection. Instead of assigning a new collection to the target field, property, or indexer, the elements given in the initializer are added to the collection referenced by the target. The target shall be of a collection type that satisfies the requirements specified in [§12.8.17.2.3](expressions.md#1281723-collection-initializers). Initialization of an embedded collection cannot be applied to properties or indexers with a value type, to ref-valued properties or indexers whose type is a reference to a value type, or to read-only fields with a value type. When an initializer target refers to an indexer, the arguments to the indexer shall always be evaluated exactly once. Thus, even if the arguments end up never getting used (e.g., because of an empty nested initializer), they are evaluated for their side effects. @@ -3626,7 +3628,7 @@ pre_decrement_expression The operand of a prefix increment or decrement operation shall be an expression classified as a variable, a property access, or an indexer access. The result of the operation is a value of the same type as the operand. -If the operand of a prefix increment or decrement operation is a property or indexer access, the property or indexer shall have both a get and a set accessor. If this is not the case, a binding-time error occurs. +If the operand of a prefix increment or decrement operation is a property or indexer access, the property or indexer either shall have both a get and a set accessor, in the case of a non-ref-valued property or indexer, or whose type is a writeable reference, in the case of a ref-valued property or indexer. If this is not the case, a binding-time error occurs. Unary operator overload resolution ([§12.4.4](expressions.md#1244-unary-operator-overload-resolution)) is applied to select a specific operator implementation. Predefined `++` and `--` operators exist for the following types: `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `char`, `float`, `double`, `decimal`, and any enum type. The predefined `++` operators return the value produced by adding `1` to the operand, and the predefined `--` operators return the value produced by subtracting `1` from the operand. In a `checked` context, if the result of this addition or subtraction is outside the range of the result type and the result type is an integral type or enum type, a `System.OverflowException` is thrown. @@ -3638,7 +3640,7 @@ The run-time processing of a prefix increment or decrement operation of the form - `x` is evaluated to produce the variable. - The value of `x` is converted to the operand type of the selected operator and the operator is invoked with this value as its argument. - The value returned by the operator is converted to the type of `x`. The resulting value is stored in the location given by the evaluation of `x` and becomes the result of the operation. -- If `x` is classified as a property or indexer access: +- Otherwise, if `x` is classified as a property or indexer access: - The instance expression (if `x` is not `static`) and the argument list (if `x` is an indexer access) associated with `x` are evaluated, and the results are used in the subsequent get and set accessor invocations. - The get accessor of `x` is invoked. - The value returned by the get accessor is converted to the operand type of the selected operator and operator is invoked with this value as its argument. @@ -6566,7 +6568,7 @@ The run-time processing of a simple assignment of the form `x = y` with type `T` - If `x` is classified as a variable, `y` is evaluated and, if required, converted to `T` through an implicit conversion ([§10.2](conversions.md#102-implicit-conversions)). - If the variable given by `x` is an array element of a *reference_type*, a run-time check is performed to ensure that the value computed for `y` is compatible with the array instance of which `x` is an element. The check succeeds if `y` is `null`, or if an implicit reference conversion ([§10.2.8](conversions.md#1028-implicit-reference-conversions)) exists from the type of the instance referenced by `y` to the actual element type of the array instance containing `x`. Otherwise, a `System.ArrayTypeMismatchException` is thrown. - The value resulting from the evaluation and conversion of `y` is stored into the location given by the evaluation of `x`, and is yielded as a result of the assignment. -- If `x` is classified as a property or indexer access: +- Otherwise, if `x` is classified as a property or indexer access: - `y` is evaluated and, if required, converted to `T` through an implicit conversion ([§10.2](conversions.md#102-implicit-conversions)). - The set accessor of `x` is invoked with the value resulting from the evaluation and conversion of `y` as its value argument. - The value resulting from the evaluation and conversion of `y` is yielded as the result of the assignment. @@ -6595,7 +6597,7 @@ The run-time processing of a simple assignment of the form `x = y` with type `T` > > *end note* -When a property or indexer declared in a *struct_type* is the target of an assignment, the instance expression associated with the property or indexer access shall be classified as a variable. If the instance expression is classified as a value, a binding-time error occurs. +When a non-ref-valued property or indexer declared in a *struct_type* is the target of an assignment, the instance expression associated with the property or indexer access shall be classified as a variable. If the instance expression is classified as a value, a binding-time error occurs. > *Note*: Because of [§12.8.7](expressions.md#1287-member-access), the same rule also applies to fields. *end note* @@ -6745,7 +6747,7 @@ The term “evaluated only once” means that in the evaluation of `x «op» y > *Example*: In the assignment `A()[B()] += C()`, where `A` is a method returning `int[]`, and `B` and `C` are methods returning `int`, the methods are invoked only once, in the order `A`, `B`, `C`. *end example* -When the left operand of a compound assignment is a property access or indexer access, the property or indexer shall have both a get accessor and a set accessor. If this is not the case, a binding-time error occurs. +When the left operand of a compound assignment is a property access or indexer access, the property or indexer either shall have both a get accessor and a set accessor, in the case of a non-ref-valued property or indexer, or shall return a writeable reference, in the case of a ref-valued property or indexer. If this is not the case, a binding-time error occurs. The second rule above permits `x «op»= y` to be evaluated as `x = (T)(x «op» y)` in certain contexts. The rule exists such that the predefined operators can be used as compound operators when the left operand is of type `sbyte`, `byte`, `short`, `ushort`, or `char`. Even when both arguments are of one of those types, the predefined operators produce a result of type `int`, as described in [§12.4.7.3](expressions.md#12473-binary-numeric-promotions). Thus, without a cast it would not be possible to assign the result to the left operand. diff --git a/standard/structs.md b/standard/structs.md index 2c352ee73..af31e0d21 100644 --- a/standard/structs.md +++ b/standard/structs.md @@ -238,7 +238,7 @@ Assignment to a variable of a struct type creates a *copy* of the value being as Similar to an assignment, when a struct is passed as a value parameter or returned as the result of a function member, a copy of the struct is created. A struct may be passed by reference to a function member using a by-reference parameter. -When a property or indexer of a struct is the target of an assignment, the instance expression associated with the property or indexer access shall be classified as a variable. If the instance expression is classified as a value, a compile-time error occurs. This is described in further detail in [§12.21.2](expressions.md#12212-simple-assignment). +When a non-ref-valued property or indexer of a struct is the target of an assignment, the instance expression associated with the property or indexer access shall be classified as a variable. If the instance expression is classified as a value, a compile-time error occurs. This is described in further detail in [§12.21.2](expressions.md#12212-simple-assignment). ### 16.4.5 Default values