[Proposal]: Compound assignment in object initializer and with
expression
#8650
Replies: 117 comments
-
I would use this feature. I want to know how it would handle property changes that trigger an event.
got refactored to
before the event would not trigger, and after it would get triggered.
Should the language enforce the events get added last, or should auto refactoring tools try to add the events last, to avoid triggering them on accident? |
Beta Was this translation helpful? Give feedback.
-
That's an existing question that applies to assignments of properties. I would be very confused if the syntax did not desugar to a series of statements in the order that the members are initialized in syntax. What if someone wants to rely on PropertyChanged getting called when Text is set, and blocking them doing it in their preferred order is just annoying them? |
Beta Was this translation helpful? Give feedback.
-
Yeah that makes sense. Just worried about someone moving them around to be ABC order. I would probably keep the event initializers at the bottom most of the time. |
Beta Was this translation helpful? Give feedback.
-
While my scenario isn't about events, I recently encountered a case where the lack of compound assignment in var newStats = oldStats with { /* update a bunch of properties here ... */ Elapsed += sw.Elapsed, /* ... and more here */ }; |
Beta Was this translation helpful? Give feedback.
-
Thanks @bartdesmet I'll definitely bring up the question if we want to support this in |
Beta Was this translation helpful? Give feedback.
-
This has happened to me as well. |
Beta Was this translation helpful? Give feedback.
-
For me especially operator |
Beta Was this translation helpful? Give feedback.
-
This would be very useful when constructing a "tree" of objects, like UI controls, but also anything else, where children are added inline: this.Controls.Add(new Panel
{
Controls = new List<Control>
{
new Button
{
Text = "Submit",
Click += this.btnSubmit_Click,
},
new Button
{
Text = "Cancel",
Click += this.btnCancel_Click,
},
}
}); |
Beta Was this translation helpful? Give feedback.
-
Question: the same field/property cannot be assigned twice, but should it be possible to attach 2+ handlers to the same event? Would it be by repeating the event name, or separating the handlers with commas or something like that? var btn = new Button
{
Click += this.btn_Click,
Click += this.anyControl_Click,
// or
Click += [this.btn_Click, this.anyControl_Click],
}; |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Is there a good reason to not simply allow you to call ANY member method of the newly created object (and treat it the same as if you called it after construction)?? After all, property setters are really just calling the "Property_Set(value)" method; so why not simply extend this capability to allow calling ANY class method for the instance being constructed? So for the event handler registration example, the answer is clear -- just have two entries: var btn = new Button()
{
Clicked += EventHandler1,
Clicked += EventHandler2,
AnyClassMethod(args),
AnyExtensionMethod(args)
} It simply means that every statement inside the brackets ({ }) is treated as though it were prefixed by "btn.". This is the Simplest solution/rule, and offers the maximum benefit. === public int MyProp { get; private set; } // Setter is PRIVATE
public void SetMyProp(int val, string reason) // USE THIS METHOD TO SET the property
{
// Here I can enable Logging, which will now include the "reason" without doing expensive reflection techniques.
} This makes it very easy to find out "why the value was set" later on, simply by looking at Log output, and also makes it easier to set Breakpoints for specific "reason" values. Using this technique, currently disables our ability to use the Object initializer, because my properties don't have public setters. What I'd prefer to do is: new MyClass()
{
SetMyProp(value, "Construct")
} Currently, we cannot use Object Initializers for this mode of API. IMO, simply allow Object Initializer blocks to call ANY method on the instance, not just Property Setters. |
Beta Was this translation helpful? Give feedback.
-
Yes. Methods imply state mutation as opposed to declarative construction. So I feel that it's much more natural for them to be after the instance read constructed. Also, that syntax is already allowed and is used for collection initialization. So it would need to be a syntax that would not be ambiguous with that. |
Beta Was this translation helpful? Give feedback.
-
This doesn't make sense to me. I agree that methods imply state mutation, but state mutaion is NOT opposed to declarative construction. Construction is really a kind of mutation in C#, because C# currently doesn't have a good mechanism to distinguish the two, until we have required init property and init-only methods. Also, although event registration should be seen as construction, a more general compound assignment is more like a state mutation. In any case, you shouldn't say something like "we don't want to add this because it is not initialization/construction". |
Beta Was this translation helpful? Give feedback.
-
I disagree. I think we do, and part of that is not blurring the lines more by using methods in these scenarios. |
Beta Was this translation helpful? Give feedback.
-
The long-term trend has been that programmers want simpler (more terse) syntax. Less typing accomplishes more (and thus less reading too). For UI construction, some controls are written where the Children list is "Get-only" -- you can modify the contents of the list, but not "set the list" -- e.g. you can call AddChildren, or AddChild, but not "Children = new List()". So UI composition that SHOULD work without writing a full suite of hackish extension methods is the following: new Grid()
{
Prop1 = propVal,
Prop2 = propVal2,
AddChildren(
new Button()
{
Clicked += EventHandler,
BindTo(binding),
///... more
},
new Button()
{
// compose Button2 here
}
) // end of AddChildren(..)
} If a developer thinks "methods don't belong in initialization", then that programmer can decide to not-call-methods in the intializer. But for the context where it is deemed grossly useful (e.g. UI composition) - allow it. Why not? |
Beta Was this translation helpful? Give feedback.
-
More succinct code, so long as it is just as clear - is preferable to more bloated syntax. That's the main point of what I'm saying here. The designer can either generated bloated C# or succinct C# to achieve the same end goal. The succinct notation, is likely superior, so long as it is equally intelligible. In this case, of including .method() calls into the Init Block, it is equally intelligible, and IMO, superior. It seems a great many things introduced into the C# language have been done despite being able to say "XYZ has existed in the .NET ecosystem for ABC years". The existence of people "dealing with how things are" doesn't mean you therefore don't try to make improvements. Most of the C# changes I've seen qualify as these types of changes; yet they are done anyways, AND were good improvements. |
Beta Was this translation helpful? Give feedback.
-
If it's being compiled as a part of the project then it doesn't really matter, as long as it's clear. The WPF designer doesn't have a problem here, and the language isn't going to adopt new syntax for the sake of making life slightly easier for designer code that is likely much easier to emit without trying to construct a complex fluent graph anyway. |
Beta Was this translation helpful? Give feedback.
-
Like for Records, and concerns of immutability -- you could achieve this by making constructors that set all of the private fields, and then mark them "readonly". Why do more? Because doing more is helpful and beneficial by making it easier for us to have immutable classes. |
Beta Was this translation helpful? Give feedback.
-
I do agree that this is a minor concern. Mostly because very little serialization is done in this fashion, and where it is being done, making the code shorter doesn't really help much. I stepped into this because someone else suggested that "Fluent API might interfere with serialization concerns" -- and my response was simply "if anything, it can only help". My proposal poses zero threat to his concerns over serialization. The rest was just a brainstorm based on experience... but you are right, that example is not a significant concern for us here. |
Beta Was this translation helpful? Give feedback.
-
I think you misinterpreted that statement, this has nothing to do with designers. It has to do with how a serialization library would be able to interpret the use of these kinds of methods when it comes to deserializing from a wire format. That is based entirely on reflection and there is no intermediate source in play. |
Beta Was this translation helpful? Give feedback.
-
Just confirming, this will work, right? var x = new Class { StrProp += " suffix" }; |
Beta Was this translation helpful? Give feedback.
-
I would think so. |
Beta Was this translation helpful? Give feedback.
-
Addtionally I would add some scenario that came up today: I have a read-only structure where properties are dependent from other properties in the same structure. It would be completely sufficient to have everything set in the initialization using a compound initialization with init it is not possible for several reasons, because in the example order might play a role, so I cannot access the Blocksize within the initialization: var gridSize1 = new CudaGridBlockSize
{
BlocksizeX = 64,
BlocksizeY = 1,
BlocksizeZ = 1,
GridsizeX = ((width - 1) / BlocksizeX / 4) + 1,
GridsizeY = ((height - 1) / BlocksizeY) + 1,
GridsizeZ = ((depth - 1) / BlocksizeZ) + 1
} so I came up with the idea of using withers but found that also here I have no access to the original source context is lost: var gridSize1 = new CudaGridBlockSize
{
BlocksizeX = 64,
BlocksizeY = 1,
BlocksizeZ = 1
}
with
{
// Accessing gridsize1 does not work
GridsizeX = ((width - 1) / gridSize1.BlocksizeX / 4) + 1,
GridsizeY = ((height - 1) / gridSize1.BlocksizeY) + 1,
GridsizeZ = ((depth - 1) / gridSize1.BlocksizeZ) + 1
}; The current solution would be: var gridSize1 = new CudaGridBlockSize
{
BlocksizeX = 64,
BlocksizeY = 1,
BlocksizeZ = 1
} ;
gridSize1 = gridSize1 with
{
GridsizeX = ((width - 1) / gridSize1.BlocksizeX / 4) + 1,
GridsizeY = ((height - 1) / gridSize1.BlocksizeY) + 1,
GridsizeZ = ((depth - 1) / gridSize1.BlocksizeZ) + 1
}; which is not super complex but if you are working on this it might be something that also fits into this topic. |
Beta Was this translation helpful? Give feedback.
-
Came here from an 2014 SO thread LOL... /subscribed |
Beta Was this translation helpful? Give feedback.
-
This is exactly the feature I've been waiting for. var countUp = myRecord with { Count++ }; |
Beta Was this translation helpful? Give feedback.
-
I think allowing postincrement and postdecrement operators may be confusing, as they usually return the previous state as expression result, and increment the source variable. A postincrement as above could be interpreted as create |
Beta Was this translation helpful? Give feedback.
-
Just my two cents...
I don't see why you wouldn't read this the same way then. var countUp = myRecord with { Count += 1 }; They both convey exactly the same mutation on exactly the same storage location. I take it to be an issue of how we intuitively read it, but I think the fact that it's part of a |
Beta Was this translation helpful? Give feedback.
-
var count = 0;
Console.WriteLine(count += 1); // 1
Console.WriteLine(++count); // 2
Console.WriteLine(count++); // Still 2 |
Beta Was this translation helpful? Give feedback.
-
They can mean something slightly different, yes, but not in this situation. |
Beta Was this translation helpful? Give feedback.
-
Well... I can kind of see where it depends on how you look at it. But one interpretation makes sense, and one does not. We aren't keeping the value of the "expression", we're keeping the side-effect. In other words, if you interpret |
Beta Was this translation helpful? Give feedback.
-
Compound assignment in object initializer and
with
expressionSummary
Allow compound assignments like so in an object initializer:
Or a
with
expression:Motivation
It's 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 to be made. This makes it impossible to simply create these sorts of objects as a simple declaration expression, negating their use from things like expression-bodied members, switch expressions, as well as just making things more verbose for such a simple concept.
The applies to more than just events though as objects created (esp. based off another object with
with
) may want their initialized values to be relative to a prior or default state.Detailed design - Object initializer
The existing https://github.com/dotnet/csharplang/blob/main/spec/expressions.md#object-initializers will be updated to state:
The spec language will be changed to:
Detailed design -
with
expressionThe existing with expression spec will be updated to state:
The spec language will be changed to:
Design Questions/Notes/Meetings
Note: there is no concern that
new X() { a += b }
has meaning today (for example, as a collection initializer). That's because the spec mandates that a collection initializer's element_initializer is:By requiring that all collection elements are
non_assignment_expression
,a += b
is already disallowed as that is an assignment_expression.--
There is an open question if this is needed. For example, users could support some of these scenarios doing something like so:
That said, this would only work for non-init members, which seems unfortunate.
LDM Discussions
https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-09-20.md#object-initializer-event-hookup
Beta Was this translation helpful? Give feedback.
All reactions