Open YairHalberstadt opened 5 years ago
@idg10 Why wouldn't you use a record instead?
@mrpmorris records and classes have different use cases.
I sort of don't mind class parameters being mutable by default, because it'll put some more weight behind dotnet/roslyn-analyzers#188 (Readonly locals and parameters).
It is another difference between primary constructors on records and on classes though; in that on records the class capture isn't visible.
public class C(string x)
{
public void M() { x = ""; } // This is legal
}
public record C(string x)
{
public void M() { x = ""; } // This is not
}
I understand why this happens, but on the other hand I do agree it's potentially confusing. On the other-other hand though, I also don't see it as a particularly big deal.
@mrpmorris records and classes have different use cases.
@OskarKlintrot I know. I am asking @idg10 why he shouldn't use a record instead of a class for his requirements.
parameters being mutable for classes is understandable, but for readonly struct
shouldn't it be immutable? If you tried it now, you will find it has unexpected behavior:
public readonly struct S(string val)
{
public override string ToString() => val;
public void M()
{
val = "x"; // currently allowed, but doesn't take effect
}
}
even though the M
method change the value of val
, you will not see it taking effect:
var x = new S("w");
Console.WriteLine(x.ToString()); // "w"
x.M(); // change val to "x"
Console.WriteLine(x.ToString()); // still "w"
if you check the generated code you will find that the change of val
happened on a copy of the struct value that got discarded, I don't understand the reason for the compiler to do this!
@ViIvanov @Waleed-KH
Good catch.
I believe the branch is lagging behind the proposal there, which was changed to explicitly call out that captures in readonly structs should be readonly:
https://github.com/dotnet/csharplang/commit/c0f2ad70c99bd6a423c944afaee4a99ea6827390
In a readonly struct the capture fields will be readonly. Therefore, access to captured parameters of a readonly struct will have similar restrictions as access to readonly fields. Access to captured parameters within a readonly member will have similar restrictions as access to instance fields in the same context.
@mrpmorris
@idg10 Why wouldn't you use a record instead?
Because in the scenarios in which I would like to use this new primary constructor syntax, I want the behaviour this new primary constructor syntax provides, and not the behaviour that records provide.
The scenario in which I see using this the most is, as I said in the original comment, when my ctor is a big-list-o'-field-initializers. Records don't help in the slightest here because they add public properties, which isn't going to help me if what I actually wanted was fields. (And yes, there are backing fields for those properties, but I actively don't want the fields made public through properties.)
For example, in projects using DI, there's a lot of this:
public class HasDependencies
{
private readonly ILogger<HasDependencies> logger;
private readonly IWurzleMangler wurzleMangler;
private readonly IOptions<MySettings> settings;
public HasDependencies(
ILogger<HasDependencies> logger,
IWurzleMangler wurzleMangler,
IOptions<MySettings> settings)
{
this.logger = logger;
this.wurzleMangler = wurzleManger;
this.setting = settings;
}
... the rest of the class goes here
Primary constructors would let me write this:
public class HasDependencies(
ILogger<HasDependencies> logger,
IWurzleMangler wurzleMangler,
IOptions<MySettings> settings)
{
... the rest of the class goes here
I only have to list out all the dependencies once. In the first example, I named them all three times.
A record
would be the wrong thing here partly because this isn't really a record
-like sort of a type, but also because I just don't want all those fields to be public properties.
But I would prefer it if they could still be readonly
.
@idg10 Changing the subject slightly away from primary constructors and to DI specifically, I believe DI containers are starting to be made aware of required properties. The syntax within the class is a bit strange and unfamiliar, but it has the exact behaviour that you're after:
public class HasDependencies
{
public required ILogger<HasDependencies> Logger { private get; init; }
public required IWurzleMangler WurzleMangler { private get; init; }
public required IOptions<MySettings> Settings { private get; init; }
// ... the rest of the class goes here
}
@Richiban It might be a solution, but it still is much more verbose than primary ctor syntax though.
@idg10 thank you for your explanation. It was very good, and I think I would try the same pattern.
Is it not possible to use the this
keyword?
using System;
public class C(string x)
{
public void M()
{
Console.WriteLine(this.x); // Fails (SharpLab)
}
}
I prefer using this
qualification to emphasize that the variable is a field and not local.
@glen-84 x
is not a field. It's a contructor parameter. If you want a field, you'll need to declare it and assign the constructor parameter to it.
That makes sense, although it does still create a (hidden) field, and behave in a similar way (accessible to any method).
Declaring a field to assign this constructor argument seems to remove some of the terseness benefits, but I'm not sure how I feel about the variable just being available everywhere (without qualification). Please don't suggest prefixing it with an underscore. 😆
I may end up just continuing to use regular fields, for consistency.
That makes sense, although it does still create a (hidden) field,
As far as the language is concerned, that's not what's happening. The impls are free to implement that way, but they do not not have to :)
Declaring a field to assign this constructor argument seems to remove some of the terseness benefits,
That might be addressed if we expand on primary constructors in the future to allow easy declaration at the constructor site that you want a field directly assigned by the parameter. That's out of scope for the initial release currently, but it's something i would be in favor of.
@CyrusNajmabadi For fields, there is a possibility to refer to a field from a method even if a local name shadows the field (using this.
). Is there such a possibility for a constructor parameter? (Or is it planned?)
No. There is no plan for that. They work just like normal parameters here :-)
There was a remark in the recent C# Preview Features blog post that caught my attention:
Consider how you assign and use primary constructor parameters to avoid double storage. For example,
name
is used to initialize the auto-propertyName
, which has its own backing field. If another member referenced the parametername
directly, it would also be stored in its own backing field, leading to an unfortunate duplication.
Has the LDT considered making this a warning? This seems like a very easy mistake to make. If Name
is read-only, then this duplication is pure waste. If Name
is not read-only, I suppose there's an argument that it might be intentional, but I imagine most of the time it'd be a mistake where the code is now outright incorrect.
There seems to be an existing warning for field duplication in the case of:
class C(int x) : B(x)
{
// use x
}
so I'm curious if it'd be a good idea to also have one for:
class C(int x)
{
public int X { get; /* set; */ } = x;
public void Z()
{
// use x (not X)
}
}
FYI: I've created an analyzer rule(s) proposal here: https://github.com/dotnet/roslyn/issues/68743
This came from discussions in https://github.com/dotnet/csharplang/discussions/7109 around the fact that it's hard to spot issues where a primary constructor parameter is accidentally used. It turns out my worries are similar to the one @reflectronic mentions above.
Also, I thought I should also restate my main opinion from that feedback topic, as it's more likely to be long-term visible here:
I'm concerned that there are a couple of ways that it is an issue that there is no way to clarify in code when you intend to refer to a PC parameter. There's the "hard to spot bugs" issue mentioned above, but there's also an issue with fields/properties from a base class shadowing them, which AFAICT there is no workaround for (i.e. no way to fully qualify the PC parameter akin to this.
).
I appreciate that there's not going to be much that can be done in C#12, but in future developments I'd really like to see a way to (optionally) turn PC parameters into fields. IIUC then that would effectively take the PC parameter itself out of class scope. If you did get into a mess with a base class member shadowing a PC parameter, then using this mechanism to create a field would sort that out, too.
e.g.
class MyClass(private string foo)
becomes
class MyClass
{
private string foo;
public MyClass(string foo)
{
this.foo = foo;
}
}
Can the primary constructor call another constructor?
After typing the : character, am facing an issue with the message Type expected (obviously expects the base type).
@egvijayanand
I don't think so, as the :
denote the base class constructor
I don't think so, as the
:
denote the base class constructor
Since this is a reserved keyword in the lang., so using this be considered as a call to another constructor of the same type?
There's currently no way for the primary constructor to call another constructor (generally, you'd want to do the reverse). The primary constructor can pass its parameters to the base-class by doing class C(int x) : B(x)
How do you differentiate the other constructor and the base class ?
The primary constructor can pass its parameters to the base-class by doing
class C(int x) : B(x)
I've modified my code to do this for the time being. Passing the necessary parameter to the base class and handling over there.
How do you differentiate the other constructor and the base class ?
Call to another constructor of the same type has to be invoked via this(
As already mentioned, this is a reserved keyword in the lang. so using this can't refer to a type name (base type in this scenario). That way it can be easily differentiated.
Example:
public class Foo(int arg) : this() {}
public class Foo(int arg) : Bar() {}
in this example:
public class Foo(int arg) : this() {}
how do you tell the base class, if you can't, it's an extremely restrictive feature for a class to not have a base class.
The primary constructor cannot call another constructor aside from a base constructor. In fact, it's illegal to have another "secondary" constructor that doesn't call the primary constructor:
public class C(int x, int y) {
// error CS8862: A constructor declared in a record with parameter list must have 'this' constructor initializer.
public C(int x) { }
}
how do you tell the base class, if you can't, it's an extremely restrictive feature for a class to not have a base class.
Partial class
@egvijayanand can you explain what you mean by that?
can you explain what you mean by that?
I hope you refer to the Partial Class comment, the base type can be defined in another definition. Add the partial modifier and define the base type in another place.
@egvijayanand That seems a lot less pleasant than just making a normal constructor.
I would like to express a few personal thoughts that I hope will provide another source of inspiration for the controversial primary constructor My English is very poor and my expression is not clear. Please understand
`public class Employee(string firstName, string lastName, string email, decimal salary)
{
public string FirstName { get; set; } = firstName;
public string LastName { get; set; } = lastName;
public string Email { get; set; } = email;
public decimal Salary { get; set; } = salary;
/*
The syntax of the function body of the primary constructor is suggested to write in this way, inspired by the destructor, the destructor is turned over to write, which not only ensures the elegant syntax but also realizes the problem of the function body of the primary constructor
The "~" symbol represents: (string firstName, string lastName, string email, decimal salary)
Simply put, the "~" symbol represents the primary constructor
*/
public Employee~
{
/*
This area is the body area of the primary constructor
The arguments passed into the primary constructor are freely accessible here,
because the ~ symbol stands for: PrimaryConstructor ====>>> (string firstName, string lastName, string email, decimal salary)
*/
//"~" is equivalent to: (string firstName, string lastName, string email, decimal salary)
}
//Destructor, the destructor with "~" in prefix, and adjusted "~" to suffix as the primary constructor. Remove parentheses
public ~Employee()
{
}
/*
Multiple constructors with the same signature (same type 、 same number of arguments) are defined below for syntactic purposes. Do not misunderstand
*/
/*
When this constructor is called to create an object, the primary constructor must be included
"with" translates to take something with you, The "~" after "with" represents the primary constructor
*/
public Employee(HttpClient http_client, string first_name, string last_name) with ~
{
FirstName = firstName; //Here you can either use first_name directly or use the firstName passed in by the primary constructor
LastName = lastName; //Here you can either use last_name directly or use the lastName passed in by the primary constructor
Email = email; //This can only use the email passed in by the primary constructor
Salary = salary;//This can only use the salary passed in by the primary constructor
}
/*
When creating an Employee instance, this is called, with the "~" after the "with" represents the primary constructor, and the "=" represents the assignment
var emp_3 = new Employee(my_httpclient, "冯", "feng") with ~= ("冯", "先生", "56820168@qq.com", 15000);
*/
/*
When this constructor is called to create an object, it can be created with or without the primary constructor, because it has been assigned a default value
*/
public Employee(HttpClient http_client, string first_name, string last_name) with ~= ("冯", "先生", "56820168@qq.com", 9000)
{
FirstName = firstName; //first_name or firstName all right
/*
We can use either first_name directly or the firstName passed in by the main constructor. If the firstName of the main constructor is also named first_name, then we can override the first_name of the main constructor by applying the nearest principle. Or you can use the main constructor first according to the principle of the main constructor first. There are only two ways to choose, and the estimation is ultimately the principle of choosing the nearest application
*/
LastName = lastName; //last_name or lastName all right
/*
We can use either last_name directly or the lastName passed in by the main constructor. If the lastName of the main constructor is also named last_name, then we can override the last_name of the main constructor by applying the nearest principle. Or you can use the main constructor first according to the principle of the main constructor first. There are only two ways to choose, and the estimation is ultimately the principle of choosing the nearest application
*/
Email = email; //This can only use the email passed in by the primary constructor
Salary = salary;//This can only use the salary passed in by the primary constructor
Console.WriteLine($"姓: {FirstName}, 名: {LastName}");//output: 姓:冯, 名:先生
}
/*
When creating an Employee instance, the "~" after "with" represents the primary constructor and "=" represents the assignment
var emp_3 = new Employee(http_client, "冯", "feng");
var emp_3 = new Employee(my_httpclient, "冯", "feng") with ~= ("冯", "先生", "56820168@qq.com", 15000);
*/
/*
One of the cases in which the parameterless constructor is called to create an object must include the primary constructor
The "~" after "with" represents the primary constructor
*/
public Employee() with ~
{
FirstName = firstName; //The firstName passed in by the primary constructor
LastName = lastName; //The lastName passed in by the primary constructor
Email = email; //The email passed in by the primary constructor
Salary = salary;//The salary passed in by the primary constructor
/*
If the constructor of the parent class needs to be called, then the following "~" represents the primary constructor of the parent class and assigns the value to the primary constructor of the parent class using the same syntax
*/
base() with ~= (firstName, lastName, email, salary);
/*
The by clause directly tells the compiler that it is calling the primary constructor of the parent class, "~" represents the primary constructor, and "by ~" directly tells the compiler that this is the primary constructor
*/
base(firstName, lastName, email, salary) by ~;
}
/*
When the no-argument constructor is called to create an object, it can be created with or without the primary constructor, because the primary constructor is preassigned a default value
"~" represents the primary constructor and "=" represents the assignment, Simply put, with the primary constructor and assign a value to the primary constructor
*/
public Employee() with ~= ("冯", "小姐", "56820168@qq.com", 9000)
{
FirstName = firstName; //The firstName passed in by the primary constructor
LastName = lastName; //The lastName passed in by the primary constructor
Email = email; //The email passed in by the primary constructor
Salary = salary;//The salary passed in by the primary constructor
/*
If the constructor of the parent class needs to be called, then the following "~" represents the primary constructor of the parent class and assigns the value to the primary constructor of the parent class using the same syntax
*/
base() with ~= ("冯", "先生", "56820168@qq.com", 9500);
/*
The by clause directly tells the compiler that it is calling the primary constructor of the parent class, "~" represents the primary constructor, and "by ~" directly tells the compiler that this is the primary constructor
*/
base(firstName, lastName, email, salary) by ~;
}
/*
There can only be one parameterless constructor, The constructor of the same signature can only have one , Here is to demonstrate the syntax, so write multiple
*/
/*
mark:AAA //Make a mark here
Any of several constructors that take only one argument and must be called with the primary Constructor when creating an object
The "~" after "with" represents the primary constructor
*/
public Employee(decimal my_salary) with ~
{
FirstName = firstName; //The firstName passed in by the primary constructor
LastName = lastName; //The lastName passed in by the primary constructor
Email = email; //The email passed in by the primary constructor
Salary = my_salary; //my_salary or salary all right
/*
Here we can use either my_salary or the salary passed in by the primary constructor. If the salary of the primary constructor is also named my_salary, then we can override the salary of the Primary constructor by applying the nearest principle. Alternatively, it is possible to use the primary constructor first according to the primary constructor first principle, Only one of the two methods can be chosen, and estimation is ultimately the principle of choosing the nearest application
*/
Console.WriteLine($"主构造的Salary:{Salary},这个单参构造的Salary:{my_salary}");//output: 主构造的Salary:999999,这个单参构造的Salary:77777
}
/*
mark:BBB //Make a mark here
Another case of a constructor with only one argument, When this constructor is called to create an object, it can be created with or without the primary constructor, because the primary constructor is preassigned a default value
"~" represents the primary constructor and "=" represents the assignment, Simply put, with the primary constructor and assign a value to the primary constructor
*/
public Employee(decimal my_salary) with ~= ("冯", "先生", "56820168@qq.com", 10000)
{
FirstName = firstName; //The firstName passed in by the primary constructor
LastName = lastName; //The lastName passed in by the primary constructor
Email = email; //The email passed in by the primary constructor
Salary = salary; //my_salary or salary all right
/*
Here we can use either my_salary or the salary passed in by the primary constructor. If the salary of the primary constructor is also named my_salary, then we can override the salary of the Primary constructor by applying the nearest principle. Alternatively, it is possible to use the primary constructor first according to the primary constructor first principle, Only one of the two methods can be chosen, and estimation is ultimately the principle of choosing the nearest application
*/
Console.WriteLine($"主构造的Salary:{Salary},这个单参构造的Salary:{my_salary}");//output: 主构造的Salary:999999,这个单参构造的Salary:77777
}
}
/ Call a constructor with only one argument, corresponding to the constructor labeled mark:AAA or mark:BBB, and pass in an argument that overrides the default value given when defined, mark:AAA and mark:BBB are duplicative, and this is just for syntax purposes so I'm duplicative The "~" represents the PrimaryConstructor and the "=" represents the assignment, Simply put, with the PrimaryConstructor and assign a value to the PrimaryConstructor / var emp_1 = new Employee(77777) with ~= ("冯", "先生", "56820168@qq.com", 999999);
/ Call a constructor with only one argument, corresponding to the constructor marked mark:BBB. Since the constructor marked mark:BBB assigns default values to the PrimaryConstructor, it can be omitted when called / var emp_1 = new Employee(77777);
/ Call the parameterless constructor, and the arguments passed in the call override the default values given when defined "~" represents the primary constructor and "=" represents the assignment, Simply put, with the PrimaryConstructor and assign a value to the primary constructor / var emp_0 = new Employee() with ~= ("冯", "小姐", "56820168@qq.com", 888888);
/ Call a parameterless constructor, when give default value assigned to the main constructor when defined, it can be omitted when called You do not need to include the primary constructor, If the primary constructor is included, the default value at the time of definition is overridden / var emp_0 = new Employee();
var http_client = new HttpClient(){ BaseAddress = new Uri("xxxxxxx") });
/ Calls the three-argument constructor, passing in arguments that override the default values given when defined "~" represents the primary constructor and "=" represents the assignment, Simply put, with the PrimaryConstructor and assign a value to the PrimaryConstructor / var emp_3 = new Employee(http_client, "冯", "feng") with ~= ("冯", "先生", "56820168@qq.com", 15000);
/ Call a three-argument constructor. In the same way that a default value is assigned to the primary constructor when defined, it can be omitted when called, You do not need to include the primary constructor, which overrides the default value when defined / var emp_3 = new Employee(http_client, "冯", "feng");
/ The primary constructor is called here The primary constructor is called here. Note that just because there is no with clause does not mean it must be the primary constructor, because the secondary constructor can also be called without the with clause. When the secondary constructor is defined with a default value assigned to the primary constructor, it can also be called without the with clause, so it does not mean that the absence of a with is the primary constructor. / var emp_primary = new Employee("冯", "先生", "56820168@qq.com", 75000);
/ Proposed addition of "by" syntax, "~" symbol represents the primary constructor, The "by" clause tells the compiler directly: this is the primary constructor So it's like telling the compiler directly: this is the primary constructor / var emp_primary = new Employee("冯", "先生", "56820168@qq.com", 75000) by ~;
/ The proposed addition of the "default" syntax means that you artificially tell the compiler to use the default value directly. I gave the primary constructor a default value when defining it, so you can use the default value directly / var emp_0 = new Employee() with ~= default; //Be equivalent to ====>>> var emp_0 = new Employee(); //The advantage is that it is obvious at a glance that the main constructor is involved, since the new syntax is used
var emp_1 = new Employee(77777) with ~= default; //Be equivalent to ====>>> var emp_1 = new Employee(77777); //Same as above
var emp_3 = new Employee(http_client, "冯", "feng") with ~= default; //Be equivalent to ====>>> var emp_3 = new Employee(http_client, "冯", "feng");
/* We never underestimate the "default" syntax, with this "default" syntax, then you can make all the classes containing the primary constructor all unified use of "new .... with ...." or "new .... by ...." syntax, In this way, it is clear at a glance that any object that uses the new syntax "new" has a primary constructor, and any object that uses the old syntax "new" has no primary constructor
It is suggested that classes that have a primary constructor use the new syntax to create instances, and classes that do not have a primary constructor use the original syntax to create instances, so that the new and old are clear at a glance */ `
Well that's an interesting implementation of the feature to say the least.
Example:
using static System.Console;
public class Program {
public static void Main() {
var st = new Student("Alice");
WriteLine(st.Name); // "Alice"
st.Name = "Bob";
st.SayMyName(); // "Alice"
st.SayName("Conrad"); // "Conrad"
}
}
public class Student(string name)
{
public string Name {get; set;} = name;
public void SayMyName() => WriteLine(name); // ops, accidentally using a parameter that is actually a hidden field now
public void SayName(string name) => WriteLine(name); // looks like a constructor parameter again, but..
}
This is a simplified illustration of the bugs to come. I do realize that records allow this behavior as well, but at least there you can argue that they are supposed to be value objects, so when you pass something in the constructor it's can't be anything but an initial value for a property. Hence pascal case in constructor argument names.
In contrast, current implementation for classes promotes a false idea that you are only simplifying the way a constructor is written, but in reality it can produce hidden side effects if you are not careful enough. It'll be a nightmare for junior devs to understand and a nightmare for all to be careful in usage.
I believe current implementation will be very error-prone and should be reconsidered.
I believe current implementation will be very error-prone
LDM agreed that a warning shoudl be issued in the case above.
LDM agreed that a warning shoudl be issued in the case above.
I'd argue that if to introduce a feature you are shipping warnings to go along with it, the feature should be redesigned. I seriously doubt that we need features for features sake.
I'd argue that if to introduce a feature you are shipping warnings to go along with it, the feature should be redesigned.
Thanks for the feedback! :-)
LDM agreed that a warning shoudl be issued in the case above.
I'd argue that if to introduce a feature you are shipping warnings to go along with it, the feature should be redesigned. I seriously doubt that we need features for features sake.
I'd argue that there are very few language features that cannot be used incorrectly.
Rather than just not adding features unless they can only be used correctly, I prefer to have those scenarios be considered right off the bat, with warnings in the official tooling.
LDM agreed that a warning shoudl be issued in the case above.
I'd argue that if to introduce a feature you are shipping warnings to go along with it, the feature should be redesigned. I seriously doubt that we need features for features sake.
I'd argue that there are very few language features that cannot be used incorrectly.
Rather than just not adding features unless they can only be used correctly, I prefer to have those scenarios be considered right off the bat, with warnings in the official tooling.
The principle of least astonishment (POLA) comes into play here. If you design a language and then have to add a warning that it doesn't behave the way the majority of users will expect it to work, then you very likely have a flawed design. Further, that warning is hypothetical/WIP at the moment.
Thankfully, there is a simple hack that users can use to work around this flaw: hide the parameter from the rest of the class by declaring a field or property with the same name:
public class Student(string Name)
{
public string Name {get; set;} = Name; // Property is initialised from the parameter but then
// hides it from the rest of the class
public void SayMyName() => WriteLine(Name); // Uses the property, not the parameter
public void SayName(string name) => WriteLine(name); // Clearly not the constructor parameter now
}
The same hack can also be used to work around the lack of ability to mark the parameter's hidden backing field as read-only too:
public class Student(string name)
{
private readonly string name = name;
public void SayMyName() => WriteLine(name); // using the field, not the parameter
public void SetMyName(string newName) => name = newName; // error CS0191: A readonly field cannot
// be assigned to...
}
The principle of least astonishment (POLA) comes into play here. If you design a language and then have to add a warning that it doesn't behave the way the majority of users will expect it to work, then you very likely have a flawed design. Further, that warning is hypothetical/WIP at the moment.
Thankfully, there is a simple hack that users can use to work around this flaw: hide the parameter from the rest of the class by declaring a field or property with the same name:
Everything has a name these days, doesn't it? :)
Issue with a hack is that it breaks common naming convention. I doubt you have ever named an argument as _httpClientFactory
or would expect the absence of a prefix for a private field in a class (assuming you are using them).
So, you'll either have to adapt how you reference fields inside a class, your _arGuMeNt names or risk accidentally creating an unintentional reference and pray that you'll notice a warning (if they aren't promoted to errors).
Personally, if this feature will end up in .NET as it is, I will purposefully avoid using it. It's just not worth it.
PS: excuse me for negativity. I generally like the direction the language is heading and the features being delivered, but no matter how I try to look at this one, I just don't see a way to justify the current implementation.
and pray that you'll notice a warning (if they aren't promoted to errors).
You can set warnings to be an error for your build. No concern about missing anything then.
Thankfully, there is a simple hack
This is not a hack. What you are describing is literally the intentional design of the feature (one discussed several times so far).
hide the parameter from the rest of the class
Note: as from prior discussions, the parameter is not hidden. It is simply higher in scope. We may (or may not) introduce a scoping operator that would allow you to still reference them in the future. (similar to how we have a scoping operator that allows referencing fields when there is a local/parameter in a closer scope).
the lack of ability to mark the parameter's hidden backing field
The language is not defined as the parameters having hidden backing fields. Indeed, we've rejected several things that would end up requiring a hidden backing field as the impl strategy as we do not think it is a good thing to require that they be implemented that way.
This is not a hack. What you are describing is literally the intentional design of the feature (one discussed several times so far).
If it walks lack a hack and quacks like a hack...
But if what is clearly a hack in my view was genuinely intended, I refer you back to my comment about POLA above.
Absolutely no part of that is a hack. Any more than being able to define locals/parameters with her same name as fields as being a hack (or using the same name for any sorts of disparate symbols). Similarly, the scoping rules you're discussing are literally the same bog-standard scoping rules that follow what c# has had since the beginning. It would literally be weird for things not to work this way, since this is just a continuation of the same concepts the language has always had, just to a new, outer, scoping level.
refer you back to my comment about POLA above.
I think you're misunderstanding the principal. It would be more astonishing to not behave this way. Parameters would not behave like parameters normally have (especially since c#2 when we added capture semantics). Scoping would not behave like scoping normally does (esp with parameters vs fields). Records, would not be a simple case of just generating members, but leaving everything else the same, Etc. Etc.
The behaviors you've been asking for would be far more surprising. Esp when you consider the rules on the language that would have to be written for them. You'd see aspects of PCs leak into all sorts of unexpected places. And you'd have very unpleasant venn diagrams with strange partial overlaps.
POLA her means we have nice, composable, features, which build on top of each other.
--
A 'hack' happens when it turns out you can use a part of the language (or a combination of parts) in an unexpected fashion to accomplish something surprising and out of the norm of the indeed areas the feature was designed for.
What you are describing here is quite literally exactly a mechanism that was designed from the beginning for exactly the use case you are using it for.
It's like saying "you can hack things by capturing a local into a local function, then pass that local function to something which then executes the local function, which then mutates the local!". Yes, you can exactly do that. It's not a hack. It's quite literally exactly what this feature was intended to make possible :-)
It would be more astonishing to not behave this way. Parameters would not behave like parameters normally have (especially since c#2 when we added capture semantics)
I'm confident that a significant number of people will disagree when they first come across these quirks. The idea that the primary constructor parameters are in scope way outside the execution of the constructor is surprising to many.
It feels weird that the above mechanism for hiding the PC parameter by assigning it to a private field doesn't work unless the field has the same name, for example. I'm glad that the new warnings added in https://github.com/dotnet/roslyn/pull/68662 help with aspects of this.
You've explained the reasoning behind the design, and I now understand why it works that way and have some sympathy for that reasoning. Once you have the right mental model then the behaviour is logical. I still think a lot of people will find some of the side effects of the design quite surprising (or even astonishing 😉), though.
I guess the main thing now is that the introductory docs for class primary constructors should explain the "parameter capture" design and its quirks clearly and up front, to help people get in the right headspace and be less surprised later. Perhaps in some ways it's similar to the surprise some people feel when they first discover some of the interactions between parameter capture and LINQ deferred execution.
I'm not sure what quirks you're referring to :-)
it's similar to the surprise some people feel when they first discover some of the interactions between parameter capture and LINQ deferred execution.
Outside of the time when we had the scoping wrong for things like 'foreach' and lambdas (which we fixed soon after), I'm not sure what you're referring to here. The scoping and capture actually seem to match what people think should happen. We even do some very advanced analysis when local functions are involved so that people can write things in an order, and with scoping, that makes sense to them and which operates in the manner they expect :-)
Absolutely no part of that is a hack...
Obviously you are entitled to hold that view. However, literally the only way to achieve a readonly field from a PC parameter is to manually create a field with that name to hide the parameter as - by default - the parameter simply leaks out of the constructor into the rest of the class and it cannot be marked as readonly. That fits the definition of a hack to me. So whilst you are entitled to your opinion, you are completely wrong in my view.
What you are describing here is quite literally exactly a mechanism that was designed from the beginning for exactly the use case you are using it for.
So not only is the hack I describe the only way to achieve a readonly field from a PC parameter, the team deliberately designed it that way?!?? 🤯 Seriously, you know that phrase, "when in a hole, stop digging"...
Issue with a hack is that it breaks common naming convention. I doubt you have ever named an argument as
_httpClientFactory
or would expect the absence of a prefix for a private field in a class (assuming you are using them).
This is a genuine problem many will face. If they name the field xXXX
, then an analyzer rule will flag they aren't following their company naming rules that fields must be of the form _xXXX
. But if they name the parameter _xXXX
to get around that, another rule for parameter names will be triggered. I can see a lot of code with #pragma warning ignore
appearing here, or companies just declaring the feature as not fit for purpose and discouraging its use until v2 appears with these warts addressed.
I'm not sure what quirks you're referring to :-)
I really hope that the smiley indicates this is an actual joke, as we've been discussing this for 11 weeks.
it's similar to the surprise some people feel when they first discover some of the interactions between parameter capture and LINQ deferred execution.
Outside of the time when we had the scoping wrong for things like 'foreach' and lambdas (which we fixed soon after), I'm not sure what you're referring to here. The scoping and capture actually seem to match what people think should happen. We even do some very advanced analysis when local functions are involved so that people can write things in an order, and with scoping, that makes sense to them and which operates in the manner they expect :-)
This is the third result for "linq lambda capture" on Google: https://unicorn-dev.medium.com/how-to-capture-a-variable-in-c-and-not-to-shoot-yourself-in-the-foot-d169aa161aa6
Their example (which prints "10" ten times):
void Foo()
{
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach(var a in actions)
{
a();
}
}
I've got no problem with what this code does and (overall) it is desirable that capture works this way, but it is still surprising to many people when they first see it. Some explanation of how things are working under the hood (and why) is often required to overcome that surprise. I think the PC parameter design will probably have the same effect (even for those used to lambda capture) as I don't think it's the first design most people would think of.
@DavidArno
This is a genuine problem many will face. If they name the field
xXXX
, then an analyzer rule will flag they aren't following their company naming rules that fields must be of the form_xXXX
.
I agree, but https://github.com/dotnet/roslyn/pull/68662 has at least added a warning that could help us here.
I think that means this code will trigger a warning (at the initialization of _dependency
):
class C1(IMyDependency dependency)
{
IMyDependency _dependency = dependency;
void M()
{
dependency.CallService(); // Oops, I forgot the _
}
}
warning CS9124: Parameter 'IMyDependency _dependency' is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
I've made some further suggestions here: https://github.com/dotnet/roslyn/issues/68743#issuecomment-1603881369
However, literally the only way to achieve a readonly field from a PC parameter is to manually create a field
Yes... That's literally the design. It's odd to describe the literal exact, single, mechanism here (which was a core part of the design success day one, and which works in line with how these features have worked since the beginning) as a hack :-)
A hack would be using a different mechanism, designed for a different purpose, to accomplish this task.
But you are trying to make a field, and initialize it from parameter, and place the field in a closer scope. This is literally the way to do that, and is 100% how we wanted it to work and how we show how to use this feature. It's like saying "hey, look at this hack where 'new' on this reference type allocates an object in the heap!". Your literally describing the feature as designed and as intended to be used.
the team deliberately designed it that way?!??
Yes. Of course. It works as the language has worked since the beginning with scoping. And it works with declaration and assignment since we did PCs back with records.
It's literally the feature. You're describing the exact feature design working exactly as we intended for it to be used, and which had been pointed out to you numerous times in all these discussions. You're acting as if it's done strange "hack". I.e. something accomplishing something unexpected or unintended, as of it's a way to sidestep things and get what you want. When, in reality, it's literally exactly what was designed in from the beginning, and stated clearly, as an intended way to use this feature.
There's no hole being dug here. You seem shocked that we both considered this case, and designed this to work (from day 1) when we did PCs. Frankly, I find it surprising:-)
@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: