-
Notifications
You must be signed in to change notification settings - Fork 211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Primary constructor on classes #2364
Comments
I'd say "yes". If it works, it feels odd to not allow it. I can't see a reason it shouldn't work. But, that depends a lot on what model/capabilities we end up with for the fields. The current model uses the primary constructor to declare fields, and only secondarily as a template for a default constructor. That is, it's not necessarily a "constructor". We could consider a different model where the "primary constructor" directly defines the unnamed constructor. With that, I'd also say that any other generative constructor must be redirecting (eventually) to the primary constructor. If we do allow the syntax for classes too, I'd also have those classes get the default A |
Yes, please! ;-) We could use the following rule: The primary constructor of a struct declares non-late instance variables that are final by default. The primary constructor of a class declares non-late instance variables that are non-final by default, but may have the keyword Classes can still declare additional instance variables, of any kind, as before. Structs might be able to do this as well—what's the harm in allowing a struct to have The primary constructor syntax gives rise to a constructor declaration and a set of instance variable declarations. Every constructor is checked statically relative to the result of this desugaring step. In other words, there is no reason to require that all constructors are redirected to the generated primary constructor, they just need to satisfy the normal constraints that we have today (e.g., that a non-late final instance variable must be initialized before any code with access to About this:
I think that's a very interesting idea to explore. |
Not sure I follow this. I was proposing to allow other generative constructors, they just must also initialize all of the fields as usual.
On reflection, I was thinking of modifying the proposal to say that if there is a "primary constructor", then there is always a
This was the model I had in mind. |
I totally agree that this syntax should be applicable to classes as well. You can make an argument that it is good enough if it only supports simple cases, e.g. class X (var x, int y, final String z, {super.key}) extends Y {
final int w = x + y;
}
// equivalent to
class X extends Y {
var x;
int y;
final String z;
final int w;
X(this.x, this.y, this.z, {super.key}) : w = x + y;
} and for anything else people can resort to traditional constructor syntax. You can probably make We can maybe even make something like: class X (var x, int y, final String z, {key}) extends Y(x, key: key) {
final int w = x + y;
} work. The weakest point of this is going from shorthand syntax to long syntax once you realise that you need constructor body, but maybe this rarely happens. |
I 1000% want any primary constructor syntax to be generalized to classes. In fact, I personally care more about that than I care about the entire views proposal. :) Most user-types do not have value semantics ( To validate that, I scraped a big corpus of code from itsallwidgets.com, open source Flutter apps, and pub packages (18+MLOC) to determine which classes with at least one generative constructor could not use a proposed primary constructor syntax. The reasons a class might not be able to use a primary constructor sugar that I considered are:
The results are:
So a little more than 3/4 of all existing class declarations could use something close to the proposed primary constructor syntax. Note that I'm assuming here that a primary constructor syntax would support users controlling which parameters are positional, named, and/or optional and would allow private names. (In other words, I did not treat those as failures.) I think the biggest design challenges are:
A while back, I worked on a primary constructor strawman syntax that looked like: class Rect new (
final int x,
final int y,
final int width,
final int height,
); So instead of a parameter list right after the class name, there is a You can put it after the other header clauses. Since the field list is likely longer than the class ArgumentSublist extends Rule<Expression> implements FormatSpan new (
/// The full argument list from the AST.
final List<Expression> _allArguments,
/// The positional arguments, in order.
final List<Expression> _positional,
/// The named arguments, in order.
final List<Expression> _named,
) {
/// The number of leading block arguments, excluding functions.
///
/// If all arguments are blocks, this counts them.
final int _leadingBlocks;
/// The number of trailing blocks arguments.
///
/// If all arguments are blocks, this is zero.
final int _trailingBlocks;
void visit(SourceVisitor visitor) { ... }
} Compare that to what you'd get using the current proposal: class ArgumentSublist(
/// The full argument list from the AST.
final List<Expression> _allArguments,
/// The positional arguments, in order.
final List<Expression> _positional,
/// The named arguments, in order.
final List<Expression> _named,
) extends Rule<Expression> implements FormatSpan {
/// The number of leading block arguments, excluding functions.
///
/// If all arguments are blocks, this counts them.
final int _leadingBlocks;
/// The number of trailing blocks arguments.
///
/// If all arguments are blocks, this is zero.
final int _trailingBlocks;
void visit(SourceVisitor visitor) { ... }
} Note how the You can use different keywords. In my strawman, you could use class Rect final (
int x,
int y,
int width,
int height,
); We could then do the same thing for In other words, this means the only thing writing You can provide a constructor name. Having a keyword before the parameter list instead of the class name also provides a natural place to insert a constructor name if you want the primary constructor to be named: class NestingLevel extends FastHash new.empty(
/// The nesting level surrounding this one, or `null` if this is represents
/// top level code in a block.
final NestingLevel? parent,
/// The number of characters that this nesting level is indented relative to
/// the containing level.
///
/// Normally, this is [Indent.expression], but cascades use [Indent.cascade].
final int indent,
) {
/// The total number of characters of indentation from this level and all of
/// its parents, after determining which nesting levels are actually used.
///
/// This is only valid during line splitting.
int get totalUsedIndent => _totalUsedIndent!;
int? _totalUsedIndent;
} The downside, of course, is that this is a bit more verbose and a little different coming from other languages whose primary constructor is right after the class name. In cases where there isn't much else in the type header, there are few fields, and they aren't documented, I think the classic primary constructor syntax looks better. But once the type scales up (and in particular, once you document your fields, which I think is generally a good idea), it gets kind of hard to read. |
This is assuming no changes to documentation conventions, which I think is not realistic. From a brief look at some kotlin code, the equivalent might look more like: /// @param _allArguments The full argument list from the AST.
/// @param _positiional The positional arguments, in order.
/// @param _named The named arguments, in order.
class ArgumentSublist(
final List<Expression> _allArguments,
final List<Expression> _positional,
final List<Expression> _named,
) extends Rule<Expression> implements FormatSpan {
/// The number of leading block arguments, excluding functions.
///
/// If all arguments are blocks, this counts them.
final int _leadingBlocks;
/// The number of trailing blocks arguments.
///
/// If all arguments are blocks, this is zero.
final int _trailingBlocks;
void visit(SourceVisitor visitor) { ... }
} Which looks fine to me (nit, I don't understand how the extra fields work in this class, since they're not initialized in the constructor?) One way of looking at this is that intuitively, we write
Is there any reason not say that every primary constructor is a const constructor (at least if the superclass has a const constructor)?
We could. It looks pretty weird to me though.
This really feels a bit over-generalized to me. If you want a named constructor, just write the constructor.
This is really the rub. My sense is that the more we generalize this, the more we lose the actual benefits. Your data scraping suggests that a huge majority of classes don't need the generality. So every bit of generality that we add that makes that majority more verbose has a massive incremental cost in aggregate, and only benefits a few niche cases. |
We'd need to give you a way to opt out of being a
That's a very good point. The only counter-point is that ever feature we make default and automatic causes an extra step if you ever need to migrate away from the shorthand. If we make a primary constructor implicitly |
FWIW I think that majority of Dart developers don't concern themselves with such matters because they are not writing reusable code. So I think we should not optimise defaults towards the minority that does. |
@ lrhn
To be slightly provocative, maybe the answer is to say "if you don't want it to be const, don't use a primary constructor". As @mraleph says, I think there is a lot of value for a feature like this that you don't have to use in optimizing strongly for the common case. To be slightly less provocative, we could at least say that getting an implicit const constructor is part of the deal with
I hear this, but I also think that there is an inherent cliff here. If you want a constructor body, you have to migrate away. If you want to delegate, you have to migrate away. If you want to initialize some fields in the initializer list, you have to migrate away. So saying that if you want non-const you have to migrate away doesn't feel that bad to me. |
That's a good point. Hoisting all the field docs alleviates much of my readability concerns.
Yeah, I agree it is 100% intuitive to have the parameters right there. I just think it looks funny when you end up having the extends/implements/with clauses jammed between the primary constructor and the class body. But... I'm convinced that it's the least bad approach.
No, good point.
Yes, I think I'm sold. I've poked around a bunch of Kotlin code and it does look weird to me to have the superclasses and superinterfaces wedged between the primary constructor and class body. But in practice, it seems like most classes with complex inheritance hierarchies don't use primary constructors. For those that do... it looks a little weird (and people seem to format them in a variety of creative ways), but not intolerable. OK, so what I'd suggest then is:
There's the weird wrinkle around private named fields as named parameters in the primary constructor. I think I'd be OK with saying that you just can't do that. |
What @munificent says. Parameter list occurs after class name — and after type parameters if any. I'm actually, uncharacteristically, fine with allowing the field names in the parameter list to be private, and automatically make them public in the implicitly added constructor. It's reasonable to want private fields, and unreasonable to have private parameter names. Something needs to be tweaked. (I'd even be willing to contemplate making the name of the parameter of the common I'm now OK with making the constructor Biggest issue: Do we need a way to specify a super-constructor other than the unnamed one? If we allow
Most people will just use the unnamed primary constructor for everything, and that'll just work. On second thought, there is one problem with implicitly inferring
then whether the constructor actually is const will depend on very fragile and accidental choices. Adding a field like I think that's generally going to be too fragile. I'd recommend you having to write const class Foo(int x, int y); That's an explicit opt-in to the primary constructor being constant. It makes it easy to give errors if some other part of the class doesn't support being Yes, it's one more word, and it'll likely be used a lot, but as long as we don't have |
Yeah, I think I agree that it's too fragile, and I'd be fine with this choice (to put const before the class). For structs/data classes (if we do them) perhaps it would be reasonable to say that |
Data classes/ The current proposal allows non-potentially-constant initializer expressions. struct Foo({List<int> indices = [0]}); cannot have a constant constructor. Again it becomes fragile to infer If initializer expressions have to be constant for structs, then we make all structs I mentioned earlier that we could have separate syntaxes for constant default values and non-constant initializers, say: struct Foo({int x = 0, List<int> l ??= <int>[0]}); That would allow a struct with only I'd still prefer to go with |
Cf. #3023, a concrete proposal about this feature. |
It is not according to my theory, it is by definition. Rereading your previous comment, It is indeed annoying to rewrite the parameters for different constructors (not 100% sure that's where you are getting at), but I'd prefer:
where
This makes a lot more sense to me because what's in the primary constructor is the full thing and not potentially partial. What do you think ? |
I still think the declarations should be kept separate. Maybe the explicit "passthrough" keyword is unnecessary: the compiler can automatically add all passthrough parameters to every generative constructor (normally, there's only one), or some shorter syntax like |
Consider an example: class A {
final int a;
A(int a): a = a==0? 42: a;
} If we rewrite it using class A {
this(final int a): a==0? 42: a;
}
(Asking for a friend 😄) |
(Friends are great! 😄) Here's the example again: class A {
this(final int a): a == 0 ? 42 : a;
} Wouldn't that be something like Anyway, the cool thing in this example would be that we avoid the duplication of the variable name, and yet we compute the value rather than just using the actual argument directly. I think this can be modeled rather naturally as follows, assuming the proposal here: class A {
this._(final int a);
A(final int a) : this._(a == 0 ? 42 : a);
} However, if we don't have specific plans to use this 2-step separation of the object initialization (e.g., by having several different redirecting constructors) then we might just as well use a completely normal constructor: class A {
final int a;
A(int a) : a = a == 0 ? 42 : a;
} We could try to come up with a generalization of the initializing formal mechanism by introducing a computation into the parameter declaration. For example, we could associate a function with the initializing formal: class A {
final int a;
A((this.a: (it) => it == 0 ? 42 : it);
} This would specify that int foo(it) => it == 0 ? 42 : it;
class A {
final int a;
A((this.a: foo);
} This could be carried over to primary constructors: // Using a function literal.
class A(final int a: (it) => it == 0 ? 42 : it);
// Using an existing function.
int foo(it) => it == 0 ? 42 : it;
class A(final int a: foo); The basic idea is simply that we're considering the meaning of initializing formals (basically, it's an implicitly induced It's a crowded syntactic space. For example, an optional parameter could become something like |
You see what happens when you start from a Schrödinger definition that superimposes field declarations over parameter declarations? Everything becomes a problem requiring more and more of exotic syntax and conventions, but WHY? class A(final int a, final int b) { // declaring class name AND passthrough fields
//,,,
} or (interchangeably - like in your proposal) class A {
// declaration of passthrough fields
this(
final int a,
final int b
); // NO CODE! This is just a declaration
} BUT, if you are not happy with the automatically generated passthrough constructor, you can write it explicitly: class A(final int a, final int b) {
A(a, ...): a = a==0?42:a; /// ... is a placeholder for other passthrough parameters
} Note that in the above notation, there's no way to use the parameter Some examples of what is possible to do with "passthrough": class A extends B {
this( // passthrough fields -NAMED ONLY
final int x;
super.a,
super.b,
// to include *all* named parameters of super constructor nnn, we can write
... super.nnn // include B.nnn constructor parameters
);
// autogenerated if there's no explicitly defined generative constructor
A(...): super(...);
// what if our constructor has a positional parameter? Then we have to write
final int something;
A(this.something, ...): super(...);
// what if super has a positional parameter? Then we pass it by hand
A(int z, ...): super(z, ...);
} Is there anything missing? |
I was trying to respond to your idea about having an initializing formal (or a primary constructor parameter whose meaning in most proposals is an initializing formal plus an instance variable declaration), and then inject some sort of computation that may use the actual argument and yields the initial value of the instance variable. At least, that's how I understood It's a simple idea: // Initializing formal.
class C {
final int i;
C(int i): this.i = i; // Can be written as `C(this.i);`
}
// Generalized initializing formal.
class D {
final int i;
D(int i): this.i = expressionUsing(i); // Maybe `C(this.i: expressionUsing)`.
} However, this is just a brief thought experiment, I don't think this idea is likely to become a language mechanism. The syntax is too esoteric, as you say, and it is not obvious to me how it could be made much more readable without destroying the brevity which is the main purpose. (It's still a fun idea, though ;-). For the passthrough mechanisms, I think they are more related to enhanced default constructors because the starting point is the variable declaration rather than the parameter declaration, and also because it includes steps whereby the parameter list of a constructor is modified (here: by implicitly inducing a named formal parameter declaration for each passthrough variable). The enhanced default constructors were criticized for their readability properties: There is (arguably) too much complexity in the process whereby a reader of a constructor declaration can determine how to call it (if the declaration exists at all in the source code). This is also the main reason why I'm focusing on mechanisms where variable declarations are derived from parameter declarations and not the other way around. |
The difference is that "enhanced default constructors" proposal (EDC for brevity) does NOT introduce the notion of "passthrough fields", which is a cornerstone of the feature. With the passthrough (PT) fields declared in a special section, the format of the invocation exactly matches the structure of the field declarations. In EDC, every field is included in the generated constructor, but this is not the case in PT. I'm not sure how to name this declaration section: I don't like "this", others don't like "passthrough", but I found another word in EDC proposal: "forward" - maybe not an ideal word, but arguably less controversial: class A extends B {
int foo; // not included in a constructor
int bar;
forward(
final int a = 0,
final int b = 1,
String super.x, // the type, if specified, must match the type of x in super
double super.y = _, // _ says that there's a default value, but we don't care what it is
//etc.
);
} It's clear from the definitions which parameter is optional and which is required. The only problem is positional parameters. A(int z, ...): super(z, ...); but I've not given it much thought yet - maybe there's a better option like forward(
positional final int foo,
positional super.z,
int super.bar
) With the introduction of a special declaration block like Do you still see any "readability issues" with the proposed format? EDIT: among other things, the proposed "forward" feature (quite expectedly) solves the (highly in-demand) problem of forwarding: class A extends B {
forward (
String? super.xxx
)
} If the parameter is omitted while calling the constructor A(), and not observed (through interception) it won't be passed into B at all (as opposed to passing null). BONUS: You don't have to write "required" while declaring the IMO, dart's insistence on having two colors of parameters is a major design mistake. There's no logical reason for this. I can't find a single instance of dart's API where positional parameters have meaningless names to the extent they cannot be used publicly. At least, this never happens in constructors. It's OK if a tear-off loses the names, but here. we are are not talking about tear-offs, or scratch functions, or closures - it's all about constructors. |
I had a fruitful brainstorming session with ChatGPT (after seeing a youtube video from a physics professor debating an unresolved problem with it and being impressed). class A {
final int a;
final String b;
A({this.*});
} See the entire response inside. ChatGPT responseTo address the problem of redundant field declarations and constructor parameter definitions in Dart, a more concise syntax could be designed that allows for a simpler way to declare fields and initialize them in the constructor without repeating the names.Here’s an idea for the new syntax: Suggested Syntax:class A {
final int a;
final String b;
A({this.a, this.b});
} This is essentially the same as your original code, but it's still verbose since the field names and the constructor parameters are repeated. We can simplify this. Proposal: Implicit Initialization from ConstructorWe could introduce a feature where the fields are automatically initialized from the constructor parameters without needing to repeat their names. The idea would be something like this: class A {
final int a;
final String b;
A(this.a, this.b);
} This syntax avoids repeating the field names, as the constructor automatically initializes fields with the same name as the parameters. Further Simplification: Using
|
@tatumizer wrote:
Aside: I'd prefer to say 'passthrough instance variables' in order to avoid introducing a special word like 'fields' just for this kind of variable—if anything, the non-standard variables are either all non-local variables (because they have a getter and possibly a setter), or all local variables (because they don't). The fact that the getter/setter for the instance variable has late binding is not significant for the variable, that's just a property that all instance getters and setters have. I think we could say 'passthrough variable' (omitting 'instance') because all of them are instance variables. Anyway, a passthrough variable would be an instance variable at the semantic level, and it's introduced syntactically in your proposal, IIUC, as a formal parameter-ish declaration in a specialized constructor declaration (it occurs in the place where we'd expect a formal parameter, but the declaration syntax itself is the same as for a variable declaration). The special power which is bestowed upon a passthrough variable is that every constructor (IIUC) will have an implicitly induced initializing formal parameter for each of those parameters (I'm not quite sure how the "unless they are overwritten" mechanism would work). All in all, this is a mechanism that uses a variable declaration (e.g., in a It is arguably more powerful than primary/declaring constructors because it can introduce an unlimited number of parameter declarations based on a single variable declaration. However, this also means that it has the same readability issues as EDCs: You need to perform a non-trivial amount of computation on the actual constructor declarations (taking other declarations into account as well) in order to understand what it does and how to call it.
This would be one step more explicit because the variables will still give rise to a number of constructor parameter declarations (I don't know if they are still called 'passthrough'), and you can't see the actual names. |
@eernstg: I don't understand where the "readability" issue comes from. const Vec3 = struct { x: f32, y: f32, z: f32 };
const my_vector = Vec3{
.x = 0,
.y = 100,
.z = 50,
}; Here, the field declarations are "primary", and the "constructor" syntax is ... I woudn't even say "derived" - it's trivial. Maybe it makes sense to go back to the basics then? The original proposal by @leafpetersen is rather simple. Here the quote from the earlier comment:
Then the question is: what extra features can be added without inadvertently adding too much complexity? |
After staring into the expression class A extends B {
final int a = 0;
final String? b;
final Foo? _foo; // NOT included into this.* - has to be handled separately
A({Foo? foo, super.*, this.*}): _foo = foo {
// constructor body
}
} The idea is that we can define all normal (passthrough) fields as public - and then they will be included into A(Foo foo, {super.*, this.*}): _foo = foo {
// constructor body
} Too fancy? |
I had exactly the same language improvement in mind when I found your comment. Just a reminder, this is a common way to code things in flutter: class Text extends StatelessWidget {
const Text(
String this.data, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
// ... many more parameters
});
const Text.rich(
InlineSpan this.textSpan, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
// ... many more parameters
});
final String? data;
final InlineSpan? textSpan;
final TextStyle? style;
final StrutStyle? strutStyle;
final TextAlign? textAlign;
final TextDirection? textDirection;
final Locale? locale;
final bool? softWrap;
final TextOverflow? overflow;
final double? textScaleFactor;
final TextScaler? textScaler;
final int? maxLines;
final String? semanticsLabel;
final TextWidthBasis? textWidthBasis;
final ui.TextHeightBehavior? textHeightBehavior;
final Color? selectionColor;
} If i could write this instead: class Text extends StatelessWidget {
const Text(
String this.data, {
super.key,
this.*, // Include all public class fields automatically
});
const Text.rich(
InlineSpan this.textSpan, {
super.key,
this.*, // Include all public class fields automatically
});
final String? data;
final InlineSpan? textSpan;
final TextStyle? style;
final StrutStyle? strutStyle;
final TextAlign? textAlign;
final TextDirection? textDirection;
final Locale? locale;
final bool? softWrap;
final TextOverflow? overflow;
final double? textScaleFactor;
final TextScaler? textScaler;
final int? maxLines;
final String? semanticsLabel;
final TextWidthBasis? textWidthBasis;
final ui.TextHeightBehavior? textHeightBehavior;
final Color? selectionColor;
} This would be a HUGE reduction in boilerplate code! By the way, removing all the fields from the parameter list has actually |
I personally don't like
Maybe that could change. An issue was created recently for this: #4251 So I'd prefer the older proposal about: class Foo {
this(final int a, {final String? b});
} And add support for private named parameters alongside it. |
Agreed. This would not be an issue if it was the call site that decided whether a parameter is named or not, that is you could use both at all times instead: class Foo {
this(final int a, final String? b);
Foo.other(this.*);
}
void main() {
final foo1 = Foo(3, '3');
final foo2 = Foo(a: 3, b: '3');
final foo3 = Foo.other(a: 3, b: '3');
final foo4 = Foo.other(b: '3', a: 3);
} Just a thought. |
@rrousselGit That's true. |
I just don't see any form of And we already have a constructor-first approach in the language: Extension types extension type Foo(int myField) {
} This is a primary constructor, just not for classes. So I'd go for: class Foo(final int value);
class const Bar(final int value);
class Baz({int value = 0}) extends Base
: assert(value > 0),
super(...);
class Quaz(this.value) { // Using this. and super. don't define a field, but refer to an existing one
final num value;
}
class Qux(keyword int x): _value = x { // Add a keyword for parameters that don't have a matching field
final Object? _value;
} It might also be worth considering making fields |
I'm having difficulty seeing how this feature addresses the common use case I described earlier.
I really have no idea how to rewrite it with the syntax above. VS going to a codebase and make
We really need some examples beyond foo/bar. |
Here's your class Home({
super.key,
required final bool useLightMode;
required final bool useMaterial3;
required final ColorSeed colorSelected;
required final ColorImageProvider imageSelected;
required final ColorSelectionMethod colorSelectionMethod;
required final void Function(bool useLightMode) handleBrightnessChange;
required final void Function() handleMaterialVersionChange;
required final void Function(int value) handleColorSelect;
required final void Function(int value) handleImageSelect;
}) extends StatefulWidget {
@override
State<Home> createState() => _HomeState();
} And here's Flutter's class Container({
super.key,
/// Align the [child] within the container.
/// [...]
final AlignmentGeometry? alignment;
final EdgeInsetsGeometry? padding;
final Color? color;
final Decoration? decoration;
final Decoration? foregroundDecoration;
keyword double? width,
keyword double? height,
keyword BoxConstraints? constraints,
final EdgeInsetsGeometry? margin;
final Matrix4? transform;
final AlignmentGeometry? transformAlignment;
final Widget? child;
final Clip clipBehavior= Clip.none,
})
extends StatelessWidget
: assert(decoration != null || clipBehavior == Clip.none),
constraints = <...> {
final BoxConstraints? constraints;
} For reference, here's the current `Container` source:class Container extends StatelessWidget {
Container({
super.key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double? width,
double? height,
BoxConstraints? constraints,
this.margin,
this.transform,
this.transformAlignment,
this.child,
this.clipBehavior = Clip.none,
}) : assert(decoration != null || clipBehavior == Clip.none),
constraints = <...>;
final Widget? child;
/// Align the [child] within the container.
/// [...]
final AlignmentGeometry? alignment;
final EdgeInsetsGeometry? padding;
final Color? color;
final Decoration? decoration;
final Decoration? foregroundDecoration;
final BoxConstraints? constraints;
final EdgeInsetsGeometry? margin;
final Matrix4? transform;
final AlignmentGeometry? transformAlignment;
final Clip clipBehavior;
} (I'm not super sure about the formatting tbh) |
Thanks! That helps a lot. I have two questions, if you don't mind:
|
A way to define a parameter without defining a field alongside it.
Sure is class MyClass.name(final int count);
void main() {
MyClass.name(42);
} All of these have been discussed way higher in the thread. I'm just rehashing already suggested ideas ; with some small twists |
The primary appeal of class Foo {
this(final int a, final String? b);
Foo.other(this.*);
} Consider a flutter widget with a lot of properties where you override only a few in a secondary constructor |
@n7trd mentioned I'm a little bit worried about mechanisms that quantify over groups like "all public instance variables declared by this class/enum/..." because they are quite inflexible. It may seem powerful and convenient in a given example, but if your use case doesn't match this lucky case 100% then you may suddenly have to write everything in full. One way to find a similar mechanism which is somewhat more flexible is to tie it up to the subsequent invocation (similar to mechanisms that we'd use with forwarding). We would abstract over all parameters of the forwardee, e.g., by referring to "something with asterisk", based on the syntactic approach taken with So let's say that For the invocation where these parameters are passed on, we could use For example, class const Text._(
String? data,
InlineSpan? textSpan, {
super.key,
TextStyle? style,
StrutStyle? strutStyle,
TextAlign? textAlign,
TextDirection? textDirection,
Locale? locale,
bool? softWrap,
TextOverflow? overflow,
double? textScaleFactor,
TextScaler? textScaler,
int? maxLines,
String? semanticsLabel,
TextWidthBasis? textWidthBasis,
ui.TextHeightBehavior? textHeightBehavior,
Color? selectionColor
}) extends StatelessWidget {
const Text(String data, {*}): this._(data, null, *:*);
const Text.rich(InlineSpan textSpan, {*}): this._(null, textSpan, *:*);
...
} The DartDoc page for each constructor should probably show both the expansion where every parameter declaration is written explicitly and the abbreviated form which helps the reader understand that all those named parameters are exactly the same as the ones in This would allow us to get the brevity which has been a main characteristic of primary constructors all the time: The constructor Note that this approach allows us to have additional instance variables whose initialization is handled in a different way (e.g., in an initializer list), and it also allows us to include instance variables whose name is private (#2509, also discussed in earlier comments in this issue). @rrousselGit wrote:
Right, that was also one of the things I was worried about. But if we're abstracting over callee parameters rather than instance variables then we can do it. class const A._(int a, {String? b}) {
A(*, {*}): this._(*, *:*);
// Of course, if we're just forwarding everything then we could also redirect.
factory A = A._; // #4135
}
Agreed! The language team has had discussions about both approaches, but I also prefer going from the parameters to the instance variables.
This proposal is already getting somewhat close to that: class Foo(final int value);
class const Bar(int value);
class Baz({int value = 0}): assert(value > 0), super(...) extends Base;
class Quaz(this.value) {
final num value;
}
class Qux(this._value) {
final Object? _value;
} The last one is easy because class A({int _x = 0}) {
void foo() => print(_x); // OK, we do have an instance variable `int _x`.
}
void main() {
A(x: 10); // OK, the name for callers is the corresponding public name for `_x`.
} @n7trd wrote:
Let's try. Here is the original: class Home extends StatefulWidget {
const Home({
super.key,
required this.useLightMode,
required this.useMaterial3,
required this.colorSelected,
required this.handleBrightnessChange,
required this.handleMaterialVersionChange,
required this.handleColorSelect,
required this.handleImageSelect,
required this.colorSelectionMethod,
required this.imageSelected,
});
final bool useLightMode;
final bool useMaterial3;
final ColorSeed colorSelected;
final ColorImageProvider imageSelected;
final ColorSelectionMethod colorSelectionMethod;
final void Function(bool useLightMode) handleBrightnessChange;
final void Function() handleMaterialVersionChange;
final void Function(int value) handleColorSelect;
final void Function(int value) handleImageSelect;
@override
State<Home> createState() => _HomeState();
} And the abbreviated version, relying on nothing but this proposal: class const Home({
super.key,
bool useLightMode,
bool useMaterial3,
ColorSeed colorSelected,
ColorImageProvider imageSelected,
ColorSelectionMethod colorSelectionMethod,
void Function(bool useLightMode) handleBrightnessChange,
void Function() handleMaterialVersionChange,
void Function(int value) handleColorSelect,
void Function(int value) handleImageSelect,
}) extends StatefulWidget {
@override
State<Home> createState() => _HomeState();
} Note that This is very similar to @rrousselGit's The bulky class header with the
The named constructor should presumably do something which differs from the behavior of Anyway, let's make the primary one private, and add a few variants: class const Home._({
super.key,
bool useLightMode,
bool useMaterial3,
ColorSeed colorSelected,
ColorImageProvider imageSelected,
ColorSelectionMethod colorSelectionMethod,
void Function(bool useLightMode) handleBrightnessChange,
void Function() handleMaterialVersionChange,
void Function(int value) handleColorSelect,
void Function(int value) handleImageSelect,
}) extends StatefulWidget {
const factory Home = Home._; // #4135
factory Home.named1({*}) => Home._(*:*);
const Home.named2({bool useLightMode = true, *}):
this._(useLightMode: useLightMode, *:*); // Change a parameter to optional.
@override
State<Home> createState() => _HomeState();
} |
Given how this proposal evolved, it goes far beyond the original idea. In its current state, it's more of a replacement for the existing class/constructor syntax. It seems to cover everything and more, becoming even more valuable as the number of parameters, fields, and named constructors grows. There's a reason why I brought all the Flutter examples into the discussion. There's a significant need to eliminate the enormous amount of field repetition due to the way its API was designed. With that in mind, primary constructors fit (unintentionally?) very nicely. Considering this, I don't think they remain limited to specialized use cases anymore. What I really hope, given all of this, is that it could be introduced more as an "upgrade" to the current class/constructor syntax. How? By sticking to the primary body constructor version and dropping the header variant. It requires slightly more typing, but at least the class declaration & additions (extends, implements, and so on) doesn't get pushed somewhere between the lines when there are more constructor parameters. AND it deviates less from the current class/constructor in general. Putting it all together, we get something like this: class Text extends StatelessWidget {
const Text._({
String? data,
InlineSpan? textSpan, {
super.key,
TextStyle? style,
StrutStyle? strutStyle,
TextAlign? textAlign,
TextDirection? textDirection,
Locale? locale,
bool? softWrap,
TextOverflow? overflow,
double? textScaleFactor,
TextScaler? textScaler,
int? maxLines,
String? semanticsLabel,
TextWidthBasis? textWidthBasis,
ui.TextHeightBehavior? textHeightBehavior,
Color? selectionColor
});
const Text(String data, {*}): this._(data, null, *:*);
const Text.rich(InlineSpan textSpan, {*}): this._(null, textSpan, *:*);
}
// edited it. hope it "compiles". Not sure how a private constructor and the proposal fits together ;-) looks nice i think 🤔 |
In dart, lexical scope always wins. Try this out: class A {
int x=10;
}
int x=-1; // comment it out and run again
class B extends A {
foo()=> print(x);
}
void main() {
B().foo();
} This program prints -1. If you comment out the declaration int a = -1;
class A {
this(int a);
method()=>print(a); // prints -1 regardless of the instance `a`.
} The idea requires tweaking the scoping rules for no apparent reason. |
@tatumizer This wasn't intended as a new idea - I was just trying to convert the code above to use a primary constructor body syntax, but I made a mistake while doing ;-) |
I'd prefer to have both. I don't really understand why we'd prevent class const TinyDataThing(int i, String s); just because class const Home._({
super.key,
bool useLightMode,
bool useMaterial3,
ColorSeed colorSelected,
ColorImageProvider imageSelected,
ColorSelectionMethod colorSelectionMethod,
void Function(bool useLightMode) handleBrightnessChange,
void Function() handleMaterialVersionChange,
void Function(int value) handleColorSelect,
void Function(int value) handleImageSelect,
}) extends StatefulWidget {
const factory Home = Home._; // #4135
factory Home.named1({*}) => Home._(*:*);
const Home.named2({bool useLightMode = true, *}):
this._(useLightMode: useLightMode, *:*); // Change a parameter to optional.
@override
State<Home> createState() => _HomeState();
} may have a header which is unusually verbose and not very readable. In that case, just use the in-body form with About the scope rules: The intention is that a primary constructor has the special ability to declare formal parameters and at the same time introduce instance variables with the same name (OK, with With class Text extends StatelessWidget {
const this._({
String? data,
InlineSpan? textSpan, {
super.key,
TextStyle? style,
StrutStyle? strutStyle,
TextAlign? textAlign,
TextDirection? textDirection,
Locale? locale,
bool? softWrap,
TextOverflow? overflow,
double? textScaleFactor,
TextScaler? textScaler,
int? maxLines,
String? semanticsLabel,
TextWidthBasis? textWidthBasis,
ui.TextHeightBehavior? textHeightBehavior,
Color? selectionColor
});
const Text(String data, {*}): this._(data, null, *:*);
const Text.rich(InlineSpan textSpan, {*}): this._(null, textSpan, *:*);
} We do need to mark the in-body primary constructor syntactically because the plain declaration I do recognize that the |
@eernstg: you are addressing just half of the argument: namely, whether it's easy or not to remember that the syntax final x=0;
class C {
this(int x=1); // the lexical scope of inner x ends here
method() {
print(x); // 0 - b/c it's the outer "final int x=0", not this.x
print(this.x); // can be anything
}
} What you are proposing is not just a silent declaration of an extra field, but also the sudden emergence of x in the lexical scope. Also (though it's a minor point compared with the above), the syntax class C {
int x;
A(this.x);
A.named(int x=0): this(x); // HERE it is. `this(x)` in the initializer
} that |
[Update: There is considerable interest on the team in adding primary constructors as a general feature. This original discussion issue has been repurposed as the tracking issue for the general feature request.]
Introduction
Primary constructors is a feature that allows for specifying one constructor
and a set of instance variables, with a concise and crisp syntax. Consider this
Point
class defined using the current class syntax for the constructor and fields:With the primary constructor feature, this class can defined with this much shorter syntax:
Discussion
[original issue content below]
In the proposal for structs and extension structs, I propose to add primary constructors to structs. Briefly, the class name (or the type parameter list if any) may/must be followed by a parenthesized list of variable declarations as such:
In the struct proposal, these are always final by default, and are restricted in various ways (i.e. they may not be late, they may not be const). They are allowed to declare initializers, which are used to generate default initialization values.
This issue is to discuss the possibility of splitting this out, and making it a general feature for classes as well.
Initial points in favor of this include:
Initial points against include:
The text was updated successfully, but these errors were encountered: