-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal]: Compound assignment in object initializer and with
expression
#5176
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? |
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? |
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. |
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 */ }; |
Thanks @bartdesmet I'll definitely bring up the question if we want to support this in |
This has happened to me as well. |
For me especially operator |
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,
},
}
}); |
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],
}; |
|
with
expression
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. |
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. |
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". |
I disagree. I think we do, and part of that is not blurring the lines more by using methods in these scenarios. |
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? |
Wrong question. Language features are tremendously expensive. The question needs to be Why? And it needs a compelling answer, far more than Why not? |
This is not the long term trend at all. And you can see very popular and very verbose languages that show that.
This is not a pro. Clarity is what matters, not terseness.
This already works in C# today. Just do this: new Whatever
{
Children = { ... }
} This will add to the children list. No need to
Or we can just not allow it at all if it's not going to be a good thing :) -- note @najak3d please use normal github markdown markers around your code. e.g. |
You can already do this in C# today with: new Grid()
{
Prop1 = propVal,
Prop2 = propVal2,
Children =
{
new Button()
{
Clicked += EventHandler, // will be supported by this proposal.
},
new Button()
{
// compose Button2 here
}
} // end of Children
} |
The two aren't strongly correlated. Shorter means more opportunity for confusion (does the code you're reading do what you think it does). It's so easy to go past concise to cryptic, particularly when you're familiar with something and your readers may not be. Real world example: What does SGTM mean? Sounds Good To Me or Silently Giggling To Myself? Assuming the wrong one once caused me some minor embarrassment. |
I am very impressed with the community here. This was my first day visiting these forums. I'm a 51 year old programmer of C# since 2003, and want to see this language "rule all" -- but it appears to me that C# has lost ground in the last 10 years, mainly due to Microsoft doing a piss-poor job of making Xamarin Forms competitive/good (they work worse than WPF). Thus we have competitors like Flutter taking market share from what would have normally been done in C#. One area where C# is hobbled is in the area of UI construction, which gave birth to XAML, which generally sucks. It's unfortunate that C# has required hackish full-suites of extension methods to achieve a notation that competes with Flutter. I think this functionality shouldn't require this much work. Yes, I agree that too-terse/overloaded statements is bad programming. I tend to go the verbose route, not combining things into single lines (e.g. method(anotherMethod(args)); IMO is normally bad). However, for UI composition, this domain/context really deserves better native support from the C# language itself. It doesn't seem to be much of a stretch to simply say "any method can be called from the Object Initializer block" -- and for safety, an order could be enforced such that "Init-Only Properties must always be called first", then after that, anything goes. This would be: (a) Terse, (b) Clear, and (c) enable UI composition naturally. It simply works similar to how VB "with" statements used to work, in that all operations are done on the main source object. They of course all operate In-Order. It should be the equivalent of making all the same calls using a local variable. In the Object Initialization block, it would just work like a "with" statement. Very simple, concise, clear, and extremely useful for certain contexts. For contexts where it's not appropriate, just don't use it. But in short, even if you use it "inappropriately", what's the harm? It works the same as if you just made all the same calls (in the same order) using a local variable assigned to the new object. So it just amounts so a bit of very useful syntactic sugar, that makes UI composition syntax "natural/built-in", rather than an obtuse hack. I think you are all awesome. Thank you for your attention, and feedback. This has been very encouraging to witness this energy , genius, and tone of the discussions. Kudos to the whole group of you. |
Then what is the reason to allow general compound assignment, instead of just subscribing events, in object initializer? As I said, I agree that event subscription is important in object initializer, but I don't think allowing it for fields and property is anywhere better than calling methods, at least based on your initialization (or construction) vs mutation logic. If I understand correctly, var item = new Item()
{
Value += 1,
}; does not distinguish whether the modification of Another reason I can think of why you say you do is that you think any modification of a field using compound assignment in object initializer can be seen as initialization instead of mutation. If that is the case, you are defining what is initialization/mutation by where the code is written, and that also means whatever we add to object initializer (e.g. method calls) will also be considered as initialization, which should be fine according to your initialization/mutation standard. |
It seems to me that "immutability" is currently enforced only by "readonly" modifiers or using "init" instead of "set" for properties. Restriction: Readonly method - cannot use Init block -- all values must be set using a coded constructor. Too Loose: Init methods - currently Order-of-ops in Init Block do NOT enforce "init methods must be called first". This seems unfortunate, because in a small way, this does not really enforce immutability, at least during the Init block. As it currently stands though, the Init Block simply runs everything "in order" as you wrote it. That's pretty simple. So if we want to enforce "immutability" via Init Blocks, then we need to either:
So in this simple/sensible fashion, C#11 Init-Blocks can offer better "immutability" support, while also adding very useful syntax for nested composite construction (e.g. UI's). Adds magnificent benefit, without confusion or downside. |
You are being completely arbitrary. You reject one 'native' c# feature for being 'hackish', but then want some other features to do the same thing. Extension methods are native. They're part of the language. Avoiding features that have been around for 15 years and are widespread and fully embraced by the ecosystem just because you don't like them is not going to motivate is to create something new. |
The NATURE of these extensions is hackish. For example, Button already has a "Text" property, but because it does not "return Button", we cannot use it! Therefore we have to create a NEW "Text()" method extension to use in it's place... to do the same thing, only it returns Button." So it forces 100% replacement of all existing methods/properties, to support this syntax. But if the Object-Init syntax were simply a bit more functional -- then we wouldn't need to use all of these hacked extension methods. Extension methods to add functionality is a good idea. And I'm glad they are available for scenarios like we are doing now -- because they allow us to "hack C# language" to essentially create a notation not currently supported by C# (but should be supported, IMO). Extensions that are awesome are ones that extend collections to give you a "Count()" method for an IEnumerable, or the various other Linq extensions. Those are all nice, but are ADDING functionality. In our case, our methods are 90% NOT adding functionality, but are simply "redoing existing properties/methods" to make up for an inadequacy of the C# language itself (which is supported by Flutter/Dart and others). That's why it's a hack, vs. the many other uses of Extensions, where it's not a hack. IMO, in a way, though, most usages of Extensions are a bit hackish in nature. In short, remove the "using ExtensionsNamespace" and your code suddenly breaks. So it's a bit hackish/weird, and in many cases have been overused by some. The best we can say about Extensions is that they are VERY helpful in overcoming awkwardness that would otherwise result without them, and so they are "good/useful" and we're glad they exist. If you can write code that relies upon 1000 Extension methods, vs. writing code that needs almost ZERO extension methods, using nearly the same syntax -- it's preferred, by far, to have the code non-reliant upon these extensions. In short, I'm not calling the "syntax for UI composition" hackish; but I am calling the current method of using 1000's of extensions to make this syntax possible, "hackish", because it is. But I'm glad this hack works, because at least we do enjoy the benefits of C#-markup-composition syntax. It's just unfortunate that this syntax requires so many awkward extensions to make it possible. |
Alternatively, i think that auto-fluent for void methods could work out for him. That's something i'm far more likely to champion. |
ah yes, that would be significantly less design work. and certainly would make things better. |
I don't know what the scenario is where I will think that we must have a forward-pipe operator. Maybe someday some important pattern (model-view-update?) will be so much better that we will be made to do it, but I am not seeing that happening in the short term. |
IMO, "fluent-everywhere" is far more likely to cause mistakes/confusion than what I'm asking for. In "fluent-everywhere" so you used to have a function that returned some other object, and so the code that uses it was expecting an entirely DIFFERENT object to be returned, and was operating on it... Now you change the return type to "void" -- and there are NO COMPILER ERRORS -- the calling code now auto-operates upon the "void" which is interpreted to be the hosting object... Oops. IMO, that is likely a very bad idea. |
Thank you for your thoughtful response above. Even for "rich" objects Serialization is heavily used in many settings. Construction via serialization is everywhere. What I'm wanting is simple, clear, and non-dangerous in nature. It ONLY applies to the "Init Block" and simply makes the Init-Block work like a "Wither" block, with one caveat that the method calls must be at the end of the block, following the property setters. So when compiler sees the first ".method()" statement, it completes construction, and the remainder of the methods calls are now operating on a constructed object. It would be equivalent, exactly, to the long-hand notation. It's simple, scoped short-hand, to aid in complex code-based compositions. My request isn't truly "Fluent" -- it's a simple attractive replacement for Fluent, with notation that looks about the same, but does NOT require methods to return back the object instance to the caller. This does NOT interfere ANY with Serialization concerns. In fact, in some cases, it'll enhance Custom Serialization logic. Imagine a custom serialization method like this: MyType Deserialize(Reader reader)
{
MyType obj = new MyType()
{
Name = reader.ReadString(),
.ApplyStyleByName(reader.ReadString()),
.SetPosition(reader.ReadInt32(), reader.ReadInt32()),
.SetMargins(reader.ReadString()), // string representation of the Margins - this would be an extension method
FontSize = 12 //>> COMPILER ERROR.. Property settings must precede method calls
};
} Serialization could simply choose whether or not they prefer this short-hand or not. If not, then it makes zero impact on concerns for Serialization and Records, or PODs. |
I created a new-clean proposal here, that presents what I'm aiming for more clearly from the start. My new proposal fully accommodates/solves this proposal's objectives, and then some. |
Regarding serialization -- there is one mode of serialization that I've seen, which is high-performance, and easy to debug, and maintain backward compatibility. Instead of saving data, it saves C# code, which is then compiled, and has the code that directly instantiates an object. So just imagine a serializer that generates the C# that initializes each object, with the values directly inserted into the C#. Then compile that, and load it as a dynamic DLL, and simply run it. Backward compatibility becomes not too hard, because you approach it simply as you would approach backward compatibility for your API... if your API is backward compatible, then the old serialized DLL's will also work for the new API. Other great thing about it is transparency and ease of debugging -- you aren't dealing with binary/string data... but you can simply look at it like raw code. And even see compiler errors, where backward compatibility might be broken. In ways, it's the best of many worlds. I've used this approach for complex 3D scenes in the past. Having nice C# syntax for construction would be useful for this type of serialization, especially. This mode of serialization is how WinForms works as well. |
This would be a library concern, not a language concern. |
You have just re-invented binary serialization from first principles. While this design is attractive in its simplicity of use it has several irreconcilable security flaws that have caused many real-world security exploits. Security aside, what if you want to inter-operate with non-C# code? Being able to easily host a REST api that returns json is one of the cornerstones of why people use ASP.NET or microservices in general. The default design in C# cannot be "other forms of serialization are second class" when they represent 90% of what people are using serialization for. |
What that generated C# looks like is a language concern. Is it succinct, or verbose? Does it require extensions to make it work, or does C# syntax allow for the simpler syntax naturally? |
Correct. It has it's downsides. For my experience, the Scene could also have Custom Code, already. And so there truly was nothing you could accomplish in this sandboxed C# that you couldn't already accomplish via custom C#. So for this scenario, that type of security wasn't an issue. For MOST situations, you are 100% correct. Which is surely a major reason why it's not used much. In our case -- to achieve reasonable security, we just compiled the code run-time, so that we could do appropriate checks and confined the compiler environment to not-compile insidious code. And so we serialized the Text, not the DLL directly. That said -- this makes succinct syntax even more attractive for this scenario. |
No, this is still very much a concern for the library. I understand that you're trying to make this point in the context of your preferred Dart/Flutter way of doing things, but given designers have existed in the .NET ecosystem since inception I think that argument is going to fall flat. Designers don't have a problem in this scenario. |
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. |
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. |
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. |
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. |
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. |
Just confirming, this will work, right? var x = new Class { StrProp += " suffix" }; |
I would think so. |
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. |
Came here from an 2014 SO thread LOL... /subscribed |
This is exactly the feature I've been waiting for. var countUp = myRecord with { Count++ }; |
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 |
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 |
var count = 0;
Console.WriteLine(count += 1); // 1
Console.WriteLine(++count); // 2
Console.WriteLine(count++); // Still 2 |
They can mean something slightly different, yes, but not in this situation. |
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 |
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
The text was updated successfully, but these errors were encountered: