Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Feedback on primary constructors#7667

Unanswered
KathleenDollard asked this question inGeneral
Nov 8, 2023· 17 comments· 63 replies
Discussion options

C# 12 introduces primary constructors for non-record types:https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#primary-constructors

What else would you like to see and what are the important scenarios to you? A gesture that creates properties in non-record types? Support for immutability in primary constructor parameters? Something else?

You must be logged in to vote

Replies: 17 comments 63 replies

Comment options

.. Support for immutability in primary constructor parameters? ..

I'm really puzzled as to why this question is being asked. It should have been super clear to MS from feedback a month or two back that lots of developers were deeply unhappy with the way mutable "captures" were the default behaviour for non-record PCs and how any other behaviour had to be achieved via extra code and custom analyzers.

We face the situation in my present company whereby the consensus in the development department is that this feature is so far removed from being "fit for purpose", we will be implementing a custom analyzer to block the use of this feature until PCs are "properly" implemented. To put it into context, we already have stories in our backlog to update our code base to C#12 and a "brown bag" session planned to show off the other new features later this month. These are developers that are normally enthusiastic about new releases. This is the first time for any C# feature, in any release, that the team has even remotely contemplated blocking it.

Just get on with supporting a means of those PC parameters being "captured" via private readonly fields with the minimum code ceremony possible, as quickly as possible, please.

You must be logged in to vote
1 reply
@qrli
Comment options

While I'm not emotional on this, since this is a feedback thread :-), I'm with DavidArno. I think mutable primary constructor parameter is a bad choice.

My feeling is that, the designers eye more on the small classes scenario, where primary constructor fits nicely just like records, or senior library developers who absolutely know what they are doing. However, my gut feeling is that majority developers will use it for other scenarios: Half will be for dependency injection, 1/4 for domain objects with single constructor (DTOs use case can be replaced by records). Both are often big classes - there are more average developers than senior ones. And some classes will even span over multiple files by partial. And the code maintainer rotates. Mutation from a far away location is as tricky as mutation made by another thread, especially worse when you are reading an unfamiliar codebase.

Comment options

i want this:

classState(){privatereadonlyTreeNode<State>tree=newTreeNode<State>(this);publicStateSubstate=>tree.Parent.Value;}

You see, the main constructor with parameters. The assignment of fields using parameters is already in the execution phase of the constructor.
Sincethis can be used during the execution phase. So why can't classes that use the main constructor usethis

You must be logged in to vote
3 replies
@CyrusNajmabadi
Comment options

Because fields are assigned prior to the method body being executed. Primary constructors don't change that.

@ufcpp
Comment options

There is a suggestion in#1026, but is it not being considered very positively?

@HaloFour
Comment options

That proposal hasn't really gone anywhere. I think the issue is that doing so will silently change the order of when certain fields are initialized, which will likely be unintuitive and surprising. It also creates a dependency problem as any member initializers that depend on that late-initialized member will also have to be late-initialized, including in derived types and possibly across assembly boundaries.

Comment options

How to invoke a method likeInitializeComponent() which takes no parameters and returns nothing (this does initialize some UI components) in the context of Primary Constructor?

In general, the question here is how to do something more with a PC.

Similar to partial methods, define a construct that gets hooked up in the PC body during auto code gen.

You must be logged in to vote
16 replies
@qrli
Comment options

to handle a method call to a method that doesn't exist.

That's why I said using an intermediate call, sayInvokeFinalInitializer(object obj) - which is another function, whose implementation may be generated by runtime, while compiler merely generates that function with a dummy implementation - e.g. empty. Or better, the intermediate function could be a built-in function of the runtime.

Without special runtime support, the intermediate function would have to resort to reflection.

@HaloFour
Comment options

While I'm sure that's technically possible, I also don't think it would be palatable. There aren't any other features like that in .NET. It also doesn't solve for the addition of of a final initializer, unless the compiler emits such a call for every instance that is initialized.

@erik-kallen
Comment options

In Scala, you can just write whatever statement you want directly in the class body.

@egvijayanand
Comment options

Unless there's an option to extend this PC with a body, the applicability is limited to the Model and Service types and definitely not to UI modeling such as XAML and RAD interfaces.

@egvijayanand
Comment options

I'm proposing an approach similar to that ofModule Initializer.

Define a Type Initializer attribute and allow the programmer to mark any (private instance) method with this attribute (a method that takes no parameters and returns void) since the whole type has access to the parameters of the PC, there's no need to take any explicit parameters.

The automation needs to invoke the method with this attribute in the source-generated constructor. And validate all the rules that apply to the constructor method as if it is the Proxy Constructor (even this abbreviates to the same PC).

@MadsTorgersen@KathleenDollard

Comment options

Personally I think we should stay away from adding features to the PC (PrimaryConstructor) parameter list that makes little-to-no sense in any other parameter list.

readonly parameters? Does that make sense to add to any ol' parameter list? I think so, thereforereadonly makes perfect sense for PCs - even ifreadonly support for non-PC parameter lists don't arrive in the same release/ever.

Creating a public property? Does that make sense to add to any ol' parameter list? I think no. Feels out of scope. That said though; Could the syntax for props-in-PCs translate cleanly to some other tangentially related concept in method signatures? Possibly, but I can't think of any right now.

I think that we should not confuse Primary Constructors with "Simplifying data-modelling" because they are tangentially related at best.

You must be logged in to vote
6 replies
@DavidArno
Comment options

@AndersKehlet , "if all you have is a hammer, everything looks like a nail"

Just about any feature you can think of can be done with generators. Unions? Why bother, just use a generator:

[Union]partialclassOption<T>{[UnionCase]privateNone__none;[UnionCase]privateT__some;}

And the reason why we don't just use a generator is that the code is pig-ugly, complicated and such non-standard, non-idiomatic code leads to splintering into dev camps. Just because a generator can be used, doesn't mean it should.

Generators should be seen as meeting the requirements of domain-specific features, not to bypass proper language design for generic features.

@HaloFour
Comment options

readonly parameters? Does that make sense to add to any ol' parameter list?

I think so, yes. Given the prevalence of captures in C# I think the ability to mark specific parameters and locals asreadonly would be beneficial to avoid accidental mutation of state. I'd prefer this to apply across the entire language, but given the scope of primary constructor parameter captures I think it makes sense to start there.

Creating a public property? Does that make sense to add to any ol' parameter list?

It already makes sense with primary constructions, given records.

The question (IMO) is whether language features are absolutely necessary for this, or if it could be solved in a satisfactory manner via analyzers and source generators. Like records, I do think this is deserving of first class support.

@AndersKehlet
Comment options

@DavidArno

Generating public members for PC parameters istrivial and it doesn't make the user code ugly.

Generating unions is technically possible for reference types, though I think the shown approach is questionable.

Generating unions that are also checked for exhaustiveness is, again, technically possible though in addition to the generator it requires a decidedly non-trivial analyzer.

Generating struct unions is... maybe possible? You could have an interface as the common type, but then you can't really guarantee the hierarchy is closed, so you lose exhaustiveness. There'd also be boxing.

The language proposal unifies all of this with a consistent syntax that is considerably simpler than what users can do.

@CyrusNajmabadi
Comment options

@AndersKehlet I agree. :)

@AndersKehlet
Comment options

@CyrusNajmabadi - So... unions when? 😄

@DavidArno - btw, the reason I think your union example is bad, is that I wrote a union generator two weeks ago and the user code looks like this:

[AutoClosed]publicpartialrecordOption<TValue>{partialrecordSome(TValueValue);partialrecordNone;}

Needless to say, making the declaration syntax "pretty" is not why I want it as a language feature. Your other concerns are slightly more reasonable, though I think you exaggerate the issue with "non-idiomatic" code. I may have preferences, but I'm not gonna fortify myself in a camp. If I see an option or maybe type in a project, I'll immediately have a pretty good inkling of what it does, and I'll be comfortable using it (even though it'll likely be a mere pseudo-union, an abominable pretender).

Comment options

Asking for three features working together: support for always-enforced construction preconditions, support forrequired init immutability, and support forwith working with these features.

  1. I would like to be able to capture in a primary constructor arbitrary preconditions/invariants that must hold when a given immutable class is constructed.
publicclassTriangle(intx,inty,intz){primary_ctor_body(){// if for any of the x, y, z the sum of the two other values is shorter or equal,// throw an exception the triangle construction precondition / invariant was violated// because it is not possible to create a valid triangle with these lengths.}}
  1. I would also like to be able to,in effect (actual syntax can differ), denote the primary ctor params asrequired init for reasons listed inObject Initializers with theinit accessor (which was added based on mydocs issue #35014), specifically:

Required init-only properties support immutable structures while allowing natural syntax for users of the type.

Now, with 1. and 2. together I can construct animmutable object (due toinit), with the flexibility of using object initializers for some of the members, with the ability to always ensure everything is well-constructed (due torequired) and ensuring the object obeys the construction preconditions (as they are enforced by the primary ctor which is always called).

  1. If then I could also usewith expression likeTriangle newTriangle = oldTriangle with { y = 10 } which would automatically call the primary ctor with appropriate arguments and check the preconditions, creating new immutable triangle, that would be awesome!
You must be logged in to vote
2 replies
@vladd
Comment options

As the property initializers run strictly after the constructor finishes (including derived ones), it seems that you are asking for a post-construction, post-property-initialization method. I think I’ve seen a proposal about such a method, but it was separate from the primary constructors.

@konrad-jamrozik
Comment options

Thank you@vladd for the clarification. Bottom line, conceptually, I am hoping for an ability to enforce "object construction invariants". It pains me a bit that currently if I want to do it, I cannot use primary constructors, because they don't have the body of a normal boilerplate-heavy constructor where I would usually put such invariants.

It would also be nice to makewith expressions work with that, but as you just clarified, this doesn't seem to be easy to do because the property initializers run after constructors, and I understand the prop initializers are used by thewith expressions.

Overall, looks like the language is adding various nifty features but because they don't work well together, I cannot really use them. Bummer.

Comment options

Now anything can be initialized anywhere. Feels like C# is moving towards javascript.

You must be logged in to vote
0 replies
Comment options

It would be nice if the behavior of properties could be defined on the primary constructor parameters themselves as closely as possible to the normal auto-properties themselves. I mean any or all of:

  • visibility,
  • mutability (readonly), although I believe this should've been the default anyways (I don't see a use case for the contrary),
  • get-only or alsosettable (optionally limiting visibility on thesetter);init makes no sense in this case, I believe.

Probably by reusing the relevant property syntaxes. Maybe:

publicclassPerson(publicstring Name{get; privateset;}){}

I believe that's as far as I would go. If you would need additional possibilities, like inheritance modeling with virtual members, there's the "classic" way to define properties.

Also, I believe final initializers would be nice to have.

My most pressing problem with primary constructors at this point is that there's no control over the capturing, thus, it's relatively easy to reference the constructor parameter instead of a propertyby mistake, for example:

publicclassPerson(stringname){publicstringName{get;set;}=name;publicoverridestringToString()=>name;// Oooops, this should've been "Name".}

As per@CyrusNajmabadi, this seems like a non-issue.

You must be logged in to vote
4 replies
@Krzysztof318
Comment options

I don't think putting full property declaration in primary constructor is a good idea.
Mutability of course but others no.

@CyrusNajmabadi
Comment options

@yugabe That mistake seems hard to make. First, the IDE won't even recommend 'name' at all, once it is captured. Second, if you were to write that, you are immediately told:

Parameter 'string name' is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.

So i'm not sure how you're ending up in that state.

@yugabe
Comment options

Thanks@CyrusNajmabadi, I didn't know there were analyzers for these. I remember there was talk about this around design, but wasn't solved yet then. Consider my fears alleviated.

@CyrusNajmabadi
Comment options

@yugabe You're welcome :)

Comment options

I wonder if too late to suggest parameter in the PC to be public (instead of private) by default, similar to that of record type. Immutability can be enabled explicitly.

You must be logged in to vote
1 reply
@CyrusNajmabadi
Comment options

The feature has shipped. It is very intentional that it does not create a public member.

Comment options

As I mentionedhere, I'd like to be able to create a field from the constructor parameter, so that it can be accessed withthis.

field keyword, private and read-only by default.

publicsealedclassExample(fieldIMediator mediator)
You must be logged in to vote
0 replies
Comment options

I'd like to see thereadonly modifier used to mark the parameters as readonly when they are captured.

I'd also like to see accessibility modifiers applied that would promote the parameter to a named member within the type.

The two could be combined to promote the parameter to areadonly member.

publicclassFoo(stringfoo,// regular mutable capturereadonlystringbar,// readonly capture,publicstring baz,// public mutable fieldpublicreadonly string qux// public readonly field){// ...}

Inline declarations of properties could be interesting also, although I wonder if that's a step too far, especially as I imagine that there's a limit to how complicated a property could be. An auto-property could be palatable, but a partial auto-property or a full property?

You must be logged in to vote
4 replies
@DavidArno
Comment options

Sorry,@HaloFour, but the idea that I'd have to useprivate readonly to achieve a read-only capture is just awful. Private is the default throughout the language: let's keep it that way.

Edits to HaloFour's comment addresses this point, so it's retracted.

@HaloFour
Comment options

I was suggesting two separate features there, that could be combined.readonly to achieve readonly captures, and accessibility modifiers likeprivate to achieve promotion to a proper field. If you wanted readonly captures, but not a proper field, you would only usereadonly.

Updated example to show some combinations.

@bzbetty
Comment options

public Foo(init int bar) {   public int Bar {get; private set; } = bar;}

wondering ifinit could be used to limit usage of the parameter to initialization only (eg can't be used in another method)\

@glen-84
Comment options

Maybepublic andprotected should create properties, andprivate should create fields?

Comment options

For those interested by this feature, but want more control over the generated field, I've created a NuGet package which allows you to put[Field] attribute on a primary contructor parameter, to generate aprivate readonly field. and also to forbid you to use the parameter inside theclass/struct.
NuGet:FaustVX.PrimaryParameter.SG
GitHub:github.com/FaustVX/PrimaryParameter

You must be logged in to vote
13 replies
@taori
Comment options

@CyrusNajmabadi Thanks. I've seen it, but i wanted to give the feedback, since adding dependencies is not always a way that is available in every development environment

@CyrusNajmabadi
Comment options

Not sure what you mean by adding a dependency :) SGs are something you can just author. They only generate code you would write yourself. Just in a way that is automated so you don't have to manually do it :)

@taori
Comment options

Oh i just meant adding a nuget package which has the source generator as "dependency". obviously it's not really a dependency per se, but a package that is added to the project. I sometimes have customers who are very restrictive with package references

@CyrusNajmabadi
Comment options

@taori this would be for your own benefit. This would not be shipped to your customers. This is about addressing hte point you made about: Sometimes i have 5-15 DI services for UI classes, and from an intellisense perspective _someService just gets my intellisense into a direction way faster.

For your local library/app development you'd use a generator to take care of that drudgery. But you wouldn't be shipping anything new (esp. nuget-wise) to your customers.

@CyrusNajmabadi
Comment options

Note: This is how Roslyn itself works. We have generators that take care of stuff for us. But we dont' ship the generators to customers. We only ship our normal compiled libraries, which contain the compiledoutputs of the generator.

Comment options

Can we have a #pragma or something, such that the fields names can get a prefix underscore?

for example,public class Person(string name) would declare the field as_name instead ofname from inside the class.
(but from outside the class, the constructor parameter should still appear asname in Intellisense and such)

Alternatively, we could declarepublic class Person(string _name) and have a way to have the initial underscore be removed from the Intellisense constructor parameter names (cleaner).
Could also be something likepublic class Person([DisplayName("name")] string _name) to have more control on the "outside" name of that parameter.

You must be logged in to vote
4 replies
@HaloFour
Comment options

You could use a source generator that automatically emits code to initialize fields using any name you want. It could be paired with an analyzer that then makes it an error to reference the parameter as a capture outside of the construction lifecycle.

@wiz0u
Comment options

I see C# 12 primary constructors as syntactic sugar to have trivially simple automatically initialized fields.
Having to write a source generator to address something many developers use (starting their private fields with an underscore) sounds no longer trivial.

@CyrusNajmabadi
Comment options

I see C# 12 primary constructors as syntactic sugar to have trivially simple automatically initialized fields.

That is not correct. They are just another way of defining a constructor with parameters. Just with parameters that can be used by the members nested within the type/cosntructor.

@CyrusNajmabadi
Comment options

Having to write a source generator to address something many developers use (starting their private fields with an underscore) sounds no longer trivial.

It's trivial to just use that (and have other developers use it) once written once.

Comment options

Just want to add some positive comments among all complaints. I like the feature. It reduces the ceremony for DI, and the lack of readonly isn't too big of a deal. Just don't mutate the value, and you'll be fine. I don't know if thereadonly modifier in any DI'd field anywhere in my code ever has saved me from accidentally mutating it. It's just something you don't do because there is no good reason to do it.

You must be logged in to vote
3 replies
@DavidArno
Comment options

Ah, the old "you don't need a seat belt. Just don't crash your car, and you'll be fine" line of reasoning 😂

@CyrusNajmabadi
Comment options

Given such a small incidence of this actually happening, I'd say that is fairly accurate. You don't need every safety measure. Not if it's not really that necessary or valuable.

@erik-kallen
Comment options

@DavidArno I do use a seat belt, but I do not use a helmet when driving. I assume you do.

Comment options

I would like to be able to affect the generated field name, i.e. to allow underscore prefixes like requested here:#7565

I know that primary constructor parameters are technically parameters andmight not be kept as fields/properties but they are in the current implementation, I even found it mentioned on the MS page:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors

When a primary constructor parameter is used, the compiler captures the constructor parameter in a private field with a compiler-generated name. (...) If the type includes the record modifier, the compiler instead synthesizes a public property with the same name as the primary constructor parameter.

I also find it confusing that primary constructors work differently for records than for (typical) classes, i.e. I cannot applyfield: orproperty:-targeted attributes on PC parameters in aclass but I can when it's arecord [class].

You must be logged in to vote
3 replies
@CyrusNajmabadi
Comment options

@baterja the exact purpose of using records is precisely to get members and proper behavior around those Members. When using a non-record type we don't want that since that's what records are for in the first place.

@baterja
Comment options

I know, but only because I've been using C# for over 10 years and was up to date when those changes were introduced. Newcomers think primary constructors will behave alike in records and not-records, and also becauseTypeScript allows similar syntax (class X { constructor(public readonly x: number) {} }). So I consider it counter-intuitive.

@HaloFour
Comment options

TypeScript primary constructor syntax involves providing an accessibility modifier.

The behavior of primary constructors in C# is identical to that in Scala and Kotlin, and it's not considered confusing in either of those languages. It's expected that if you want the class to behave as a data carrier and define the schema of that data based on the constructor that you need to opt-in to that by making the class an equivalent of a record, viacase ordata.

Comment options

This feature won't be useful for me until primary constructors can have bodies for logic (like the "do" block in F#). I am mostly stuck with normal constructors for now, but if primary constructors eventually get logic bodies, I would probably be able to replace almost all of my normal constructors with primary constructors. The syntax for primary constructors is just must nicer overall.

You must be logged in to vote
0 replies
Comment options

USE CASE

public class Tag(string name, string value, string? description = null) : Item(name, description), ITag{  public required string Value { get; set; } = value; .....}...new Tag("Name", "Subscription")      --> Error occurs "Required member [TAG.Value] must be set in the object initializer..."

Two issues:

(1) No body supplied for the primary constructor!!!! What genius thought this was a good idea!!!
(2) I should be able to both set a required field AND define primary constructor! This code will not compile. What's the point in having a "required" keyword if I am not allowed to satisfy the required field via the primary constructor? Morever, this annotation [SetsRequiredMembers] seems to be an afterthought.

In general, this whole attempt to shoehorn a “primary constructor” into the language has not been thought through and is incomplete at best.

I my opinion, new isn’t always better. In this case introducing a half-baked idea all for the sake of brevity is just plain stupid.

You must be logged in to vote
1 reply
@stepanbenes
Comment options

Why do you use therequired keyword if you are providing a value from constructor? Either remove therequired keyword or provide value via object initializer and it will compile.

Comment options

I propose the following solution for enhancing functionality within the context of PC:

For backward compatibility, the constructor method should retain the same name as its containing type.

Historically, nearly all access modifiers have been employed to define constructors, except for the newly introducedfile.

Thus, this new modifier can be used to define the additional method that gets invoked as part of the PC, with the invocation order determined using an attribute.

publicpartialclassMainPage(MainViewModelviewModel):ContentPage{// Newly proposed method that acts like a constructor body, gets invoked in the PCfileMainPage(){InitializeComponent();// UI InitializationBindingContext=viewModel;}}
You must be logged in to vote
0 replies
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Labels
None yet
27 participants
@KathleenDollard@DavidArno@glen-84@bzbetty@erik-kallen@vladd@333fred@ufcpp@hannah23280@konrad-jamrozik@CyrusNajmabadi@FaustVX@taori@HaloFour@qrli@stepanbenes@wiz0u@baterja@yugabe@KennethHoff@11clockand others

[8]ページ先頭

©2009-2025 Movatter.jp