Champion issue: #9896
Allow compound assignments in an object initializer:
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1d),
Tick += (_, _) => { /*actual work*/ },
};Or a with expression:
var newCounter = counter with { Value -= 1 };It is not uncommon, especially in UI frameworks, to create objects that both have values assigned and need events hooked up as part of initialization. While object initializers addressed the first part with a nice shorthand syntax, the latter still requires additional statements. This makes it impossible to create these sorts of objects as a simple declaration expression, preventing their use in expression-bodied members or in nested constructs like collection initializers or switch expressions. Spilling the object creation expression out to a variable declaration statement makes things more verbose for such a simple concept.
The declarative UI story can be made much more complete with a small change to the language. Windows Forms in particular can immediately gain a more appetizing story for dynamic or manual creation of UI controls, both in vanilla form and when using third-party vendor frameworks that build on Windows Forms.
The same reasoning applies to more than just events. Newly created objects (and especially objects produced via with) may want their initialized values to be relative to a prior or default state, which is exactly what a compound operator expresses.
The following updates are presented as a diff against the corresponding sections of the C# 7 standard (expressions.md), and against the with expression subsection of the C# 9 records proposal.
Throughout this section, strikethrough indicates text being removed from the existing specification, and bold indicates text being added. Unchanged prose is quoted verbatim for context.
The following diff is applied to the grammar in §12.8.16.3:
object_initializer
: '{' member_initializer_list? '}'
| '{' member_initializer_list ',' '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
- : initializer_target '=' initializer_value
+ : initializer_target '=' object_or_collection_initializer
+ | initializer_target assignment_operator expression
;
initializer_target
: identifier
| '[' argument_list ']'
;
- initializer_value
- : expression
- | object_or_collection_initializer
- ;The prose in §12.8.16.3 is updated as follows. The terms compound assignment operator and compound assignment below are used as defined in §12.21.1.
An object initializer consists of a sequence of member initializers, enclosed by { and } tokens and separated by commas. Each member_initializer shall designate a target for the initialization. An identifier shall name an accessible field or property or event of the object being initialized, whereas an argument_list enclosed in square brackets shall specify arguments for an accessible indexer on the object being initialized. It is an error for an object initializer to include more than one member initializer for the same field or property. For any given field, property, or event target, any number of member initializers using a compound assignment operator are permitted for the same target. If present, an = member initializer shall appear in lexical order before any other member initializer for that target. No such restriction applies to indexer targets.
A member_initializer of the first form (initializer_target '=' object_or_collection_initializer) is exclusive: it must be the only member_initializer in the enclosing member_initializer_list whose initializer_target designates the same field, property, or event.
Note: While an object initializer is not permitted to set the same field or property more than once with =, the existing "same indexer arguments multiple times" allowance is preserved for plain assignment and compound forms. The relaxation above for compound operators supports, among other things, subscribing multiple handlers to the same event in a single initializer (Click += h1, Click += h2) and accumulating into the same property (Value = 10, Value += 5). The target = { … } form is exclusive because it configures the nested instance the target already references; combining it with a slot-overwriting = or a read-modify-write compound assignment operator would overwrite that configuration. end note
Each initializer_target is followed by an equals sign and either an expression, an object initializer or a collection initializer. It is not possible for expressions within the object initializer to refer to the newly created object it is initializing. The single exception is the target of a compound member initializer (a member_initializer of the second form whose operator is a compound assignment operator): both the read and the write of the target occur on the newly created object. Member initializers are processed in lexical order, so the read performed by a compound member initializer takes place after every preceding member initializer has executed; the read accordingly reflects whatever state that target's get accessor reports at that point.
A member initializer that specifies an expression after the equals sign is processed in the same way as an assignment (§12.21.2) to the target. A member_initializer of the second form target op value in an object initializer is semantically equivalent to the statement_expression x.target op value;, where x is the otherwise invisible and inaccessible temporary variable holding the instance being initialized, and where x.target is the field, property, event, or indexer access designated by initializer_target. The meaning of that statement_expression is given by §12.21: simple assignment (§12.21.2) when op is =, compound assignment (§12.21.4) when op is a compound assignment operator and the target is not an event, and event assignment (§12.21.5) when op is += or -= and the target is an event. In particular, the statement_expression context required by §12.21.5 is satisfied by this lowering, and the get-and-set requirement on property and indexer targets imposed by §12.21.4 applies to the target of a compound member initializer.
The existing §12.8.16.3 paragraphs about nested object initializer and nested collection initializer are unchanged; they apply only to the first-form member_initializer, which is the only form that admits a nested object_or_collection_initializer.
Example: The following class combines properties and an event:
public class Counter
{
public int Value { get; set; }
public event EventHandler Changed;
}An instance of Counter can be created and initialized using a mixture of = and compound member initializers:
Counter c = new Counter
{
Value = 10,
Value += 5,
Changed += OnChanged,
Changed += OnChanged2,
};which has the same effect as
Counter __c = new Counter();
__c.Value = 10;
__c.Value += 5;
__c.Changed += OnChanged;
__c.Changed += OnChanged2;
Counter c = __c;where __c is an otherwise invisible and inaccessible temporary variable. Each generated line is a statement_expression; the first two are a simple assignment and a compound assignment on a property, and the last two are event assignments on an event.
end example
The following diff is applied to the grammar of the with expression:
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
- : identifier '=' expression
+ : identifier assignment_operator expression
;The prose of the with expression subsection is updated as follows.
On the right hand side of the with expression is a member_initializer_list with a sequence of assignments to identifier, which must be an accessible instance field or property or event of the receiver's type.
For any given field, property, or event target, any number of member initializers using a compound assignment operator are permitted for the same target. If present, an = member initializer shall appear in lexical order before any other member initializer for that target.
First, receiver's "clone" method (specified above) is invoked and its result is converted to the receiver's type. Then, each Then, for each member_initializer is processed the same way as an assignment to a field or property access of the result of the conversion. Assignments are processed in lexical order.member_initializer target op value in lexical order, the statement_expression x.target op value; is executed, where x is the otherwise invisible and inaccessible temporary variable holding the converted clone. The meaning of that statement_expression is given by §12.21, as for an object initializer. When op is a compound assignment operator, both the read and the write of target are performed on the clone x; the original receiver of the with expression is not read again after its clone method has returned. Member initializers are processed in lexical order, so the read performed by a compound member initializer takes place after every preceding member initializer has executed; the read accordingly reflects whatever state that target's get accessor reports on the clone at that point.
Example: Given
public record Counter(int Value)
{
public event EventHandler Changed;
}
Counter original = ...;the expression
Counter c = original with { Value -= 1, Changed += OnChanged };has the same effect as
Counter __c = (Counter)original.<Clone>();
__c.Value -= 1; // both the read and the write are on __c
__c.Changed += OnChanged;
Counter c = __c;where __c is an otherwise invisible and inaccessible temporary variable.
end example
-
Accessor requirements on property and indexer targets. A compound member_initializer on a property or indexer target inherits the access requirements of §12.21.4 via the lowering above. In practice, a property or indexer target in either an object initializer or a
withexpression is valid with a compound assignment operator when it has agetaccessor together with asetorinitaccessor, or when itsgetaccessor returns a reference (a ref-returning property or indexer, which classifies the access as a variable per §12.8.7). Aninitaccessor is accepted on the same terms as for a direct=member initializer. Plain=member initializers continue to use the accessor requirements already specified in their respective sections, unchanged by this proposal. -
Event targets. On an event target,
+=and-=dispatch to the event'saddandremoveaccessors respectively, per §12.21.5. A single object initializer orwithexpression may therefore subscribe or unsubscribe multiple handlers in lexical order, including on the same event; see the relaxed uniqueness rule above. -
Indexer targets. The grammar of §12.8.16.3 continues to permit
'[' argument_list ']'as an initializer_target for both member_initializer forms. The "arguments shall always be evaluated exactly once" rule already specified for indexer initializer targets in §12.8.16.3 is unchanged and applies equally to compound member initializers; the get-and-set requirement from §12.21.4 applies to the selected indexer. -
Required members. In an object initializer, a
requiredfield or property is satisfied only by a=member_initializer; a compound member initializer does not on its own discharge the requirement. Therequiredtarget may additionally appear under one or more compound member initializers in the same object initializer, subject to the "=before any compound" ordering above. In awithexpression no such restriction applies — the receiver has already been constructed and itsrequiredmembers satisfied by that earlier construction, so thewithclause admits compound-only member initializers onrequiredtargets.SetsRequiredMembersAttributecontinues to discharge the obligation in both forms, as in the existing specification.Motivation:
requiredexists to ensure each marked slot is in a valid state before any read of it. A compound assignment operator reads the slot before writing it; on a freshly constructed object the read sees the slot's default value, which is exactly the staterequiredis meant to prevent the object from being observed in. Awithclone has already been initialized once, so the read is safe. -
Dynamic. When the target's instance expression (the temporary
x) has an accessed member whose container has compile-time typedynamic, dynamic binding of the member initializer follows §12.21.2 (for=) or §12.21.4 (for compound), unchanged. -
Collection initializers are unaffected. The grammar of §12.8.16.4 uses non_assignment_expression for the unbraced element form, which by definition excludes assignment (§12.22). A form such as
a += bis an assignment and therefore remains ill-formed as an element initializer of a collection initializer. No change to §12.8.16.4 is required.
This is a pure extension. The second-form member_initializer introduced into §12.8.16.3 (initializer_target assignment_operator expression) generalizes the existing initializer_target '=' expression form to the full assignment_operator, picking up target += value, target ??= value, etc.; the existing target = value form continues to be admitted by this same production with op equal to =. None of the newly admitted operator forms are valid member_initializers today, so no expression that compiles today changes meaning. The same reasoning applies to the with expression's member_initializer. Any program that compiled before this feature continues to compile with the same meaning.
As with any language feature, the additional specification complexity must be weighed against the clarity and correctness improvements it offers users. The feature is localized to one subsection of §12.8.16.3 and one subsection of the with expression, and reuses §12.21's existing machinery at every step, so the marginal complexity is small.
-
Do nothing. Users continue to use patterns like an extension method that takes a receiver and a configuration lambda:
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1d), }.Init(t => t.Tick += (_, _) => { /*actual work*/ });
This works for non-
initmembers, but not forinit-only properties, and loses the first-class expression shape. -
Restrict to events only. A narrower feature that would permit
Event += handlerin an object initializer but notProperty += 1. This addresses the headline UI scenario but excludes the accumulation-into-property and accumulation-into-cloned-value scenarios that motivate thewithcase.
The single most compelling motivation for this feature is events, and events naturally chain: subscribing two handlers to the same event in one initializer is useful and unsurprising, or unsubscribing and resubscribing. Forbidding this would make the case (Click -= h1, Click += h2) illegal in exactly the contexts where the feature is most valuable. The same reasoning extends to compound operators on properties (Value = 10, Value += 5), where each step has an observable effect.
At the same time, = remains destructive: permitting a second = for the same target would make the first assignment dead code. The rule adopted here, "at most one = per target, any number of compound member initializers per target after it," keeps = as unambiguously initializing and lets compound operators compose on top.
Phrasing a member_initializer as an equivalent statement_expression makes all of the necessary rules fall out of §12.21 with no further writing. In particular, §12.21.5 already requires event assignment to appear in a statement_expression context; the lowering satisfies that requirement by construction, without special-casing events at the initializer level. Each of simple assignment, compound assignment, and event assignment is invoked by the same uniform rule: "the meaning of x.target op value;."
TBD