Open YairHalberstadt opened 5 years ago
My general opinion: I don't think it offers any real power over current solutions, and only really benefits brevity, at the cost of significant new rules and unfamiliar syntax. C# is a complex language as it is, and we should reserve additional complexity for where it can make a real difference.
I feel like records however will significantly change the way we write code, by encouraging greater use of small, immutable, data only objects.
As such my vote would go to records over primary constructors.
EDIT:
I partially retracted after some experience with scala. See my comment below
Apologize for brevity, on phone.
I like this, Scala has it. The primary constructor itself is just shorthand for the constructor with properties. You get the rest of the "record" behavior by making it a case
class, which provides identity and deconstruction. Wrap that in an "enum" and you have DUs.
@HaloFour
I think if this could be neatly packaged with records in a way that they felt like one unified feature, I might agree with you. At the moment, I'm not seeing that.
@YairHalberstadt
I could see it being helpful for DI, don't need to declare your dependencies three times (fields, parameters and assignments), just once. But aside that I probably agree. I'm ok with primary ctors as being just unopinionated shorthand, as long as it doesn't go down the rabbit hole of feature parity with normal ctors.
I'm fine with PCs being a simple shorthand, with all their limitations, if we get a lightbulb operation that expands them into proper constructors.
@HaloFour the use of PCs for DI could be limited by their visibility. For example, your constructor takes three proper parameters and three injected dependencies. Your proper parameters must be validated and transformed by the constructor. You cannot extract your three dependencies into a primary constructor, because you would want it to be private.
@orthoxerox
You cannot extract your three dependencies into a primary constructor, because you would want it to be private.
I don't get why you'd need/want the PC to be private in that case, or why you'd need two constructors at all. I'd just have a single PC of the six parameters.
I'm also not saying that PCs would necessarily solve the issue for all DI-related construction, but they'd likely manage a good 90% or more.
@HaloFour
I think if this could be neatly packaged with records in a way that they felt like one unified feature, I might agree with you. At the moment, I'm not seeing that.
I've said it before in other issues, but I'll say it again here. What do primary constructors have to do with records?
@Richiban
Nothing intrinsically, but @Halofour was suggesting a link, Mads indicated the syntaxes may clash, and they aim to solve similar pain points.
@Richiban
I've said it before in other issues, but I'll say it again here. What do primary constructors have to do with records?
With the way records have been proposed for C# they include symmetric construction and deconstruction as well as identify based on a specific set of properties. Primary constructors get you all of that in one parameter list given that the parameters are also properties and that list gives you an order in which those properties can be deconstructed.
C# records, as they have been proposed, are more like Scala case classes or F#'s single case unions, and both languages define the construct by how they are constructed.
case class Point(x: Int, y: Int)
val p = Point(1, 2)
val (x, y) = p
type Point = Point of X : int * Y : int
let p = Point(1, 2)
let x y = p
class Point(int x, int x);
var p = new Point(1, 2);
var (x, y) = p;
I really don't see the benefit for this. It lowers readability, and adds complexity.
If a class constructor gets more than 3,4 parameters you will usually want to refactor it to a builder or a group up the parameters into a "configuration" object, so not a lot is saved there. As for simple data classes, you can just have visual studio generate the constructors for you.
@Richiban the syntaxes proposed for these two features are the same:
class Name(string value) {}
so if you use this syntax for primary constructors you need to think how records (who have value semantics) will look like.
I really don't see the benefit for this. It lowers readability, and adds complexity.
If a class constructor gets more than 3,4 parameters you will usually want to refactor it to a builder or a group up the parameters into a "configuration" object, so not a lot is saved there. As for simple data classes, you can just have visual studio generate the constructors for you.
You mean it improves readability, surely?
I mean, it may well depend on one's coding style, but our codebase contains literally thousands of classes that look something like this:
internal class CosmosCustomerRepository : ICustomerRepository
{
private readonly string _connectionString;
private readonly string _collectionName;
public CosmosCustomerRepository(string connectionString, string collectionName)
{
_connectionString = connectionString;
_collectionName = collectionName;
}
public Customer Retrieve(CustomerId id)
{
// Create connection, execute query, return result
}
}
Being able to reduce the above to:
internal class CosmosCustomerRepository(string connectionString, string collectionName)
: ICustomerRepository
{
public Customer Retrieve(CustomerId id)
{
// Create connection, execute query, return result
}
}
...is a big win for readability, if you ask me.
The constructors in the example above (of which there are many; a quick sample taken from our codebase of 30 classes shows that 22 of them (73%) had an explicit constructor defined and of those 21 (> 95%) did nothing other than set private readonly
fields) are dumb, tedious, can be auto generated, are rarely read--normally skipped over--by humans (because they're usually so dumb) and are therefore a surprisingly common source of bugs.
Have you ever had to track down a bug that looked like this?
internal class SomeClass
{
private readonly string _depA;
private readonly string _depB;
private readonly int _depC;
public SomeClass(string depA, string depB, string depC)
{
_depA = depA;
_depA = depA;
_depC = depC;
}
// Methods here...
}
Or this?
internal class SomeOtherClass
{
private readonly string _depA;
private readonly string _depB;
public SomeOtherClass(string depA, string depB)
{
_depA = depB;
_depB = depA;
}
// Methods here...
}
I really truly believe that the majority of constructors look like this and they should not be written by humans.
Leaning on the IDE to generate them for you isn't really a solution because:
a) It won't stay in sync automatically. If you add a field you have to remember to add a corresponding constructor argument and assign it properly. b) The developer still has to look at and read the auto-generated constructor. Sure, you could hide it in a region, but if the constructor is both auto-generated and then hidden then it really should be a language feature.
@Richiban the syntaxes proposed for these two features are the same:
class Name(string value) {}
so if you use this syntax for primary constructors you need to think how records (who have value semantics) will look like.
Since records are unlike other classes (in that they have value semantics) I kind of feel that they should be differentiated from other class definitions more anyway (probably with a keyword), never mind that it also frees up the syntax for primary constructors.
Records:
data class Point(int x, int y)
Primary constructor:
class Graph(Point[] points)
{
}
There's a really nice interplay between the two features this way: the constructor syntax makes it clear that the following declarations are parameters that become members of the class, and the data
keyword additionally says "they're public properties and participate in equality comparisons.".
Neat, I think
Simple examples are cool, but you will eventually run into a class where you cannot see the class definition and constructor definition in the same screen, so you will be left to wonder: is this a bug or a feature? And imagine code reviews with this. You see a constructor with no assignments and you nod in agreement and move on, only to see that several lines up somebody forgot to add something to class definition. So it is just as error prone as the usual assignment.
And in your second example, the class obviously lacks readability. It is basically defining members inline with class definition. Just imagine combining that with several interface implementation declarations, several attribute definitions and the usual abstract / sealed keywords.
This feature is just too prone to abuse. I agree writing boilerplate is annoying and ides are not ideal, but this is just a recipe for abuse and cryptic code.
I would like to have the captured values as readonly.
@mcosmin222
This feature is just too prone to abuse. I agree writing boilerplate is annoying and ides are not ideal, but this is just a recipe for abuse and cryptic code.
I'm sorry, but I simply do not understand your argument. I don't really see how this is open to abuse or cryptic code at all. Can you help me out with an example?
@Richiban
I kind of feel that they should be differentiated from other class definitions more anyway (probably with a keyword), never mind that it also frees up the syntax for primary constructors.
I agree, and this is exactly how Scala does it. In normal classes the primary constructor only buys you the constructor parameters being in scope for the entire class as fields. But add the case
keyword and you also get publicly exposed properties (by default), value semantics, positional deconstruction, string representation and a few other goodies for free:
class Foo(name: String) {
def greeting: String = s"Hello $name!"
}
val foo1 = new Foo("Richiban")
assert(foo1.greeting == "Hello Richiban!")
val name = f1.name // compiler error, name is not resolved as an accessible member
val foo2 = new Foo("Richiban")
assert(foo1 != foo2)
case class Bar(name: String) {
def greeting: String = s"Hello $name!"
}
val bar1 = Bar("Richiban")
assert(bar1.greeting == "Hello Richiban!")
assert(bar1.name == "Richiban") // name is an accessible member, by default
val name = bar1 match {
case Bar(name) => Some(name) // name can be deconstructed/extracted
case _ => None
}
assert(name.contains("Richiban"))
val bar2 = Bar("Richiban")
assert(bar1 == bar2) // compares equality based on name
@HaloFour
I agree, and this is exactly how Scala does it. In normal classes the primary constructor only buys you the constructor parameters being in scope for the entire class as fields. But add the case keyword and you also get publicly exposed properties (by default), value semantics, positional deconstruction, string representation and a few other goodies for free:
I never actually learned any Scala but, yeah, that's exactly how it should work. case
is an odd choice of keyword though.
@Richiban
case
is an odd choice of keyword though.
I believe Scala considers these types much more as ADTs or members of a DU than as just a "data class", which kind of makes sense as they are usually short and sweet, immutable and contain zero business logic. I kind of anticipate that C# records will have similar use cases as opposed to attempting to replace POCOs which are often much larger and mutable.
I've never understood why the C# design team has not considered the syntax TypeScript adopted, where the field is initialized and declared all in the parameter list.
class Foo
{
Foo(private string Bar) { }
}
The benefits of this are:
The presence of overloads in C# make this slightly more complex than in TypeScript, but I don't believe that's a blocker.
This accomplishes the same goals as primary constructors with minimal disadvantages.
I've never understood why the C# design team has not considered the syntax TypeScript adopted
It has been considered.
where the field is initialized and declared all in the parameter list.
This has the negative problem of the parameter and field having inconsistent naming with the naming of the .net ecosystem. The language has not wanted to wade into the space of "how would we name these?" and all the associated baggage (i.e. "how would the user override the naming?").
I've never understood why the C# design team has not considered the syntax TypeScript adopted
It has been considered.
where the field is initialized and declared all in the parameter list.
This has the negative problem of the parameter and field having inconsistent naming with the naming of the .net ecosystem. The language has not wanted to wade into the space of "how would we name these?" and all the associated baggage (i.e. "how would the user override the naming?").
private protected
cough. This has been a non-issue in TypeScript.
The TypeScript ecosystem is not the C# or .Net ecosytem. Patterns and practices are different htere.
The feature is widely used and I can't recall anyone complaining about it because of style reasons.
The style desires of the communities and ecosystems are different. This is greatly mitigated in TS because parameters and properties are normally cased the same for them, where they are not for .net.
This is already an issue with tuples and to a lesser degree records. (Do you capitalize the members or not? Should it depend on the usage of the tuple?)
This has already been a big issue. See the large debate in roslyn/corefx where an API that was tuple-returning was killed because this issue could not be resolved adequately.
The LDT really should have learned the lesson by now to stop discarding features
The feature was not discarded. Where did you get that idea? I responded to your question about why it was not considered by talking about how it has been considered. You jumped from that to it being discarded when that is not the case.
solely
Where did you get 'solely' from?
on the basis of community bike-shedding over minor style issues.
This was not community bike-shedding. The LDM members themselves (including myself) could not come up with an adequate proposal that didn't have significant issues (plural). As such, no forward movement has happened until someone can propose something that will be appropriately championed.
With much more important work getting attention, no one has had the time to invest here. Perhaps that will change in the future.
public class C(int i, string s) : B(s) { { if (i < 0) throw new ArgumentOutOfRangeException(nameof(i)); } int[] a = new int[i]; public int S => s; }
I think the example in the proposal is against tens of years of convention. We are not used to see scopes without any header in class. The parameter definitions are just moved from constructor definition to class definition without any benefit and mostly obscuring things.
Also the initialization of the field int[] a = new int[i];
is somewhat controversial too. Today we cannot initialize the fields using members (only statics are allowed). Can we use any field we want? No. So this new field must be a special thing to be able to used like this. This can introduce some confusion (especially to the newcomers).
I am in favor of automatic code generation (#107). It can cover all of the benefits of this proposal (and more) without a new syntax.
public class C
{
[PrimaryConstructor]
// You can specify the visibility of the constructor, as we can for some twenty years.
public C(string someString, int someInt)
{
}
// You can specify methods to run before or after member assignments:
[PrimaryConstructor(beforeAssignmentMethod: nameof(BeforeAssignment), afterAssignmentMethod: nameof(AfterAssignment))]
public C(string someString, int someInt)
{
}
private void BeforeAssignment(string someString, int someInt)
{
if (someInt < 0) throw new ArgumentOutOfRangeException(nameof(someInt));
}
public string SomeOtherString { get; private set; }
// Not necessary, but this method may be inlined in the primary constructor. If so, the private set part of the above property can be omitted.
private void AfterAssignment(string someString, int someInt) // May or may not have the parameters.
{
SomeOtherString = SomeString.ToLower();
}
// You can specify whether the parameters should be stored in members or in auto properties:
[PrimaryConstructor(createMembersAs: MemberCreation.AutoProperty)]
// These are created for you:
// public string SomeString { get; }
// public int SomeInt { get; }
// Or
[PrimaryConstructor(createMembersAs: MemberCreation.PrivateField or MemberCreation.PrivateReadonlyField)]
// private (readonly) string _someString;
// private (readonly) int _someInt;
public C(string someString, int someInt)
{
}
}
Many customizations are available in this way and they are not constrained by the syntax. Also some other attributes that inherit from PrimaryConstructor
can simplify things. E.g:
PrimaryConstructorWithAutoProperties
PrimaryConstructorWithReadonlyFields
I remember this was ever discussed in length long before, maybe in a design note post rather than its own post. One issue is the primary constructor body syntax, which is consistent with other languages like Python, F#, etc., but looks confusing enough in C#.
@yusuf-gunaydin Unfortunately I think the code generation idea is not really going anywhere. I also think that these situations are common enough to warrant their own language feature. As evidence, let's look at some other languages that offer exactly this functionality:
F#:
type Greeter(name : string) =
member this.SayHello () = printfn "Hello, %s" name
Scala:
class Greeter(name: String) {
def sayHi() = println("Hello, " + name)
}
Kotlin:
class Greeter(val name: String) {
fun greet() {
println("Hello, ${name}");
}
}
Typescript:
class Greeter {
constructor(private name: string) {}
greet() {
console.log(`Hello, ${this.name}!`)
}
}
So, you see, there's massive precedence for this feature.
I have read somewhere that the team will reconsider code generation (possibly reducing some of its scope) after releasing C# 8.0. So I am somewhat optimistic that a result will come out of this.
As I see it, primary constructors will only use code generation (no manipulation of existing code), so it can even be implemented today with existing Roslyn code generator libraries. (Although, it may be in a less consice way than full manipulation support since the constructor body has to be declared in the source code.)
By the way, I am not against the primary constructor concept itself. But, inventing a new, unnatural, and limited syntax should not be the way.
@yusuf-gunaydin
By the way, I am not against the primary constructor concept itself. But, inventing a new, unnatural, and limited syntax should not be the way.
It's not unnatural. See my examples of a handful of other very popular languages that use exactly this syntax. There's nothing wrong with it at all.
It could be a somewhat limiting syntax, depending on your viewpoint. For me it's not limiting at all since, as I said before, in our codebases the vast majority of constructors do nothing other than set fields. Even if what you want to do is null checks, there's a new syntax (probably) coming for that too:
class Greeter(string name!)
{
public string Greet() => $"Hello, {name}!";
}
Thanks @YairHalberstadt for opening the issue. I went ahead and championed it. There are good arguments for and against primary constructors, but the focus on records in C# 9.0 will force a discussion, so this issue can represent that.
It can be better if it is combined with my similar proposal dotnet/roslyn-analyzers#2768 :
Example:
class MyClass (private int X = 0, readonly int Y = 1, protected int Z = 2)
{
[NonConstructed] public MyClass(string Msg)
{
//do something
}
}
This will generate:
class MyClass
{
private int X {get; set;}
public int Y {get;}
protected int Z {get; set;}
public MyClass(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public MyClass(string Msg)
{
X = 0;
Y = 1;
Z = 2;
//do something
}
}
Primary constructor parameters are in scope throughout the class body. If they are captured by a function member or anonymous function, they become stored as private fields in the class. If they are only used during initialization they will not be stored in the object.
Can they be accessed as a member on an instance of class? Or more importantly, what about as a member on another instance of the class?
For example:
class C(int a)
{
void M(C other)
{
var x = this.a; // is this legal?
var y = other.a; // what about this?
}
}
I don't think it makes much sense to allow this, but on the other hand not allowing it is confusing - why can I access a private field from another instance of this class, but not a primary constructor parameter?
@YairHalberstadt
That's how it works in Scala:
class C(a: Int) {
def M(other: C): Boolean = other.a == this.a
}
But it doesn't work like that consistently across languages as far as I can tell. In F# it's only available for initialization. In Kotlin you have to explicitly declare the primary constructor parameter to also be a field:
class C(private val a: Int) {
fun M(other: C): Boolean = other.a == this.a
}
@HaloFour
That doesn't compile in scala :
@YairHalberstadt
That doesn't compile in scala :
D'oh, you're right. That's what I get for using IDE squigglies to inform me as to the legality of Scala code. You can still reference this.a
, just not other.a
.
@HaloFour
In F# it's only available for initialization.
Not true; this is fine:
type C(s : string) =
member this.M() = s
Specifically the scenario just mentioned doesn't work though:
type C(s : string) =
member this.F(other : C) = other.s // Error: The field, constructor or member 's' is not defined.
It seems that F# doesn't seem to allow access to private members on another instance?
@Richiban
Not true; this is fine:
Gotcha, I was trying this:
type C(s : string) =
member this.M() = this.s
So it looks like it's captured but can't be accessed as a member of the class.
It can be better if it is combined with my similar proposal dotnet/roslyn-analyzers#2768 :
- add the property modifier in the definition of the Primary Constructors, and if not, public is the default one.
- Use [NonConstructed] to exclude some constructors so they don't initialize the properties. In this case allow default values for the Primary Constructors.
Example:
class MyClass (private int X = 0, readonly int Y = 1, protected int Z = 2) { [NonConstructed] public MyClass(string Msg) { //do something } }
I don't see the use case for [NonConstructed] - if the primary constructor has default arguments, then the constructor taking a string already can work that way by adding : this()
, which could even be implied for secondary constructors the way : base()
is for constructors in regular classes.
Primary Constructor is the way to save 10 characters and make clumsy code across a whole class. Dangerous feature which must not be in professional language - leave it for JS coders!
Guys, are you really think, that primary constructors will be very useful feature of C#? I think, we try to remove some additional lines of code and kill readability of it. I prefer to define classes by the next way:
public class HelloWorld
{
public int Property1{get;set;}
public string Property2{get;set;}
public HelloWorld(int property1, string property2)
{
Property1 = property1;
Property2 = property2;
}
public HelloWorld(int property1)
: this(property1, string.Empty)
{
}
}
I think, it's readable and useful. Am I wrong? C# is not Scala, not all features, which are useful in Scala will be the same in C#! Don't kill our favorite language, please!
@Maximys
Style is always a question of personal preference. I think for a lot of components with dependencies having to write out separate fields and constructor assignments only adds pointless verbosity and multiple points of potential failure (missed assignments). For data classes or POCOs I don't think primary constructors are a good fit. But a language feature doesn't need to be useful across all use cases to be worthwhile.
I also think that primary constructors allow for easily building another feature borrowed from Scala and other functional languages: case classes. The primary constructor syntax brings an inherit positionality that normal classes don't expose so that the compiler can automatically emit a deconstructor for pattern matching. Discriminated unions could be further built on case classes allowing for a very simple syntax for declaring a closed set of data carriers. So yes, I think primary constructors would be a very useful addition to the language, if only they exist for the purposes of building additional language features.
First of all, primary constructors break readability. Nowhere in any book on OOP you'll find this clumsy syntax. It's really weird and it's not "question of taste" - it's a MESS inside elegant "C-like" syntax. I'm still against introducing to the language so questionable things. Narrow-mind solutions - sorry, keep it for your projects! In all my programs I never met necessity to have Pr.Constr., means somebody wanna solve his private problems by messing C# syntax. Why we should support it??
Note on score of the feature: +24 and... -24!!! Don't you thing something is wrong with feature, DECLINED BY HALF of our society?? I wouldn't even discuss such clumsy thing if it has so much opposite opinions.
@WrongBit
Nowhere in any book on OOP you'll find this clumsy syntax.
Or lambdas. Or async
. Or pattern matching. Or dozens of other features in C# or many other OOP-ish languages. C# isn't strictly an OOP language, nor does language evolution stop at the limit of the imagination of a particular book on a subject.
Even Java is getting primary constructors for the purposes of records. To quote one of their previous proposals, "this should no longer be a controversial feature".
In all my programs I never met necessity to have Pr.Constr.
Fine, then you don't have to use them. I use dependency injection a great deal and would find said feature immediately useful. But it's not surprising to me that you're finding it difficult to apply a language feature that doesn't yet exist to your programs. To me that just suggests that you're not familiar with enough other languages.
Note on score of the feature: +24 and... -24!!! Don't you thing something is wrong with feature, DECLINED BY HALF of our society?? I wouldn't even discuss such clumsy thing if it has so much opposite opinions.
C# is not a popularity contest. And last I checked, there were a few more than 48 C# developers. Quite literally every feature added to C# (and every language) has a segment of the community that doesn't approve of it.
Whenever people think they want primary constructors, most of the time, they really want records. I agree, primary constructors bring additional level of complexity.
@HaloFour Who speak about popularity?? Our votes is DECLINE of raw, absurd ideas. Nobody have to teach one more chapter "primary constructors" just because a few coders use DI. Especially when language already allows any of those "monkey patterns". IMHO "class" declaration already has enough complexity - look at maximum possible class declaration and ask yourself - hey, do we really need more here?? What global problem we solve, pushing "DI idea" everywhere? It's narrow mind solution. Too much mess in a syntax in a favor of too few developers to use their "lovely patterns". Language should be extended in favor of all people, bringing maximum profit.
@WrongBit
Who speak about popularity?? Our votes is DECLINE of raw, absurd ideas.
You did, by mentioning the votes.
IMHO "class" declaration already has enough complexity
You're welcome to your opinion. All new features introduce complexity. Are you arguing that C# should cease to add all new features?
What global problem we solve, pushing "DI idea" everywhere? It's narrow mind solution.
It would facilitate DI, yes, but that's not all. A lot of class definitions are comprised of a single constructor that does nothing beyond assign the parameters to readonly fields which are then used throughout the class. This feature facilitates all of those class definitions. And as I've already mentioned primary constructors also lend themselves to further language features already slated to be included in C#, namely records and discriminated unions. It's very likely that primary constructors will be added to the language as a part of the implementation of either of those features. The team has been actively discussing them since before Roslyn was moved to Github.
Lastly, your tone makes it incredibly difficult to engage in meaningful or constructive conversation with you. Not everything you disagree with is "absurd" or "dangerous" or "clumsy" or any other pointless subjective superlative.
Has anyone discussed the topic of ORMs yet?
Having a primary constructor would be a benefit to ORMs. Currently most ORMs don't either ignore non-default constructors or can only handle a single non-default constructor. Being able to say "this is the one you should use by default" would be beneficial.
I realize that is only a minor detail in regards to this feature, but if it is implemented we need a way to use reflection to determine which constructor is primary.
I think this feature is nearly useless without attribute-based parameter validation.
See "Primary constructor bodies" in the proposal for what I don't want to see in my code. But without that, we can't even use non-nullable reference types because there would be no way to enforce non-null checks.
Why does this have to be a "primary constructor"? Why can't I omit the body of any constructor and have it auto-magically do its thing?
Today we cannot initialize the fields using members (only statics are allowed).
Can we change that? Coming from VB, I always found this C# limitation to be needlessly annoying.
@gdar91
Whenever people think they want primary constructors, most of the time, they really want records.
You're certainly not the first person to say this, but I completely disagree. Surely anyone who writes constructors at all today would want this feature.
I agree, primary constructors bring additional level of complexity.
I would argue that it removes complexity. If, today, you want to write a class with dependencies you must write:
It's a lot of code to express a really simple concept, and that means that your resulting class definition is much more complex than it needs to be.
@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: