Constructors

Player now contains behavior and data you defined. For example, you specified an isImmortal property:

    val isImmortal = false

You used a val because once the player is created, their immortality should never be reassigned. But this property declaration means that, at the moment, no player can be immortal: There is currently no way to initialize isImmortal to any value other than false.

This is where a primary constructor comes into play. A constructor allows its caller to specify the initial values that an instance of a class will require in order to be constructed. Those values are then available for assignment to the properties defined within the class.

Like a function, a constructor defines expected parameters that must be provided as arguments. To specify what is needed for a Player instance to work correctly, you are going to define the primary constructor in Player’s header. Update Player.kt to provide initial values for each of Player’s properties using temporary variables.

(Why prepend these variable names with underscores? Temporary variables, including parameters that you do not need to reference more than once, are often given a name starting with an underscore to signify that they are single-use.)

Now, to create an instance of Player, you provide arguments that match the parameters you added to the constructor. Instead of hardcoding the value for the player’s name property, for example, you pass an argument to Player’s primary constructor. Change the call to Player’s constructor in main to reflect this.

Consider how much functionality the primary constructor has added to Player: Before, a player of NyetHack was always named Madrigal, was never immortal, and so on. Now, a player can be named anything, and the door to immortality is open – none of Player’s data is hardcoded.

Run Game.kt to confirm that the output has not changed.

Notice the one-to-one relationship between the constructor parameters in Player and the class properties: You have a parameter and a class property for each property to be specified when the player is constructed.

For properties that use the default getter and setter, Kotlin allows you to specify both in one definition, rather than having to assign them using temporary variables. name uses a custom getter and setter, so it cannot take advantage of this feature, but Player’s other properties can.

Update the Player class to define healthPoints, isBlessed, and isImmortal as properties in Player’s primary constructor. (Do not neglect to delete the underscores before the names of the variables.)

For each constructor parameter, you specify whether it is writable or read-only. By specifying the parameters with val or var keywords in the constructor, you define properties for the class, whether they are val or var properties, and the parameters the constructor will expect arguments for. You also implicitly assign each property to the value passed to it as an argument.

Duplication of code makes it harder to make changes. Generally, we prefer this way of defining class properties because it leads to less duplication. You were not able to use this syntax for name, because of its custom getter and setter, but in other cases, defining a property in a primary constructor is often the most straightforward choice.

Constructors come in two flavors: primary and secondary. When you specify a primary constructor, you say, “These parameters are required for any instance of this class.” When you specify a secondary constructor, you provide alternative ways to construct the class (while still meeting the requirements of the primary constructor).

A secondary constructor must either call the primary constructor, providing it all of the arguments it requires, or call through to another secondary constructor – which follows the same rule. For example, say you know that in most cases a player will begin with 100 health points, will be blessed, and will be mortal. You can define a secondary constructor to provide that configuration. Add a secondary constructor to Player:

You can define multiple secondary constructors for different combinations of parameters. This secondary constructor calls through to the primary constructor with a certain set of parameters. The this keyword in this case refers to the instance of the class for which this constructor is defined. Specifically, this is calling into another constructor defined in the class – the primary constructor.

Because this secondary constructor provides default values for healthPoints, isBlessed, and isImmortal, you do not need to pass arguments for those parameters when calling it. Call Player’s secondary constructor from Game.kt instead of its primary constructor.

You can also use a secondary constructor to define initialization logic – code that will run when your class is instantiated. For example, add an expression that reduces the player’s health points value to 40 if their name is Kar.

Although they are useful for defining alternative logic to be run on instantiation, secondary constructors cannot be used to define properties like primary constructors can. Class properties must be defined in the primary constructor or at the class level.

Run Game.kt to see that Madrigal is still blessed and has health points, showing that Player’s secondary constructor was called from Game.kt.

When defining a constructor, you can also specify default values that should be assigned if an argument is not provided for a specific parameter. You have seen these default arguments in the context of functions, and they work the same way with both primary and secondary constructors. For example, set the default value for healthPoints with a default parameter value of 100 in the primary constructor, as follows:

Because you added a default argument value to the healthPoints parameter in the primary constructor, you removed the healthPoints argument passed from Player’s secondary constructor to its primary constructor. This gives you another way to instantiate Player: with or without an argument for healthPoints.

    // Player constructed with 64 health points using the primary constructor
    Player("Madrigal", 64, true, false)

    // Player constructed with 100 health points using the primary constructor
    Player("Madrigal", true, false)

    // Player constructed with 100 health points using the secondary constructor
    Player("Madrigal")

The more default arguments you use, the more options you have for calling your constructor. More options can open the door for more ambiguity, so Kotlin provides named constructor arguments, just like the named arguments that you have used to call functions.

Compare the following two options for constructing an instance of Player:

    val player = Player(name = "Madrigal",
            healthPoints = 100,
            isBlessed = true,
            isImmortal = false)

    val player = Player("Madrigal", 100, true, false)

Which option do you find to be more readable? If you chose the first, we agree with your judgment.

Named argument syntax lets you include the parameter name for each argument to improve readability. This is especially useful when you have multiple parameters of the same type: If you see “true” and “false” both passed into the Player constructor, named arguments will help you determine which value corresponds to which parameter. This reduced ambiguity leads to another benefit: Named arguments allow you to specify the arguments to a function or constructor in any order. If parameters are unnamed, then you need to refer to the constructor to know their order.

You may have noticed that the secondary constructor you wrote for Player used named arguments, similar to the ones that you saw in Chapter 4.

    constructor(name: String) : this(name,
            healthPoints = 100,
            isBlessed = true,
            isImmortal = false)

When you have more than a few arguments to provide to a constructor or function, we recommend using named parameters. They make it easier for readers to keep track of which argument is being passed as which parameter.