diff --git a/working/2364 - primary constructors/feature-specification.md b/working/2364 - primary constructors/feature-specification.md index 99c5ecb17..2faf3562d 100644 --- a/working/2364 - primary constructors/feature-specification.md +++ b/working/2364 - primary constructors/feature-specification.md @@ -4,14 +4,15 @@ Author: Erik Ernst Status: Draft -Version: 1.4 +Version: 1.5 Experiment flag: primary-constructors -This document specifies _primary constructors_. This is a feature that allows -one constructor and a set of instance variables to be specified in a concise -form in the header of the declaration. In order to use this feature, the given -constructor must satisfy certain constraints, e.g., it cannot have a body. +This document specifies _primary constructors_. This is a feature that +allows one constructor and a set of instance variables to be specified in a +concise form in the header of the declaration, or in the body. In order to +use this feature, the given constructor must satisfy certain constraints. +For example, a primary constructor in the header cannot have a body. One variant of this feature has been proposed in the [struct proposal][], several other proposals have appeared elsewhere, and prior art exists in @@ -21,11 +22,16 @@ specification [here][scala primary constructors] and some examples feature have taken place in github issues marked with the [primary-constructors label][]. +Recently, [Bob proposed][] that primary body constructors should use the syntax +`this.name(...)` rather than `primary C.name(...)`. This proposal includes +that choice. + [struct proposal]: https://github.com/dart-lang/language/blob/master/working/extension_structs/overview.md [kotlin primary constructors]: https://kotlinlang.org/docs/classes.html#constructors [scala primary constructors]: https://www.scala-lang.org/files/archive/spec/2.11/05-classes-and-objects.html#constructor-definitions [scala primary constructor examples]: https://www.geeksforgeeks.org/scala-primary-constructor/ [primary-constructors label]: https://github.com/dart-lang/language/issues?q=is%3Aissue+is%3Aopen+primary+constructor+label%3Aprimary-constructors +[Bob proposed]: https://github.com/dart-lang/language/blob/main/working/declaring-constructors/feature-specification.md ## Introduction @@ -43,14 +49,25 @@ class Point { } ``` -A primary constructor allows us to define the same class much more -concisely: +A primary constructor in the header allows us to define the same class much +more concisely: ```dart -// A declaration with the same meaning, using a primary constructor. +// A declaration with the same meaning, using a primary header constructor. class Point(int x, int y); ``` +A primary body constructor is slightly less concise, but it allows for +an initializer list and a superinitializer and a body. The previous +example would look as follows using a primary body constructor: + +```dart +// A declaration with the same meaning, using a primary body constructor. +class Point { + this(int x, int y); +} +``` + In the examples below we show the current syntax directly followed by a declaration using a primary constructor. The meaning of the two class declarations with the same name is always the same. Of course, we would @@ -66,8 +83,13 @@ class Point { Point(this.x, this.y); } -// Using a primary constructor. +// Using a primary header constructor. class Point(int x, int y); + +// Using a primary body constructor. +class Point { + this(int x, int y); +} ``` These examples will serve as an illustration of the proposed syntax, but @@ -77,30 +99,45 @@ declarations using the current syntax. Note that an empty class body, `{}`, can be replaced by `;`. -The basic idea is that a parameter list that occurs just after the class -name specifies both a constructor declaration and a declaration of one -instance variable for each formal parameter in said parameter list. - -A primary constructor cannot have a body. However, it can have assertions, -it can have initializing formals (`this.p`), it can have super parameters -(`super.p`), and it can have an initializer list. - -The motivation for the missing support for a body is that a primary -constructor is intended to be small and easy to read at a glance. If more -machinery is needed then it is always possible to express the same thing as -a body constructor (i.e., any constructor which isn't a primary -constructor). - -The parameter list uses the same syntax as constructors and other functions -(specified in the grammar by the non-terminal ``). - -This implies that there is no way to indicate that the instance variable -declarations should have the modifiers `late` or `external` (because formal -parameters cannot have those modifiers). This omission is not seen as a problem -in this proposal: It is always possible to use a normal constructor declaration -and normal instance variable declarations, and it is probably a useful property -that the primary constructor uses a formal parameter syntax which is completely -like that of any other formal parameter list. +The basic idea with the header form is that a parameter list that occurs +just after the class name specifies both a constructor declaration and a +declaration of one instance variable for each formal parameter in said +parameter list. + +A primary header constructor cannot have a body, and it cannot have an normal +initializer list (and hence, it cannot have a superinitializer, e.g., +`super.name(...)`). However, it can have assertions, it can have +initializing formals (`this.p`) and it can have super parameters +(`super.p`). + +The motivation for these restrictions is that a primary constructor is +intended to be small and easy to read at a glance. If more machinery is +needed then it is always possible to express the same thing as a body +constructor. + +The parameter list of a primary header constructor uses the same syntax as +constructors and other functions (specified in the grammar by the +non-terminal ``). + +A primary body constructor can have a body and an initializer list as well +as initializing formals and super parameters, just like other constructors +in the body. It only differs from those other constructors in that a +parameter which is not an initializing formal and not a super parameter +serves to declare the formal parameter and the corresponding instance +variable. + +The parameter list of a primary body constructor uses the same syntax as +constructors and other functions, except that it also supports a new +modifier, `novar`, indicating that the given parameter does not introduce +an instance variable (that is, this is just a regular parameter). + +There is no way to indicate that the instance variable declarations should +have the modifiers `late` or `external` (because formal parameters cannot +have those modifiers). This omission is not seen as a problem in this +proposal: It is always possible to use a normal constructor declaration and +normal instance variable declarations, and it is probably a useful property +that the primary constructor uses a formal parameter syntax which is +completely like that of any other formal parameter list. An `external` instance variable amounts to an `external` getter and an `external` setter. Such "variables" cannot be initialized by an @@ -115,15 +152,23 @@ class ModifierClass { ModifierClass(this.x); } -// Using a primary constructor. +// Using a primary header constructor. class ModifierClass(this.x) { late int x; external double d; } + +// Using a primary body constructor. +class ModifierClass { + late int x; + external double d; + this(this.x); +} ``` -`ModifierClass` as written does not make sense (`x` does not have to be -`late`), but there could be other constructors that do not initialize `x`. +`ModifierClass` as written does not really make sense (`x` does not have to +be `late`), but there could be other constructors that do not initialize +`x`. Super parameters can be declared in the same way as in a body constructor: @@ -138,9 +183,18 @@ class B extends A { B(super.a); } -// Using a primary constructor. +// Using a primary header constructor. class A(final int a); class B(super.a) extends A; + +// Using a primary body constructor. +class A { + this(final int a); +} + +class B extends A { + this(super.a); +} ``` Next, the constructor can be named, and it can be constant: @@ -155,29 +209,32 @@ class Point { // Using a primary constructor. class const Point._(final int x, final int y); + +// Using a primary constructor. +class Point { + const this._(final int x, final int y); +} ``` Note that the class header contains syntax that resembles the constructor declaration, which may be helpful when reading the code. -The modifier `const` could have been placed on the class (`const class`) -rather than on the class name. This proposal puts it on the class name -because the notion of a "constant class" conflicts with with actual -semantics: It is the constructor which is constant because it is able to be -invoked during constant expression evaluation; it can also be invoked at -run time, and there could be other (non-constant) constructors. This means -that it is at least potentially confusing to say that it is a "constant -class", but it is consistent with the rest of the language to say that this -particular primary constructor is a "constant constructor". Hence `class -const Name` rather than `const class Name`. +With the primray header constructor, the modifier `const` could have been +placed on the class (`const class`) rather than on the class name. This +proposal puts it on the class name because the notion of a "constant class" +conflicts with with actual semantics: It is the constructor which is +constant because it is able to be invoked during constant expression +evaluation; it can also be invoked at run time, and there could be other +(non-constant) constructors. This means that it is at least potentially +confusing to say that it is a "constant class", but it is consistent with +the rest of the language to say that this particular primary constructor is +a "constant constructor". Hence `class const Name` rather than `const class +Name`. The modifier `final` on a parameter in a primary constructor has the usual -effect that the parameter itself cannot be modified. The only location -where such modifications can occur is in an assertion, and they "should" -not have side effects, so we can basically ignore this. However, much more -importantly, this modifier is also used to specify that the instance -variable declaration which is induced by this primary constructor parameter -is `final`. +effect that the parameter itself cannot be modified. However, this modifier +is also used to specify that the instance variable declaration which is +induced by this primary constructor parameter is `final`. In the case where the constructor is constant, and in the case where the declaration is an `extension type` or an `enum` declaration, the modifier @@ -188,7 +245,6 @@ any case: ```dart // Current syntax. - class Point { final int x; final int y; @@ -203,21 +259,30 @@ enum E { const E(this.s); } -// Using a primary constructor. - +// Using a primary header constructor. class const Point(int x, int y); - enum E(String s) { one('a'), two('b') } + +// Using a primary body constructor. +class Point { + const this(int x, int y); +} + +enum E { + one('a'), + two('b'); + + const this(String s); +} ``` -Note that an extension type declaration is specified to use a -primary constructor (in that case there is no other choice, -it is in the grammar rules): +Note that an extension type declaration is specified to use a primary +header constructor (in that case there is no other choice, it is in the +grammar rules): ```dart -// Using a primary constructor. - -extension type I.name(int x); // Must use a primary constructor. +// Using a primary header constructor. +extension type I.name(int x); ``` Optional parameters can be declared as usual in a primary constructor, with @@ -231,16 +296,26 @@ class Point { Point(this.x, [this.y = 0]); } -// Using a primary constructor. +// Using a primary header constructor. class Point(int x, [int y = 0]); + +// Using a primary body constructor. +class Point { + this(int x, [int y = 0]); +} ``` We can omit the type of an optional parameter with a default value, in which case the type is inferred from the default value: ```dart -// Infer the type of `y` from the default value. +// Infer type from default value, in header. class Point(int x, [y = 0]); + +// Infer type from default value, in body. +class Point { + this(int x, [y = 0]); +} ``` Similarly for named parameters, required or not: @@ -253,8 +328,13 @@ class Point { Point(this.x, {required this.y}); } -// Using a primary constructor. +// Using a primary header constructor. class Point(int x, {required int y}); + +// Using a primary body constructor. +class Point + this(int x, {required int y}); +} ``` The class header can have additional elements, just like class headers @@ -268,16 +348,19 @@ class D extends A with M implements B, C { const D.named(this.x, [this.y = 0]); } -// Using a primary constructor. -class const D.named( - int x, [ - int y = 0 -]) extends A with M implements B, C; +// Using a primary header constructor. +class const D.named(int x, [int y = 0]) + extends A with M implements B, C; + +// Using a primary body constructor. +class D extends A with M implements B, C { + const this.named(int x, [int y = 0]); +} ``` -It is possible to specify assertions on a primary constructor, -just like the ones that we can specify in the initializer list of a -regular (not primary) constructor: +It is possible to specify assertions on a primary constructor, just like +the ones that we can specify in the initializer list of a regular +constructor: ```dart // Current syntax. @@ -287,17 +370,21 @@ class Point { Point(this.x, this.y): assert(0 <= x && x <= y * y); } -// Using a primary constructor. +// Using a primary header constructor. class Point(int x, int y): assert(0 <= x && x <= y * y); + +// Using a primary body constructor. +class Point { + this(int x, int y): assert(0 <= x && x <= y * y); +} ``` -Finally, it is possible to use an initializer list in order to -invoke a superconstructor and/or initialize some explicitly -declared instance variables with a computed value. +Finally, when using a primary body constructor it is possible to use an +initializer list in order to invoke a superconstructor and/or initialize +some explicitly declared instance variables with a computed value. ```dart // Current syntax. - class A { final int x; const A.someName(this.x); @@ -312,26 +399,92 @@ class B extends A { } // Using primary constructors. - class const A.someName(int x); -class const B(int x, int y, {required String s2}) - : s1 = y.toString(), assert(s2.isNotEmpty), super.someName(x + 1) - extends A { +class B extends A { final String s1; + const this(novar int x, novar int y, {required String s2}) + : s1 = y.toString(), assert(s2.isNotEmpty), super.someName(x + 1); } ``` -A formal parameter of a primary constructor which is used in a variable -initialization or in a superinitializer does not implicitly induce an -instance variable. +A formal parameter of a primary body constructor which has the modifier +`novar` does not implicitly induce an instance variable. This makes it +possible to use a primary constructor (thus avoiding the duplication of +instance variable names and types) even in the case where some parameters +should not introduce any instance variables (so they are just "normal" +parameters). + +Finally, here is an example that illustrates how much verbosity this +feature tends to eliminate: -In particular, `int x` does not give rise to an instance variable in the -class `B` because `x` is used in the superinitializer. Similarly, `int y` -does not give rise to an instance variable because it is used in the -initializer list element `s1 = y.toString()`. However, `s2` _does_ give -rise to an instance variable (it is used in the assertion, but that does -not prevent the instance variable from being induced). +```dart +// Current syntax. +class A { + A(String _); +} + +class E extends A { + LongTypeExpression x1; + LongTypeExpression x2; + LongTypeExpression x3; + LongTypeExpression x4; + LongTypeExpression x5; + LongTypeExpression x6; + LongTypeExpression x7; + LongTypeExpression x8; + external int y; + int z; + final List w; + + E({ + required this.x1, + required this.x2, + required this.x3, + required this.x4, + required this.x5, + required this.x6, + required this.x7, + required this.x8, + required this.y, + }) : z = 1, + w = const [], + super('Something') { + // A normal constructor body. + } +} + +// Using a primary body constructor. +class A(novar String _); + +class E extends A { + external int y; + int z; + final List w; + + primary E({ + required LongTypeExpression x1, + required LongTypeExpression x2, + required LongTypeExpression x3, + required LongTypeExpression x4, + required LongTypeExpression x5, + required LongTypeExpression x6, + required LongTypeExpression x7, + required LongTypeExpression x8, + required this.y, + }) : z = 1, + w = const [], + super('Something') { + // A normal constructor body. + } +} +``` + +Moreover, we may get rid of all those occurrences of `required` in the +situation where it is a compile-time error to not have them, but that is a +[separate proposal][inferred-required]. + +[inferred-required]: https://github.com/dart-lang/language/blob/main/working/0015-infer-required/feature-specification.md ## Specification @@ -347,17 +500,17 @@ constructors as well. 'class' ? ? | ...; - ::= // New rule. + ::= // New rule. ? ('.' )? ? ::= // New rule. - + | ; ::= // New rule. - 'const'? + 'const'? | ; ::= ? @@ -381,6 +534,19 @@ constructors as well. (',' )* (',')? (';' ( )*)? '}'; + + ::= // Modified rule. + ( | 'this') ('.' )? + + ::= + + | 'new' + + ::= // Modified + 'novar'? + | ``` A class declaration whose class body is `;` is treated as a class declaration @@ -397,12 +563,13 @@ Object {}`, and all three of them have `Object` as their direct superclass.* ### Static processing Consider a class declaration or an extension type declaration with a -primary constructor *(note that it cannot be a ``, -because that kind of declaration does not support primary constructors, -it's just a syntax error)*. This declaration is desugared to a class or -extension type declaration without a primary constructor. An enum -declaration with a primary constructor is desugared using the same -steps. This determines the dynamic semantics of a primary constructor. +primary header constructor *(note that it cannot be a +``, because that kind of declaration does not +support primary constructors, it's just a syntax error)*. This declaration +is desugared to a class or extension type declaration without a primary +constructor. An enum declaration with a primary header constructor is +desugared using the same steps. This determines the dynamic semantics of a +primary constructor. The following errors apply to formal parameters of a primary constructor. Let _p_ be a formal parameter of a primary constructor in a class `C`: @@ -415,27 +582,30 @@ A compile-time error occurs if _p_ has both of the modifiers `covariant` and `final`. *A final instance variable cannot be covariant, because being covariant is a property of the setter.* +A compile-time error occurs if _p_ has both of the modifiers `covariant` +and `novar`. *A parameter with the modifier `novar` does not induce an +instance variable, so there is no variable and hence no setter.* + Conversely, it is not an error for the modifier `covariant` to occur on another formal parameter _p_ of a primary constructor (this extends the -existing allowlist of places where `covariant` can occur), unless _p_ -occurs in an initializer element of the primary constructor which is not -an assertion. *For example, `class C(covariant int p): super(p + 1);` is an -error.* - -The desugaring consists of the following steps, where _D_ is the class, -extension type, or enum declaration in the program that includes a primary -constructor in the header, and _D2_ is the result of desugaring. The -desugaring step will delete elements that amount to the primary -constructor; it will add a new constructor _k_; it will add zero or more -instance variable declarations; and it will add zero or more top-level -constants *(holding parameter default values)*. +existing allowlist of places where `covariant` can occur). + +The semantics of the primary constructor is found in the following steps, +where _D_ is the class, extension type, or enum declaration in the program +that includes a primary constructor, and _D2_ is the result of the +derivation of the semantics of _D_. The derivation step will delete +elements that amount to the primary constructor; it will add a new +constructor _k_; it will add zero or more instance variable declarations; +and it will add zero or more top-level constants *(holding parameter +default values)*. Where no processing is mentioned below, _D2_ is identical to _D_. Changes occur as follows: Assume that `p` is an optional formal parameter in _D_ which is not an -initializing formal, and not a super parameter. Assume that `p` does not -occur in the initializer list of _D_, except possibly in some assertions. +initializing formal, and not a super parameter, and does not have the +modifier `novar`. + Assume that `p` does not have a declared type, but it does have a default value whose static type in the empty context is a type (not a type schema) `T` which is not `Null`. In that case `p` is considered to have the @@ -451,25 +621,28 @@ The current scope of the formal parameter list and initializer list (if any) of the primary constructor in _D_ is the body scope of the class. *We need to ensure that the meaning of default value expressions is -well-defined, taking into account that the primary constructor is -physically located in a different scope than normal non-primary -constructors. We do this by specifying the current scope explicitly as the -body scope, in spite of the fact that the primary constructor is actually -placed outside the braces that delimit the class body.* +well-defined, taking into account that a primary header constructor is +physically located in a different scope than in-body constructors. We do +this by specifying the current scope explicitly as the body scope, in spite +of the fact that the primary constructor is actually placed outside the +braces that delimit the class body.* Next, _k_ has the modifier `const` iff the keyword `const` occurs just -before the name of _D_, or _D_ is an `enum` declaration. - -If the name `C` in _D_ and the type parameter list, if any, is followed by -`.id` where `id` is an identifier then _k_ has the name `C.id`. If it is -followed by `.new` then _k_ has the name `C`. If it is not followed by `.` -then _k_ has the name `C`. - -If it exists, _D2_ omits the part derived from `'.' ` that -follows the name and type parameter list, if any, in _D_. - -Moreover, _D2_ omits the formal parameter list _L_ that follows the name, -type parameter list, if any, and `.id`, if any. +before the name of _D_ or before `this`, or _D_ is an `enum` declaration. + +Consider the case where _D_ is a primary header constructor. If the name +`C` in _D_ and the type parameter list, if any, is followed by `.id` where +`id` is an identifier then _k_ has the name `C.id`. If it is followed by +`.new` then _k_ has the name `C`. If it is not followed by `.` then _k_ +has the name `C`. If it exists, _D2_ omits the part derived from +`'.' ` that follows the name and type parameter list, if +any, in _D_. Moreover, _D2_ omits the formal parameter list _L_ that +follows the name, type parameter list, if any, and `.id`, if any. + +Otherwise, _D_ is a primary body constructor. If the reserved word `this` +is followed by `.id` where `id` is an identifier then _k_ has the name +`C.id`. If it is followed by `.new` then _k_ has the name `C`. If it is not +followed by `.` then _k_ has the name `C`. The formal parameter list _L2_ of _k_ is identical to _L_, except that each formal parameter is processed as follows. @@ -480,16 +653,12 @@ preserve the name and the modifier `required`, if any. An optional positional or named parameter remains optional; if it has a default value `d` in _L_ then it has the default value `d` in _L2_ as well. -- An initializing formal parameter *(e.g., `this.x`)* is copied from _L_ to - _L2_, along with the default value, if any, and is otherwise unchanged. +- An initializing formal parameter *(e.g., `T this.x`)* is copied from _L_ + to _L2_, along with the default value, if any, and is otherwise unchanged. - A super parameter is copied from _L_ to _L2_ along with the default value, if any, and is otherwise unchanged. -- Assume that _p_ is a formal parameter (named or positional) of the form - `T p` or `final T p` where `T` is a type and `p` is an identifier. - Assume that _p_ occurs in the initializer list of _D_, in an element - which is not an assertion. In this case, _p_ occurs without changes in - _L2_. *Note that the parameter cannot be covariant in this case, that is - an error.* +- A formal parameter with the modifier `novar` is copied unchanged from + _L_ to _L2_. - Otherwise, a formal parameter (named or positional) of the form `T p` or `final T p` where `T` is a type and `p` is an identifier is replaced in _L2_ by `this.p`, along with its default value, if any. @@ -502,217 +671,26 @@ positional or named parameter remains optional; if it has a default value removed from the parameter in _L2_, and it is added to the instance variable declaration named `p`. +In every case, any DartDoc comments are copied along with the formal +parameter, and in the case where an instance variable is implicitly induced +the DartDoc comment is also added to that instance variable. + If there is an initializer list following the formal parameter list _L_ then _k_ has an initializer list with the same elements in the same order. -*The current scope of the initializer list in _D_ is the body scope -of the enclosing declaration, which means that they preserve their -semantics when moved into the body.* +*The current scope of the initializer list in _D_ is the body scope of the +enclosing declaration even when _D_ is a primary header constructor, which +means that they preserve their semantics when moved into the body.* Finally, _k_ is added to _D2_, and _D_ is replaced by _D2_. ### Discussion -It could be argued that primary constructors should not support -superinitializers because the resulting declaration is too complex to be -conveniently readable, and developers could just write a regular primary -constructor instead. - -We expect that primary constructors will in practice be small and simple, -but they may use different subsets of the expressive power of the -mechanism. For example, - - -```dart -// Use super parameters. - -class const Point2D(int x, int y); - -class const Point3D(super.x, super.y, int z) extends Point2D; - -// Use a named constructor and a computed super argument. - -class A._(int x); - -class B(int y): assert(y > 2), super._(y - 1) - extends A with Mixin1, Mixin2; -``` - -Like many other language mechanisms, primary constructors need developers -to use their human judgment to create declarations that are both readable, -useful, and maintainable. - -There was a [proposal from Bob][] that the primary constructor should be -expressed at the end of the class header, in order to avoid readability -issues in the case where the superinterfaces contain a lot of text. It -would then use the keyword `new` or `const`, optionally followed by `'.' -`, just before the `(` of the primary constructor parameter -list: - -[proposal from Bob]: https://github.com/dart-lang/language/issues/2364#issuecomment-1203071697 - -```dart -class D extends A with M implements B, C - const.named( - LongTypeExpression x1, - LongTypeExpression x2, - LongTypeExpression x3, - LongTypeExpression x4, - LongTypeExpression x5, -) { - ... // Lots of stuff. -} -``` - -That proposal may certainly be helpful in the case where the primary -constructor receives a large number of arguments with long types, etc. -However, the proposal has not been included in this proposal. One reason is -that it could be better to use a body constructor whenever there is so much -text. Also, it could be helpful to be able to search for the named -constructor using `D.named`, and that would fail if we use the approach -where it occurs as `new.named` or `const.named` because that particular -constructor has been expressed as a primary constructor. - -It has been argued that a primary constructor parameter should be able to -introduce an instance variable (which is already true in this proposal), -and also to be a regular parameter (that doesn't introduce anything extra, -which is not supported by this proposal). The point would be that this -makes primary constructors more expressive. In particular, if we also -generalize the proposal to allow a full initializer list with a -superinitialization then we could allow the primary constructor to invoke -any superconstructor, with any actual argument list. - -This would provide a considerable enhancement of the expressive power of -primary constructors. At the same time, it would make primary constructors -considerably more verbose, which seems to lessen a crucial motivating -factor for primary constructors in the first place. - -A proposal which was mentioned during the discussions about primary -constructors was that the keyword `final` could be used in order to specify -that all instance variables introduced by the primary constructor are -`final` (but the constructor wouldn't be constant, and hence there's more -freedom in the declaration of the rest of the class). However, that -proposal is not included here, because it may be a source of confusion that -`final` may also occur as a modifier on the class itself, and also because -the resulting class header does not contain syntax which is already similar -to a body constructor declaration. - -For example, `class final Point(int x, int y);` cannot use the similarity -to a body constructor declaration to justify the keyword `final`. - -```dart -class Point { - final int x; - final int y; - Point(this.x, this.y); -} - -class final Point(int x, int y); // Not supported! -``` - -There is an easy partial workaround: Make the constructor `const`. It is -very often possible to make the constructor `const`, even in the case where -the class isn't necessarily intended to be used in constant expressions: -There is no body. The only ways it can be an error to use `const` on a -primary constructor is if the superclass doesn't have a constant -constructor, or if the class has a mutable or late instance variable, or it -has some non-constant expressions in instance variable declarations or in -the initializer list. Using `const` is not a complete solution, but -probably OK in practice. - -Finally, we could allow a primary constructor to be declared in the body of -a class or similar declaration, possibly using a modifier like `primary`, -in which case it could have an initializer list and a body, and it would -still have the ability to introduce instance variable declarations -implicitly: - -```dart -// Current syntax. -class D extends A with M implements B, C { - final int x; - final int y; - const D.named(this.x, [this.y = 0]); -} - -// Using a primary constructor in the class body. -class D extends A with M implements B, C { - primary const D.named(int x, [int y = 0]); -} -``` - -This approach offers more flexibility in that a primary constructor in the -body of the declaration can have a body, just like other constructors. In -other words, `primary` on a constructor has one effect only, which is to -introduce instance variables for formal parameters in the same way as a -primary constructor in the header of the declaration. For example: - -```dart -// Current syntax. -class A { - A(String _); -} - -class E extends A { - LongTypeExpression x1; - LongTypeExpression x2; - LongTypeExpression x3; - LongTypeExpression x4; - LongTypeExpression x5; - LongTypeExpression x6; - LongTypeExpression x7; - LongTypeExpression x8; - external int y; - int z; - final List w; - - E({ - required this.x1, - required this.x2, - required this.x3, - required this.x4, - required this.x5, - required this.x6, - required this.x7, - required this.x8, - required this.y, - }) : z = 1, - w = const [], - super('Something') { - // A normal constructor body. - } -} - -// Using a primary constructor in the class body. -class E extends A { - external int y; - int z; - final List w; - - primary E({ - required LongTypeExpression x1, - required LongTypeExpression x2, - required LongTypeExpression x3, - required LongTypeExpression x4, - required LongTypeExpression x5, - required LongTypeExpression x6, - required LongTypeExpression x7, - required LongTypeExpression x8, - required this.y, - }) : z = 1, - w = const [], - super('Something') { - // A normal constructor body. - } -} -``` - -We may get rid of all those occurrences of `required` in the situation -where it is a compile-time error to not have them, but that is a -[separate proposal][inferred-required]. +### Changelog -[inferred-required]: https://github.com/dart-lang/language/blob/main/working/0015-infer-required/feature-specification.md +1.5 - November 25, 2024 -### Changelog +* Reintroduce in-body primary constructors with syntax `this(...)`. 1.4 - November 12, 2024