Discussion: Constructor ergonomics #1815
Replies: 8 comments 7 replies
-
I absolutely love this proposal's idea to invoke the |
Beta Was this translation helpful? Give feedback.
-
C# doesn't allow it because it's not valid according to the CLR. It produces unverifiable assemblies. |
Beta Was this translation helpful? Give feedback.
-
I love your proposal of removing the need to use the type name as the constructor name. I just added an "issue" for discussion about this, before I read your comment. I also like a lot the possibility of calling the base constructor anywhere in the constructor. I think Java allows it does it? |
Beta Was this translation helpful? Give feedback.
-
Java uses a function-like syntax to invoke the base constructor, but it is still restricted to being the first statement in the constructor. Both the JRE and the CLR severely restrict what you can do in a constructor prior to calling the base constructor to ensure that the instance is properly initialized so it doesn't make much sense to remove the limitations on the language. |
Beta Was this translation helpful? Give feedback.
-
In the late 1990's, I used to do a lot of Delphi programming. The Object Pascal language required the implementer of any subclass to manually call the constructor of the base class, using the keyword inherited. It was common to place this as the first line of the constructor, but it could be placed anywhere. This was a fertile source of bugs. If memory serves, the usual case was a class that worked perfectly for the original scenario, but which failed spectacularly when reuse was attempted; because a different sequence of operations often meant that methods of the subclass ended up being invoked before initialization of the class was complete. |
Beta Was this translation helpful? Give feedback.
-
To be clear, I am absolutely not calling for any changes to be made to that restriction at all - I'm asking for the ability to have a block of multi-statement "static-like" code, including support for local variables, that is executed between a subclass constructor and a superclass constructor. As the Here's another example of why the current design is "broken" in my opinion. There exists an FxCop rule to require null-checking reference-type parameters of public methods and constructors, but if you require some kind of parameter argument marshalling between a subclass and a superclass then it becomes infeasible to comply with that FxCop rule.
If The only way to comply with the rule is to do this (in C# 6.0):
...or this:
...and I'm sure you'll agree both options are patently ugly. And if |
Beta Was this translation helpful? Give feedback.
-
Regarding computations and local variables before calling the base-class constructor, you can get a similar effect this way: partial class Bar : Foo
{
public Bar(String packed)
: this(DoTheComputations(packed))
{
}
private static (Int32 x, String y) DoTheComputations(string packed)
{
return (x: ExtractX(packed), y: ExtractY(packed));
}
private Bar((Int32 x, String y) arg)
: base(arg.x, arg.y)
{
this.OriginalX = arg.x;
}
} Regarding field access before calling the base-class constructor, I see related restrictions in two clauses of ECMA-335 6th Edition:
It seems the runtime would then allow a constructor to read and write fields (including backing fields of read-only auto-properties) of |
Beta Was this translation helpful? Give feedback.
-
Your code example seems more Java-like than C#-like and this may be the cause of your confusion around how to handle such a situation. To solve your problem here, static factories and complex code aren't required. You simply need a private constructor that takes the extracted x & y: public class Foo
{
public Foo(int x, string y) => (X, Y) = (new Random().Next(), y);
public int X { get; }
public string Y { get; }
}
public class Bar : Foo
{
public Bar(string packed) : this(ExtractX(packed), ExtractY(packed)) {}
private Bar(int x, string y) : base(x, y) => OriginalX = x;
public int OriginalX { get; }
private static int ExtractX(string s) { ... }
private static string ExtractY(string s) { ... }
}
A quote and a truism spring to mind when reading your code here: You have a person. You say you want a student. But what you really want is a student record associated with that person. Rather than disappear down a rabbit hole of trying to redefine how C#s inheritance model works, stop and question whether you took a wrong turn somewhere. Your solution effectively mangles composition in order to try and achieve inheritance. Therefore, use composition and all your complexity and need to change a fundamental feature of the language all fall away: record Person(string Name, DateTime DoB);
record StudentRecord(Person Person, DateTime EnrollmentDate); Job done: your data is modeled. Now you can focus on the business logic. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I want to start a discussion about certain ergonomic issues I've encountered with C#'s constructors and if we could crowd-source possible solutions - or if there exist any workarounds today.
I note that the problem of the lack of primary constructors is solved with the Record Type functionality that looks to be introduced in C# 8.0, so this discussion will presume that language-feature is already implemented.
Logic before and between superclass constructor and own constructor
My first grievance concerns the inability to use local variables when invoking a superclass' constructor, consider:
Note how the subclass
Bar
uses a static method to convert its singleString packed
constructor parameter argument into separate arguments for use byFoo
- which makes sense, except thatBar
would like to use the value of thex
parameter, but there is no way to pass intermediate values from the parent constructor call to the subclass, so theExtractX
method has to be invoked twice, which is inefficient (note thatFoo.X
does not store the originalx
value in this contrived example).Today the main workaround to this problem is a static factory method with a "pass-through" primary-constructor (usually private or protected) - unfortunately any further subclasses cannot re-use the same factory method themselves, so any helper or parameter marshalling methods must also be exposed. But this approach isn't always possible if you're consuming a not-well-thought-out external library. The code below demonstrates this static factory-method approach:
I don't want to make a formal proposal, but a possible general-solution to this problem would be to move the call to the
base(...)
constructor to inside the constructor body, and then allow code to run beforebase(...)
is invoked, with the restriction that the code cannot use thethis
reference, like so:Boilerplate required to instantiate subclass of a superclass given a superclass instance
Briefly disregarding Record types, there exists an ergonomic problem in C# with copying data from a POCO
class Foo
to a derived POCOclass Bar : Foo
- asBar
's constructor needs to be redefined, with new parameters for each copied member, then the arguments passed to the superclass's constructor:The ergonomic problem exists where you already have an existing
Person
instance with data but you want to promote it to aStudent
instance. Using normal primary-constructors you have to specify each property/argument which makes for harder maintenance as adding members toPerson
means updating code elsewhere in at least 3 different places.A workaround exists by manually defining a "copy-constructor" that passes the properties values from an instance of the same type to a primary-constructor (or even the same constructor):
So then
Student
can be defined as:...while this eliminates the maintenance requirement from the derived classes, the copy-constructor on
Person
must still be manually maintained. Looking over the Records proposal at https://github.com/dotnet/csharplang/blob/master/proposals/records.md I don't see any references to copy-constructors so I don't know if the work on Records types is considering this use case.Dependency Injection Parameter Spam
I refer you to this comment by @Richiban: #1087 (comment) which details the problem of repetition in dependency-injection scenarios. None of the proposed solutions in the thread particularly appeal to me as they move declarations away from conceptually being class-level fields to being "special" parameters imbued with field persistence - this may confuse beginner users of the language but it also means the code is just less immediately-comprehensible.
Having to duplicate the type-name in the constructor method
(I'm sure this has been raised before) using the type-name as the constructor method name seems a bit strange to me: it makes the code brittle (because renaming a type won't rename the constructor without an IDE with refactoring functionality) and it adds no value. I imagine it was inherited from Java or C++, I'm sure we'd all prefer a more succint syntax like
new(args) { }
instead, additionally asnew
is already a keyword it means the constructors would be highlighted by the IDE without additional logic.partial
constructors and assigning toreadonly
fields outside a constructorA constructor can call instance methods before the
this
instance is fully initialized - butreadonly
fields can only be set in the constructor. The C# compiler is not currently smart-enough to allowreadonly
fields to be set in a method invoked only from the constructor - while also not supporting the concept ofpartial
constructors. In practice this is a problem when a class' constructor is toolchain-generated, which means either the constructor is tool-generated and the user cannot assign their ownreadonly
fields, or the constructor is user-maintained and the tool-generated fields cannot bereadonly
because they're initialized in a separate method (such as theInitializeComponent
method we're all familiar with from WinForms and WebForms).Where to next?
All of these ergonomic issues makes me wonder if there's an alternative to constructors: what if, instead of a type declaring its own constructors (and that the type's consumers are able to invoke such constructors and that the type declares all the necessary constructors required (a primary constructor, a static factory method when parameter argument preprocessing logic is needed, convenience constructors or parameter-subset constructors, copy-constructors, etc) instead the responsibility is offloaded to the compiler (that's its job after-all: to handle the boring repetitive tasks of generating code from a more abstract, higher-level representation): the class's "constructor" (at least its signature/prototype) is always generated by the compiler while any explicit constructor code is only written if it's actually needed - effectively a kind of "inferred constructor" generation - if that makes sense.
Additionally the C# compiler is well within its means to also generate the copy-constructors mentioned previously.
Just an idea off the top of my head. (Note this is not a proposal! Just my musing about how the ergonomics of C#'s constructors could be improved):
What are your experiences?
In this discussion I'd like to see what ergonomic problems you've experienced with constructors - do you have any alternative solutions? Could we dispense with custom constructors entirely and rely on some variant of the Generic-factory-pattern with a generic constraint to allow subclasses to use a superclass's factory method? Could explicit dependency-injection be replaced with something more elegant (does anyone agree with merely injecting the service-locator instead of individual services?)
Or in short: "How can we make constructors better?"
Update
I also pitched-in some ideas for ctor "parameter-inheritance" in this thread: #4025 (comment)
Beta Was this translation helpful? Give feedback.
All reactions