dotnet / csharplang

The official repo for the design of the C# programming language
11.59k stars 1.03k forks source link

Primary Constructors (VS 17.6, .NET 8) #2691

Open YairHalberstadt opened 5 years ago

YairHalberstadt commented 5 years ago

@MadsTorgersen added a proposal for primary constructors yesterday: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md

I wanted to link the proposal to the issue for primary constructors, but I couldn't find any, so I thought I'd create this issue as a dumping ground for discussion.

NOTE:

I will interpret upvotes and downvotes as upvotes/downvotes on the proposal.

Meeting Notes:

JeroMiya commented 4 years ago

If you put aside the "C# shouldn't add any new features" style arguments, I don't see this proposal as controversial - except perhaps a minor quip about PascalCase vs camelCase when it's combined with record types.

WrongBit commented 4 years ago

@HaloFour : I see nothing "constructive" when you try to "protect" doubtful ideas. I see group of people who put their wishes on discussion, but completely deaf to any contra-arguments. It's childish behaviour - "I want this feature at any cost! I don't care what you say!" - that's what I read from this topic. You annoy me by this behaviour, my nerves are not unlimited. I still insist to NOT DIRT the language with any caprices you push there. We already have clear, obvious syntax for class & its members. What you offer just makes life difficult in a favor of ridiculous "solution" of... WHAT? Are you so tired to write constructors?? Like I said before, IF this feature was clearly useful, NOBODY will speak against it. Reality shows it is not - you make quite questionable "improvement" and make difficult even to people to recognize the code. Remember, programming is not only WRITE code, but read too! But seems you're "write only" developer. Pity to see people like you in decision team.

YairHalberstadt commented 4 years ago

@WrongBit

As you can see above, I was initially against this feature. Since then I've started using Scala. On the whole I don't love the language, but one thing I really appreciate is primary constructors. I constantly find myself accidentally writing them in C#, and then having to rewrite it.

I would suggest giving a language which has support for primary constructors a go. You night be pleasantly surprised :-)

WrongBit commented 4 years ago

+32 votes against -36. If you are professional developer, you already feel the smell of "doubtful idea"! GOOD idea will never ever assemble so much downvotes! (more than half!) I have no more arguments except said before. C# team just annoys me with incompetent approach. Your manager definitely blind if he doesn't see how unprofessional your decisions.

theunrepentantgeek commented 4 years ago

@WrongBit

Like I said before, IF this feature was clearly useful, NOBODY will speak against it.

Different people have different priorities, different experiences, different priorities, and different preferences for style. They naturally reach different conclusions.

As has been said many times in the discussions in this repo, there has never yet been a feature added to C# that wasn't violently opposed by some users.

Pity to see people like you in decision team.

So far as I know, @HaloFour doesn't work for Microsoft. He's just someone with a passion for the language who's been around long enough to see how things work.

Your manager definitely blind if he doesn't see how unprofessional your decisions.

This repo isn't where decisions are made. The C# language design team takes input from a wide variety of sources. To be clear, the LDM folks have made it clear that this repo is a valuable source of opinions and insight, but it is just one of many channels of information.

I'd also suggest that you should review the code of conduct. I suspect you're skating right up against the line of acceptable behavior. Perhaps you should focus on the issues and leave the personal attacks out of it.

Grauenwolf commented 4 years ago

Are you so tired to write constructors??

Yes I am. As already stated by others, a large percentage of code consists of error-prone boilerplate constructors.

This is like auto-properties, which most people agree was a good feature for removing tedious code.

I stress "most" because some people still don't like it. There will always be naysayers for every feature.


As for the votes, that is misleading. Some are voting against it because they don't like the idea at all. Others are voting no because they don't think it goes far enough. Without a clear separation between the two it doesn't provide much insight.

notanaverageman commented 4 years ago

I downvoted this because I don’t like the proposed syntax. Yes, this is a useful feature and we should reduce the boilerplate in the language. But, it should not be in a hacky way.

I have written before, code generation is the perfect solution for these type of problems. I know that the code generation proposal is stalled, but if the team can afford adding a new syntax, they can afford an internal code generation feature just for this problem, too.

By the way, both the internal and the external impact of using code generation will probably be much less than the other way around. (Just adding a new attribute and generating fields similar to auto properties.)

CyrusNajmabadi commented 4 years ago

but if the team can afford adding a new syntax, they can afford an internal code generation feature just for this problem, too.

This is not true. There are vastly different costs between the two.

By the way, both the internal and the external impact of using code generation will probably be much less than the other way around.

This is also not true.

JeroMiya commented 4 years ago

Just a few additional points:

Some of my thoughts, feedback:

I don't like the default constructor block syntax in the proposal. It needs a keyword similar to the static constructor syntax.

/// Not written as an actual use case, only to demonstrate syntax
class SimpleClass(int value)
{
    new { // Alternatives: "constructor", "SimpleClass" (i.e. name of class without argument list)
        if(value < 0) { value = 0; }
    }

    public int MyProperty => value * value;
}

As a side note, Scala implements default constructor bodies by letting you write code inside the class definition itself, which I do not necessarily suggest for C# (going to guess it would not work in the existing grammar). Included for completeness:

class SimpleClass(var value: Int) {
  if(value < 0) { value = 0 }
  def myProperty = value * value
}
Grauenwolf commented 4 years ago

Some times I wish it were true. Some AOP capabilities would be really helpful.

But it would also dramatically increase the complexity of any code base that actually used it. So when I'm doing evaluations of legacy code I would probably hate it.

egorpavlikhin commented 4 years ago

Would be great to have TypeScript syntax

class A {
  A(private IService service) {
     // this.service is usable here
  }
}
chaowlert commented 4 years ago

For those who want Primary Constructor for dependency injection, I created a source generator to do this. Give it a try. https://github.com/chaowlert/PrimaryConstructor

Declare class with partial, and annotate with [PrimaryConstructor].
And then you can declare your dependencies with readonly fields. (for those who have Java background, this library is very similar to Lombok's RequiredArgsConstructor.)

[PrimaryConstructor]
public partial class MyService
{
    private readonly MyDependency _myDependency;

    ...
}

When compile, following source will be injected.

partial class MyService
{
    public MyService(MyDependency myDependency)
    {
        this._myDependency = myDependency;
    }
}

NOTE: Visual Studio version 16.8 and above is required

przemo098 commented 3 years ago

In my opinion this feature should be implement in TS way: https://dev.to/satansdeer/typescript-constructor-shorthand-3ibd When you add access parameter to constructor field then it should be automatically binded to the class...

Thaina commented 3 years ago

I don't against the concept but I don't feel good about this

For the above example, the effect of the class declaration is as if rewritten like this:

public class C : B
{
    public C(int i, string s) : base(s)
    {
        __s = s;        // store parameter s for captured use
        a = new int[i]; // initialize a
    }
    int __s; // generated field for capture of s

    int[] a;
    public int S => __s; // s replaced with captured __s
}

It should always generate the stored parameter as the exact same name as what we specified there. s should be s not __s. At least it would not confused when using reflection

HakamFostok commented 3 years ago

I read the comments above which is related to the Typescript way,

I really (even after I read the comments) do NOT understand why the typescript way was not adopted??!! it is really the best way to do that, I was praying that this idea will come to the minds of C# designers, but when I read that the idea was discussed and then was rejected (especially because of naming inconvenience) I was disappointed too much.

I do not think a great idea like this (which really affect positively the conciseness of the code) should be abandoned because of naming concerns,

The developer can break the naming conventions for everything, the naming convention must be left to the developer, C# should provide the feature and it should not be responsible for the misuse (not to mention breaking conventions) of it.

breaking the naming convention is the fault of the developer, not the fault of C#.

by the way, why not suggesting a naming convention with a capital letter to determine this is property side-to-side with parameter, this will make the intention clearer


public class A
{
     public A(private IService Service,    // this is will be captial letter to emphasize that this is not a normal parameter
                   int paramater)                   // this is will be small letter as it was before
     {}
}
HaloFour commented 3 years ago

@HakamFostok

C# allows for multiple overloaded constructors, each with a unique set of parameters and their own bodies. TypeScript does not, it requires all constructor overloads to call into a single constructor in which the shorthand properties can be declared. That limitation is what enables the language to support property shorthand. Otherwise the language would have to deal with different constructors that declare different sets of properties.

C# already has primary constructors at the type signature level with positional records. IMO it makes sense to continue to build on that syntax vs. adding yet another way to declare the same thing. Quite a few other languages have the same constructor syntax.

HakamFostok commented 3 years ago

@HaloFour

Thank you very very much for the informative response, I really now understood the complications of that, of course, these are very important concerns to consider.

because I still think that great things should not be abandoned when obstacles show on the way, I will still make some endeavours there are many ideas that could come to mind, even some of them could appear completely crazy, so please bear with me

  1. The class which use the Typescript-way could be restricted to have only one constructor, at the first this could be very aggressive restrictions, but from (at least) my own experience this will be so much helpful. .NET core has introduced dependency-injection (which one of the greatest things done for the .NET, IMO) The class that constructed with DI-Container typically has one constructor, all of its parameters is provided by the DI-Container ,all of them are readonly

here is some code to make this more lively

public class Service
{
      private readonly IService1 _service1;
      private readonly IService2 _service2;
      private readonly IService3 _service3;
      private readonly IService4 _service4;

     public Service(
        IService1 service1,
        IService2 service2,
        IService3 service3,
        IService4 service4)
    {
          _service1 = service1;
          _service2 = service2;
          _service3 = service3;
          _service4 = service4;

    }
}

the above class is a common pattern in a lot of areas,

What I mean that even if the compiler restricted those classes to have only one constructor and prevented other constructors to exist, it still be ok for 99.9% of the classes that use DI-Container to fulfil its constructor's parameters (Most of those classes are following the 'Service' Pattern, as described in the DDD)

  1. if the previous suggestion is a little bit aggressive, we can make it looser a little bit as you said

C# allows for multiple overloaded constructors, each with a unique set of parameters and their own bodies. TypeScript does not, it requires all constructor overloads to call into a single constructor in which the shorthand properties can be declared.

ok, why not restricted the classes which use the typescript-way to make all the constructor converge in only one constructor, and allow the definitions of the properties only on that converge point. As you said it works in typescript (so it could work here also)

I think those restrictions could make this choice viable again.

Thank you again Sir for your time.

Waleed-KH commented 2 years ago

I also think that this feature should be implemented in a Typescript way, but also have the record way as a shortcut, for example:

public class Service(IService1 _service1, IService2 _service2, IService3 _service3)
{ }
public class Service
{
    public Service(
        private readonly IService1 _service1,
        private readonly IService2 _service2,
        private readonly IService3 _service3)
    { }
}

The above code is the same and would generate the following:

public class Service
{
    private readonly IService1 _service1;
    private readonly IService2 _service2;
    private readonly IService3 _service3;

    public Service(
        IService1 service1,
        IService2 service2,
        IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;      
    }
}

There are many benefits in the Typescript syntax (control access modifiers, add more logic to the constructor...), but you would still have the option to use the record syntax depending on the case.

@CyrusNajmabadi

This has the negative problem of the parameter and field having inconsistent naming with the naming of the .net ecosystem.

I believe this issue is still present in the currently proposed syntax, why not let the compiler 'normalize' the parameters in the generated constructor (remove the prefixed _ in class primary constructor, PascalCase to camelCase in record primary constructor) (see the above example), and have an option to disable this behavior

CyrusNajmabadi commented 2 years ago

We've made an explicit decision that we do not want to be in teh space of having to understand anything about naming, let alone how to canonically map between names.

Waleed-KH commented 2 years ago

I totally understand that, but using this feature right now with records result in inconsistent names with the naming of the .Net ecosystem (as you mentioned before): image and this issue would still exist with the Primary Constructors for classes (should we name the primary constructor parameters as private fields or as method parameters? then how it will be consistent with the naming standard?)

Personally, I would like to see the feature get implemented (with Typescript way 😁) without all of this, but the naming issue seems to affect the decisions for the feature. That's why I believe we need a way to generate code that is consistent with the .Net ecosystem (compiler option, attribute, .....).

Finally, I would really hope that you reconsider the Typescript way, I had a case where I was limited with the current record way and switched to the normal constructor.

JeroMiya commented 2 years ago

It seems to me that the .NET argument naming convention (if not officially then de facto w.r.t. documentation, samples, and common practice) already has been updated in the case of C# 10 record types to recommend PascalCased arguments when the compiler generates properties for those arguments. If a primary constructor proposal were to be adopted then I presume the same convention would be used there.

As for the typescript like proposal, I'd say my vote would be for more of a Scala-like syntax.

HaloFour commented 2 years ago

With records it's a bit different as the primary constructor parameters are promoted to properties which creates this mismatch in naming conventions between parameters and properties. With non-record primary constructors I don't believe this issue exists. There is no set convention as to what private fields should be named, even if you wanted to consider them as such.

JeroMiya commented 2 years ago

With records it's a bit different as the primary constructor parameters are promoted to properties which creates this mismatch in naming conventions between parameters and properties. With non-record primary constructors I don't believe this issue exists. There is no set convention as to what private fields should be named, even if you wanted to consider them as such.

Ah, yes, I see what you mean. In this case, I would think of the primary constructor arguments not as fields, but more like a closure. They are function arguments, so they use the function argument naming convention. Methods, properties, and initialization expressions simply close over the arguments (at least at a syntactic level - they might get compiled into fields plus initialization by the compiler). Thus there is no need to use other naming conventions, e.g. the leading underscore.

The only modifier I see proposed is readonly, yes? I don't see a use case for public (unless we promote such arguments to properties like with records, in which case they would be similarly PascalCased by convention), private (they are private by default), nor static (makes no sense). Perhaps protected (would there be issues with initialization in subclasses?)? For readonly, I don't see why it couldn't be supported for all method arguments for consistency.

MadsTorgersen commented 2 years ago

I've created an updated proposal. Instead of editing the above-linked non-record-primary-constructors I've made changes to the original proposal primary-constructors. Based on the discussion above and elsewhere I decided to "return to the roots" of primary constructor parameters being closed over the same way locals and parameters are closed over elsewhere. The capture may be (and very likely will be!) implemented by using fields, but that is an implementation detail.

I updated the rules so that the behavior of existing primary constructors in records can simply be retconned as these more general rules, with some additional synthesized members added. Since other aspects of records are already described primarily in terms of synthesized members, this feels like a good fit and relieves records of their syntactic "specialness".

There are many potential variations and extensions listed in the proposal, and this version has not been discussed in LDM yet, but I'm curious what people think.

Waleed-KH commented 2 years ago

@MadsTorgersen Thanks for the update, Primary constructor bodies & Combined parameter and member declarations are really cool, but they don't solve all the limitations, e.g. what if I want the Primary constructor to be protected/private and have other public constructor call it?

Why can't we have a syntax in a TypeScript way that solve a lot of the limitations?

Example:

public class C
{
    protected C(private bool b, protected int i, private string s)
    {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }

    public C(int i) : this(false, i, "") { }
}

also check the example: https://github.com/dotnet/csharplang/issues/2691#issuecomment-1077820032

I'd like to have the Scala-like syntax as a shortcut, and TypeScript-like syntax for more advance use

MadsTorgersen commented 2 years ago

@Waleed-KH:

Why can't we have a syntax in a TypeScript way that solve a lot of the limitations?

I don't think there's a technical reason why we couldn't add this on top in the future. It still suffers from the same problems as I mention with combined parameter and member declarations:

  • What if a property is desired, not a field? Having { get; set; } syntax inline in a parameter list does not seem appetizing.
  • What if different naming conventions are used for parameters and fields? Then this feature would be useless.

On top of that there's also an aesthetic/clarity argument that member declarations are sort of hidden inside other member declarations. But TypeScript seems to do fine with that.

Finally, though, with the approach you propose, ordinary constructor parameters wouldn't seem to be in scope for the whole class, which is the main point of (this version of) my proposal.

Richiban commented 2 years ago

The problem with the Typescript-style syntax is the fact that classes in C# can have multiple constructors, whereas they can't in Typescript. This means that C# has to have really careful messaging around why this is not allowed:

public class Service
{
    public Service(private string fieldA)
    {

    }

    public Service(private string fieldB)
    {

    }
}

This is a solvable problem, of course, but in my mind there isn't enough of a distinction between a regular constructor and a primary constructor. It'll be weird having to look for field definitions inside the parameter list of a constructor, instead of in the body of the class. That's just my opinion though.

Waleed-KH commented 2 years ago

Can we have access modifier for Primary constructor? I think we could insert it before the Primary constructor body

public class C(bool b, protected int i, string s) : B(s)
{
    protected {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }

    public C(int i) : this(false, i, "") { }
}
HaloFour commented 2 years ago

IMO primary constructors are not intended to replace normal constructors and shouldn't attempt to achieve feature parity with them. Start simple and iterate based on concrete use cases depending on where that drop-off happens to be, as is the approach taken with auto-properties. Attempting to add tons of syntax for the sake of having feature parity and a smooth gradient between primary- and non-primary-constructors is exactly what killed the conversation on primary constructors ~8 years ago.

CyrusNajmabadi commented 2 years ago

I agree with @HaloFour . Similar to records, the intent for me is to be really good at a reasonable subset of cases which are currently extremely verbose and overwrought. Many classes will be able to use these, but if they can't, that's ok and what existing constructors will still be great for.

RenderMichael commented 1 year ago

Attempting to add tons of syntax for the sake of having feature parity and a smooth gradient between primary- and non-primary-constructors is exactly what killed the conversation on primary constructors ~8 years ago.

Adding onto this, ensuring that there's always a non-breaking change you can make going from primary to non-primary constructor is probably the most important part of this.

Otherwise, people would get locked into their primary-constructor class and if they suddenly need an unsupported feature, they have to suffer the breaking change, depreciate the whole class, or hound developers in this repo.

KeithHenry commented 1 year ago

I like the basic idea, but I really don't like how confusing it will be with record classes:

public class MyClass(int foo, string bar) { }  // foo and bar are private fields

public record MyClass(int Foo, string Bar); // Foo and Bar are public properties

This feels like it could be very confusing - I'd add two prefixes:

Then you would have something like:

public class MyClass(field int foo, field string bar) { } // Same as proposal

public record MyClass(int Foo, string Bar); // Foo and Bar are public properties, record syntax unchanged

public class MyClass(property int Foo, property string Bar) { } // Same as a record class

public class MyClass(field int foo, protected property string Bar) { } // mix it up

I think these new modifiers should also work with the regular constructor syntax, as that will be the simplest upgrade path and easiest for anyone who also uses TypeScript:

public class MyClass { 
  public MyClass (field int foo, field string bar) {
    this.combined = $"{foo}: {bar}";
  }

The messy corner case here is multiple constructors:

public class MyClass { 
  public MyClass (field int foo, field string bar) {
    this.combined = $"{foo}: {bar}";
  }

  public MyClass (field string combined) {

  }

Does this throw an error for mismatched constructors or create all 3 fields and require them to be initialised?

HaloFour commented 1 year ago

@KeithHenry

I like the basic idea, but I really don't like how confusing it will be with record classes:

This is pretty normal. In Scala the primary constructor arguments aren't promoted to public members without a case modifier on the type. The same with Kotlin but with a data modifier. Records already buy you a bunch of additional generated members, I don't think it's weird that it's true in this case as well.

EvanMulawski commented 1 year ago

I would like to have the ability to name the backing field to match project conventions, e.g.:

class MyClass(string name as _name)
{
    public string Description => $"Name={_name}";
}
Richiban commented 1 year ago

I like the basic idea, but I really don't like how confusing it will be with record classes

I think, as with many language features, the confusion will only be an issue when the feature is new. Once it's been out in the wild for a bit the confusion will have gone; I actually think that the chosen syntax here makes more sense given how records work. This area of the language can be summed up with "This is how you declare constructor arguments for a class. If it's a record then each constructor argument gets a public property generated". When you look at it after the fact it's less confusing.

HaloFour commented 1 year ago

I would like to have the ability to name the backing field to match project conventions

Per the current spec the primary constructor parameters are not considered fields. They are captures that can be used as expressions which can result in them being promoted to fields, but they would have unspeakable names in that case.

See: https://github.com/dotnet/csharplang/blob/main/proposals/primary-constructors.md

kellyelton commented 1 year ago

Regardless of what they technically are, I think at the end of the day these parameters will end up being private readonly fields in most cases, and there shouldn't have to be any additional syntax or keyword to make that happen.

This contrived example is what I believe the default behavior should be.

public class Person(string firstName, string lastName) {
}
public class Person {
    private readonly string firstName;
    private readonly string lastName;
    public Person(string firstName, string lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

If you need more nuanced behavior, then write a class the normal way 🤷

In short, this feature doesn't have to cover a bunch of edge cases to be useful, and could always be built upon in future releases. Don't overengineer this.

KeithHenry commented 1 year ago

@HaloFour

This is pretty normal. In Scala the primary constructor arguments aren't promoted to public members without a case modifier on the type. The same with Kotlin but with a data modifier. Records already buy you a bunch of additional generated members, I don't think it's weird that it's true in this case as well.

I don't think that's a problem, C# will really benefit from having something like this.

My issue is that record already exists, and this syntax is extremely similar in some ways but different in others.


@Richiban

Once it's been out in the wild for a bit the confusion will have gone; I actually think that the chosen syntax here makes more sense given how records work.

I really like the record shorthand, but it does take some getting used to. Maybe it is something we'll get used to, but we have two near identical syntaxes that based on one keyword:

There will be some nasty bugs someone gets with serialisation or reflection or code generation that takes them ages to fix because nobody spots record that should be class or vice versa.


@kellyelton

Regardless of what they technically are, I think at the end of the day these parameters will end up being private readonly fields in most cases, and there shouldn't have to be any additional syntax or keyword to make that happen.

Are they readonly in the current spec? It's not obvious. I agree on the commonest use case if we could specify these modifiers would be a private readonly field, and in isolation that means those should be the default.

But record exists and already has defaults.

My concern would be that clash of defaults. When defaults are what you expect then they feel really useful, but when they aren't they're just confusing.

Kishoft commented 1 year ago

This will cause too many headcaches with "record" keyword. However the idea of creating private fields for each constructor parameter sounds like a good idea as long as there is nothing more than that. I understand that it is a high level language but if we have to spend a lot of time looking at documentation to remind ourselves of all the black magic it does, it is no longer time saving because we can generate "implicit" bugs.

HaloFour commented 1 year ago

@KeithHenry

My issue is that record already exists, and this syntax is extremely similar in some ways but different in others.

I think it's good that primary constructors are being extracted into a standalone feature that can be used with non-record classes. The intent is to retcon the design of records so that they build upon this primary constructor syntax.

I do agree that there could be confusion given the difference in mutability and perhaps that is something that should be discussed. In the retcon the immutability of positional records comes from the synthesis of the init-only property, but that seems like a very nuanced difference:

SharpLab

// class with primary constructor
public class C1(int x) {
    void M() { x++; } // legal
}

// record with primary constructor
public record R(int x) {
    void M() { x++; } // illegal
}

// class with primary constructor and manual declaration of init-only auto-property,
// as would be synthesized by a record
public class C2(int x) {
    public int x { get; init; } = x; // this member is synthesized by a positional record
    void M() { x++; } // illegal
}
muhamedkarajic commented 1 year ago

Hi, sorry for posting my oppinion which is negative but in general I don't like the feature. I feel like its far too different from records and its making it not so useful to work with - most of the things are just half better with this feature.

Hope someone from .NET / C# sees it this way cause the language definitly is getting more and more complex and complicated.

I'm not against complex things but I am against complicated things. Its hard to reson about the code, like what did I get with having

public class C2(int x) {
    public int x { get; init; } = x; 
}

Sorry but I don't see how thats so useful and I know if we add this we won't get rid of it. Solutions shouldn't be complicated they should make life easier - I don't want to have to look every time if the person defined a record or a class to understand the code.

In my oppinion a nice to have thing would be to have the same feature for classes as for records that I can simply write:

public class C2(int X)

In general I love to see some ideas but I just can't agree with the coding style it feels wrong for C# to do it this way.

Eirenarch commented 1 year ago

I too feel it is inconsistent with records. Maybe keep the behavior of records for the default syntax and add "field" keyword or some other way to create fields?

muhamedkarajic commented 1 year ago

I too feel it is inconsistent with records. Maybe keep the behavior of records for the default syntax and add "field" keyword or some other way to create fields?

Sounds really in the end just complicated all of those things for 2 lines less.

vgb1993 commented 1 year ago

I don’t like the fact that fields are used instead of properties. I would like to decide what it generates.

This feature has the potential to massively reduce the amount of code required to create classes.

Please don’t force us into having to manually declare properties!

HaloFour commented 1 year ago

@Eirenarch

I too feel it is inconsistent with records.

Records are really a combination of a bunch of different features. Most of those features can be used independently from records, such as init-only members, equality, and positional deconstruction. Primary constructors is one feature that was not initially designed to work independently of records, which meant that if you wanted to use the syntax you were required to also buy in to everything records bring to the table.

Records bring a lot of extra baggage. For example, it results in member synthesis which impacts the public surface of the type. This is not always desirable. Nor is the core underlying feature of records, which is value identity, remotely relevant to many types where primary constructors are also useful.

For example, if you are writing an ASP.NET service you are likely using dependency injection, which is currently quite verbose:

public class MyService {
    private OtherServiceA otherServiceA;
    private OtherServiceB otherServiceB;

    public MyService(OtherServiceA otherService, OtherServiceB otherServiceB) {
        this.otherServiceA = otherServiceA;
        this.otherServiceB = otherServiceB;
    }

    // other members omitted for brevity
}

Primary constructors immediately cuts half of the boilerplate and makes this a lot less prone to bugs:

public class MyService(OtherServiceA otherService, OtherServiceB otherServiceB) {
    // other members omitted for brevity
}

You wouldn't want to define such a type as a record as those members should never be exposed. Nor do any other features of positional records make sense here. You don't want identity and you never want to deconstruct this type.

@vgb1993

Maybe keep the behavior of records for the default syntax and add "field" keyword or some other way to create fields?

I don’t like the fact that fields are used instead of properties. I would like to decide what it generates.

Primary constructors as specified here don't synthesize any additional members. No fields are generated unless they have to be, and that is an implementation detail. That only happens if the value needs to live beyond the constructor:

// the following:
public class C(int x, int y, int z);

// is equivalent to:
public class C {
    public C(int x, int y, int z) { }
}

This feature has the potential to massively reduce the amount of code required to create classes.

Please don’t force us into having to manually declare properties!

And it still does. For example, it could be paired with a source generator that could emit the other members based on the primary constructor declaration. This has the added benefit of being able to allow developers to tune the policy of how the type is generated.

Eirenarch commented 1 year ago
public class MyService(OtherServiceA otherService, OtherServiceB otherServiceB) {
    // other members omitted for brevity
}

You wouldn't want to define such a type as a record as those members should never be exposed. Nor do any other features of positional records make sense here. You don't want identity and you never want to deconstruct this type.

That's true I wouldn't want these features but I still think the same syntax should do the same thing and if it does something else there should be difference in the syntax. For example

public class MyService(field OtherServiceA otherService, field OtherServiceB otherServiceB) {
    // other members omitted for brevity
}
Richiban commented 1 year ago

public class MyService(field OtherServiceA otherService, field OtherServiceB otherServiceB) {

I think you've missed the part of primary constructors where the "class parameters" are not necessarily fields. They will be automatically lifted into fields if needed, but otherwise they're just constructor parameters. For example, this code:

public class C(int x, int y)
{
    public int Z { get; } = x + y;
}

lowers to this:

public class C
{
    public C(int x, int y)
    {
         Z = x + y;
    }

    public int Z { get; }
}

So no field is generated, because it's not needed.

https://sharplab.io/#v2:EYLgZgpghgLgrgJwgZwLQAUEEsC2UECeAwgPYB2yMCcAxjCQsgD4Bu+ABDewLztkQB3dkQAUAFgA07AKwBKANwBYAFAqAAgEYAdJoCcImloBaCleoDM7NQCZhIrGRjsAHlIdOCslQG8V7f1aW7uxG7N7sAOYQMPLsAL48LuwA1OwESspxQA=

tachibanayui commented 1 year ago

IMO writing parameters after public class X is kind of verbose. My proposal is to write default constructor like a normal constructor but with a special modifier. This way we can eliminate the need for an initialization block, arguably reduce the confusion with records.

public class MyClass {
    public primary MyClass(int anInt, int anotherInt) {
        // Extra logic in the constructor. No need for an initialization block...

    }

    public MyClass() : this(15, 6) {}
}
HaloFour commented 1 year ago

@tachibanayui

IMO writing parameters after public class X is kind of verbose. My proposal is to write default constructor like a normal constructor but with a special modifier.

Writing it as a separate constructor with a new modifier is even more verbose.

This way we can eliminate the need for an initialization block, arguably reduce the confusion with records.

IMO it's much more confusing to have two completely different syntaxes for primary constructors that are unrelated to one another. With the current proposal there is one language feature, primary constructors, which can be applied the same way to both record and non-record types.

Washi1337 commented 1 year ago

While I understand where this feature idea is coming from, I think this is not a good idea in general as it doesn't really fit the general C# design. The way I see it, there are a couple of problems:

Bottom line is that I feel like this feature forces a little too much on how C# should be written according to the C# team. Furthermore, I feel my general opinion on this is that this is similar to the !! operator. Shorter isn't always better. Clarity is. If we want to reduce clutter in C#, I think other areas should get more focus than constructors (such as type inference to reduce generic parameters etc.).