跳转至

9 Constructors

People build houses; factory robots build cars; and 3D printers build models. In the programming world, constructors are methods that create, or construct, instances of a class. That is to say, constructors build new objects. Constructors have the same name as the class, and the implicit return type of the constructor method is also the same type as the class itself.

This chapter will teach you about the differences between the various types of constructor methods Dart provides for building classes, which include generative, named, forwarding and factory constructors.

Default Constructor

When you don’t specify a constructor, Dart provides a default constructor that takes no parameters and just returns an instance of the class. For example, defining a class like this:

class Address {
  var value = '';
}

Is equivalent to writing it like this:

class Address {
  Address();
  var value = '';
}

Including the default Address() constructor is optional.

Sometimes you don’t want the default constructor, though. You’d like to initialize the data in an object at the same time that you create the object. The next section shows how to do just that.

Custom Constructors

If you want to pass parameters to the constructor to modify how your class builds an object, you can. It’s similar to how you wrote functions with parameters in Chapter 7, “Functions”.

Like the default constructor above, the constructor name should be the same as the class name. This type of constructor is called a generative constructor because it directly generates an object of the same type.

You’ll continue to build on the User class you wrote in Chapter 8, “Classes”. Here’s the code you had at the end of that chapter:

class User {
  int id = 0;
  String name = '';

  String toJson() {
    return '{"id":$id,"name":"$name"}';
  }

  @override
  String toString() {
    return 'User(id: $id, name: $name)';
  }
}

Copy that to your project below the main method before continuing on with the rest of the chapter.

Long-Form Constructor

In Dart, the convention is to put the constructor before the property variables. Add the following generative constructor method at the top of the class body:

User(int id, String name) {
  this.id = id;
  this.name = name;
}

This is known as a long-form constructor. You’ll understand why it’s considered “long-form” when you see the short-form constructor later.

Without the toJson and toString methods, this is what the class looks like:

class User {
  User(int id, String name) {
    this.id = id;
    this.name = name;
  }

  int id = 0;
  String name = '';

  // ...
}

this is a new keyword. What does it do?

The keyword this in the constructor body allows you to disambiguate which variable you’re talking about. It means this object. So this.name refers to the object property called name, while name (without this) refers to the constructor parameter. Using the same name for the constructor parameters as the class properties is called shadowing. So the constructor above takes the id and name parameters and uses this to initialize the properties of the object.

Write the following code in main to create a User object by passing in some arguments:

final user = User(42, 'Ray');
print(user);

Once you’ve created the object, you can access its properties and other methods just as you did in the last chapter. However, you can’t use the default constructor User()anymore since id and name are required positional parameters.

Short-Form Constructor

Dart also has a short-form constructor where you don’t provide a function body, but you instead list the properties you want to initialize, prefixed with the this keyword. Arguments you send to the short form constructor are used to initialize the corresponding object properties.

Here’s the long-form constructor you currently have:

User(int id, String name) {
  this.id = id;
  this.name = name;
}

Now replace that with the following short-form constructor:

User(this.id, this.name);

Dart infers the constructor parameter types of int and String from the properties themselves that are declared in the class body.

The class should now look like this:

class User {
  User(this.id, this.name);

  int id = 0;
  String name = '';

  // ...
}

Run the code again. You’ll see the short-form constructor works just like the longer form you replaced, but it’s just a little tidier now.

Note

You could remove the default property values of 0 and '' at this point since id and name are guaranteed to be initialized by the constructor parameters. However, there’s an intermediate step in the next section where they’ll still be useful. Keeping the default values a little longer will allow this chapter to postpone dealing with null safety until it can be covered more fully in Chapter 11, “Nullability”.

Named Constructors

Dart also has a second type of generative constructor called a named constructor, which you create by adding an identifier to the class name. It takes the following pattern:

ClassName.identifierName()

From here on, this chapter will refer to a constructor without the identifier as an unnamed constructor:

// unnamed constructor
ClassName()

// named constructor
ClassName.identifierName()

Why would you want a named constructor instead of the nice, tidy default one? Well, sometimes you have some common cases that you want to provide a convenience constructor for. Or maybe you have some special edge cases for constructing certain classes that need a slightly different approach.

Say, for example, that you want to have an anonymous user with a preset ID and name. You can do that by creating a named constructor. Add the following named constructor below the short-form constructor:

User.anonymous() {
  id = 0;
  name = 'anonymous';
}

The identifier, or named part, of the constructor is .anonymous. Named constructors may have parameters, but in this case, there are none. And since there aren’t any parameter names to get confused with, you don’t need to use this.id or this.name. Rather, you just use the property variables id and name directly.

Call the named constructor in main like so:

final anonymousUser = User.anonymous();
print(anonymousUser);

Run that and you’ll see the expected output:

User(id: 0, name: anonymous)

Note

Without default values for id and name, Dart would have complained that these variables weren’t being initialized, even though User.anonymousdoes in fact initialize them in the constructor body. You could solve the problem by using the late keyword, but that’s a topic for Chapter 11, “Nullability” — hence the default values here.

Forwarding Constructors

In the named constructor example above, you set the class properties directly in the constructor body. However, this doesn’t follow the DRY principle you learned earlier. You’re repeating yourself by having two different locations where you can set the properties. It’s not a huge deal, but imagine that you have five different constructors instead of two. It would be easy to forget to update all five if you had to make a change. And if the constructor logic were complicated, it would be easy to make a mistake.

One way to solve this issue is by calling the main constructor from the named constructor. This is called forwarding or redirecting. To do that, you use the keyword this again.

Delete the anonymous named constructor that you created above and replace it with the following:

User.anonymous() : this(0, 'anonymous');

This time there’s no constructor body, but instead, you follow the name with a colon and then forward the properties to the unnamed constructor. The forwarding syntax replaces User with this.

Also, now that you’ve moved property initialization from the constructor body to the parameter list, Dart is finally convinced that id and name are guaranteed to be initialized.

Replace these two lines:

int id = 0;
String name = '';

with the following:

int id;
String name;

No complaints from Dart.

You call the named constructor exactly like you did before:

final anonymousUser = User.anonymous();

The results are the same as well.

Optional and Named Parameters

Everything you learned about function parameters in Chapter 7, “Functions”, also applies to constructor method parameters. That means you can make parameters optional using square brackets:

MyClass([this.myProperty]);

Or you can make them optional and named using curly braces:

MyClass({this.myProperty});

Or named and required using curly braces and the required keyword:

MyClass({required this.myProperty});

Adding Named Parameters for User

Earlier when you instantiated a User object, you wrote this:

final user = User(42, 'Ray');

For someone not familiar with your User class, they might think 42 is Ray’s age, or his password, or how many pet cats he has. Using named parameters here would help a lot with readability.

Refactor the unnamed constructor in User by adding braces around the parameters. Since that makes the parameters optional, you could use the required keyword, but this time simply give them default values:

User({this.id = 0, this.name = 'anonymous'});

The compiler will complain about this because that change also requires refactoring the anonymous named constructor to use the parameter names in the redirect.

However, since the parameter defaults are now just what the anonymous constructor was doing before, you can delete the anonymous constructor and replace it with the following:

User.anonymous() : this();

Using the named constructor will now forward to the unnamed constructor with no arguments.

In main, the way to create an anonymous user is still the same, but now the way to create Ray’s user object is like so:

final user = User(id: 42, name: 'Ray');

That’s a lot more readable. They’re not cats; it’s clearly just an ID.

In case you’ve gotten lost, here’s what things look like now:

void main() {
  final user = User(id: 42, name: 'Ray');
  print(user);
  final anonymousUser = User.anonymous();
  print(anonymousUser);
}

class User {
  // unnamed constructor
  User({this.id = 0, this.name = 'anonymous'});

  // named constructor
  User.anonymous() : this();

  int id;
  String name;

  // ...
}

Initializer Lists

You might have discovered a small problem that exists with your class as it’s now written. Take a look at the following way that an unscrupulous person could use this class:

final vicki = User(id: 24, name: 'Vicki');
vicki.name = 'Nefarious Hacker';
print(vicki);
// User(id: 24, name: Nefarious Hacker)

If those statements were spread throughout the codebase instead of being in one place as they are here, someone printing the vicki user object and expecting a real name would get a surprise. “Nefarious Hacker” is definitely not what you’d expect. Once you’ve created the User object, you don’t want anyone to mess with it.

But forget nefarious hackers; you’re the one who’s most likely to change a property and then forget you did it.

There are a couple of ways to solve this problem. You’ll see one solution now and a fuller solution later.

Private Variables

As you learned in the previous chapter, Dart allows you to make variables private by adding an underscore _ in front of their name.

Change the id property to _id and name to _name. Since these variables are used in several locations throughout your code, let VS Code help you out. Put your cursor on the variable name and press F2. Edit the variable name and press Enter to change all of the references at once.

img

Oh…, that actually renamed a few more things than you intended since it also renamed what was in the main function. But that’s OK. Just delete everything inside the body of main for now.

There is still one problem left with the unnamed constructor in User:

User({this._id = 0, this._name = 'anonymous'});

The compiler gives you an error:

Named parameters can’t start with an underscore.

Fix that by deleting the unnamed constructor and replacing it with the following:

User({int id = 0, String name = 'anonymous'})
      : _id = id,
        _name = name;

Do you see the colon that precedes _id? The comma-separated list that comes after it is called the initializer list. One use for this list is exactly what you’ve done here. Externally, the parameters have one name, while internally, you’re using private variable names.

The initializer list is always executed before the body of the constructor if the body exists. You don’t need a body for this constructor, but if you wanted to add one, it would look like this:

User({int id = 0, String name = 'anonymous'})
    : _id = id,
      _name = name {
  print('User name is $_name');
}

The constructor would initialize _id and _name before it ran the print statement inside the braces.

Why Aren’t the Private Properties Private?

It turns out that your nefarious hacker can still access the “private” fields of User. Add the following two lines to main to see this in action:

final vicki = User(id: 24, name: 'Vicki');
vicki._name = 'Nefarious Hacker';

What’s that all about? Well, using an underscore before a variable or method name makes it library private, not class private. For your purposes in this book, a library is simply a file. Since the main function and the User class are in the same file, nothing in User is hidden from main. To see private variables in action, you’ll need to make another file so that you aren’t using your class in the same file in which it’s defined.

Create a new folder in the root of your project called lib, which is short for “library”. This is the standard name and location where Dart expects to find the project files you want to import. To create the new folder in VS Code, you can click on the project folder in the Explorer panel and then press the New Folder button:

img

Next, create a new file in lib called user.dart. You can click on lib in the Explorer panel and then press the New File button:

img

Now move the entire User class over to lib/user.dart.

If you’re using the starter project, your folder structure should look like this now:

img

Go back to bin/starter.dart, the one with the main function, and add the library import to the top of the class:

import 'package:starter/user.dart';

Here are some notes:

  • If you aren’t using the starter project, replace starter with whatever your project name is.
  • There’s no reference to the lib folder here. Dart knows that the package files with your project name are in the lib folder. A package is a collection of library files.

Now you’ll notice that in the main function you no longer have access to _name:

vicki._name = 'Nefarious Hacker';

This produces an error:

The setter '_name' isn't defined for the type 'User'.

Great! Now it’s no longer possible to change the properties after the object has been created. Delete or comment out that entire line:

// vicki._name = 'Nefarious Hacker';

Constant Constructors

You’ve already learned how to keep people from modifying the properties of a class by making them private. Another thing you can do is to make the properties immutable, that is, unchangeable. By using immutable properties, you don’t even have to make them private.

Making Properties Immutable

There are two ways to mark a variable immutable in Dart: final and const. However, since the compiler won’t know what the properties are until runtime, your only choice here is to use final.

In the User class in lib/user.dart, add the final keyword before both property declarations. Your code should look like this:

final String _name;
final int _id;

Adding final means that _name and _id can only be given a value once, that is, when the constructor is called. After the object has been created, those properties will be immutable. You should keep the String and int type annotations because removing them would cause the compiler to fall back to dynamic.

Making Classes Immutable

If the objects of a particular class can never change because all fields of the class are final, you can add const to the constructor to ensure that all instances of the class will be constants at compile time.

Since both the fields of your User class are now final, this class is a good candidate for a compile-time constant.

Replace both constructors with the following:

const User({int id = 0, String name = 'anonymous'})
    : _id = id,
      _name = name;

const User.anonymous() : this();

Note the const keyword in front of both constructors.

Now you can declare your User objects as compile-time constants like so:

const user = User(id: 42, name: 'Ray');
const anonymousUser = User.anonymous();

Benefits of Using Const

In addition to being immutable, another benefit of const variables is that they’re canonical instances, which means no matter how many instances you create, as long as the properties used to create them are the same, Dart will only see a single instance. You could instantiate User.anonymous() a thousand times across your app without incurring the performance hit of having a thousand different objects.

Note

Flutter uses this pattern frequently with its const widget classes in the user interface of your app. Since Flutter knows that the const widgets are immutable, it doesn’t have to waste time recalculating and drawing the layout when it finds these widgets.

Make it your goal to use const objects and constructors as much as possible. It’s a performance win!

Exercise

Given the following class:

class PhoneNumber {
  String value = '';
}
  1. Make value a final variable, but not private.
  2. Add a const constructor as the only way to initialize a PhoneNumber object.

Factory Constructors

All of the constructors that you’ve seen up until now have been generative constructors. Dart also provides another type of constructor called a factory constructor.

A factory constructor provides more flexibility in how you create your objects. A generative constructor can only create a new instance of the class itself. However, factory constructors can return existing instances of the class, or even subclasses of it. You’ll learn about subclasses in Dart Apprentice: Beyond the Basics, Chapter 3, “Inheritance”. This is useful when you want to hide the implementation details of a class from the code that uses it.

The factory constructor is basically a special method that starts with the factorykeyword and returns an object of the class type. For example, you could add the following factory constructor to your User class:

factory User.ray() {
  return User(id: 42, name: 'Ray');
}

The factory method uses the generative constructor to create and return a new instance of User. You could also accomplish the same thing with a named constructor, though.

A more common example you’ll see is using a factory constructor to make a fromJsonmethod:

factory User.fromJson(Map<String, Object> json) {
  final userId = json['id'] as int;
  final userName = json['name'] as String;
  return User(id: userId, name: userName);
}

You would create a User object from the constructor like so:

final map = {'id': 10, 'name': 'Sandra'};
final sandra = User.fromJson(map);

You’ll learn how the Map collection works in Chapter 14, “Maps”. The thing to pay attention to now is that the factory constructor body allows you to perform some work before returning the new object, and you didn’t exposing the details of that work to whoever is using the class.

For example, you could create a User.fromJson constructor with a named constructor like so:

User.fromJson(Map<String, Object> json)
    : _id = json['id'] as int,
      _name = json['name'] as String;

However, there isn’t much else you can do with id and name. With a factory constructor, though, you could do all kinds of validation, error checking and even modification of the arguments before creating the object. This is actually highly desirable in the case here because if 'id' or 'name' didn’t exist in the map, then your app would crash because you aren’t handling null.

Note

Using a factory constructor over a named constructor can also help to prevent breaking changes for subclasses of your class. That topic is a little beyond the scope of this chapter, but you can read https://stackoverflow.com/a/66117859 for a longer explanation.

You’ll see a few more uses of the factory constructor in Chapter 10, “Static Members”.

Constructor Comparisons

Since there are so many ways that constructors can vary, here’s a brief comparison.

Constructors can be:

  • Forwarding or non-forwarding.
  • Named or unnamed.
  • Generative or factory.
  • Constant or not constant.
  • With parameters or without.
  • Short-form or long-form.
  • Public or private.

Many of those options can vary independently of each other. For example, the following is a public, non-forwarding, unnamed, generative, const constructor with parameters.

const User(this.id, this.name);

Or here’s a private, non-forwarding, named, generative, non-constant constructor without parameters:

DatabaseHelper._internal();

You’ll learn one use for private constructors in the next chapter when you get to the section on singletons. See you there!

Challenges

Before moving on, here is a challenge to test your knowledge of constructors. It’s best if you try to solve it yourself, but a solution is available if you get stuck. It’s located with the supplementary materials for this book.

Challenge 1: Bert and Ernie

Create a Student class with final firstName and lastName string properties and a variable grade as an int property. Add a constructor to the class that initializes all the properties. Add a method to the class that nicely formats a Student for printing. Use the class to create students bert and ernie with grades of 95 and 85, respectively.

Key Points

  • You create an object from a class by calling a constructor method.
  • Generative constructors can be unnamed or named.
  • Unnamed generative constructors have the same name as the class, while named generative constructors have an additional identifier after the class name.
  • You can forward from one constructor to another by using the keyword this.
  • Initializer lists allow you to initialize field variables.
  • Adding const to a constructor allows you to create immutable, canonical instances of the class.
  • Factory constructors allow you to hide the implementation details of how you provide the class instance.