Louis DeVesto
Louis DeVesto

Reputation: 43

New to Swift - failable initializers

I am trying to convert a Java class to Swift:

// Card class with only a main function
public class Card {
// fields
private int numvalue;

// constructor(s)
public Card(int v) {
    if (v > 0 && v < 11) {
        this.numvalue = v;}
    else {
        System.out.println("Error: Card value is outside the allowed range.
        Value must be between 1 and 10 inclusive.")
    }

If I try to initialize a Card object using the constructor with a value that isn't between 1 and 10, the initialization fails, and a statement is printed in the Terminal explaining that the value was not within the acceptable range. I tried to recreate this in Swift.

class Card {
    var numValue : Int?
    init(numValue:Int) {
        if (numValue > 0 && numValue < 11) {self.numValue = numValue}
        else {print("Card value must be between 0 and 11")}
    }
    func setNumValue(value:Int) -> () {
        self.numValue = value
    }
    func getNumValue() -> Int {
        return self.numValue!
    }
}

The above works in the proper cases, but it appears to still create a Card instance even when an unacceptable value is passed, because I can still use the setNumValue(value:) method to change the value.

I tried making the init(numvalue:) method failable, but then I get an Optional(Card) if initialization is successful. Ideally, I'd like for any successfully initialized Card object to be a Card object, not an Optional containing a Card. Is this possible?

Upvotes: 2

Views: 640

Answers (3)

Leo Dabus
Leo Dabus

Reputation: 236410

If your object type doesn't need to persist you should use a Struct. BTW no need to make the value property a variable and create methods to just change its properties. You just create a new Card object to replace it instead of changing the old one.

Your Card struct should look like this:

struct Card {
    let value: Int
    init?(value: Int) {
        guard 1...10 ~= value else {
            print("Card value is out of range 1...10")
            return nil
        }
        self.value = value
    }
}

let card = Card(value: 2)       // returns an object of optional type (Card?)   "optional Card(value: 10)\n"
let nilCard = Card(value: 11)   // valus out of the range 1...10 will return nil

if let card = Card(value: 10)  {
    print(card)   // "Card(value: 10)\n"
}

I'd like for any successfully initialized Card object to be a Card object, not an Optional containing a Card. Is this possible?

Yes it is possible. Just add a precondition to your initializer and make it non failable:

struct Card {
    let value: Int
    init(value: Int) {
        precondition(1...10 ~= value, "Card value is out of range 1...10")
        self.value = value
    }
}


let card = Card(value: 2)                // returns an object of type (Card)   "Card(value: 2)\n"
let cantInitCard = Card(value: 11)       // won't execute (precondition not met range 1...10)

You can also add a second parameter range (optional because you can set a default value) for your card initializer and add a range property to your card so you can check whats your possible card values:

struct Card {
    let value: Int
    let range: ClosedRange<Int>
    init(value: Int, range: ClosedRange<Int> = 1...10) {     // you can set a default range for yor cards
        precondition(range ~= value, "Card value is out of range: " + range.description)
        self.value = value
        self.range = range
    }
}

let card = Card(value: 5)       // returns an object of type (Card) "Card(value: 5, range: 1...10)\n"
print(card.value, card.range)   // "5 1...10\n"

If you would like to have a card with values with a different range 1...13 just pass the range

let newCard = Card(value: 11,  range: 1...13)    // ok to execute (precondition met range 1...13) 
print(newCard.value, newCard.range)              // "11 1...13\n"

Upvotes: 1

Ryan H.
Ryan H.

Reputation: 2593

Ideally, I'd like for any successfully initialized Card object to be a Card object, not an Optional containing a Card. Is this possible?

This is not possible with the standard initialization schemes available in Swift. You will ultimately end up with an Optional<Card> if your initialization should fail under certain conditions.


Failable Initialization

You can make your initializer failable by adding a ? to the end of the init keyword. That is init?.

This indicates that the initializer could fail (return nil).

In your initializer, you can then return nil when the conditions for creating your instance are not met.

In this case, I'm using a guard statement, but you could also use an if statement.

class Card {
    var numValue : Int?

    init?(numValue: Int) {
        guard numValue > 0 && numValue < 11 else {
            print("Card value must be between 0 and 11")
            return nil
        }
        self.numValue = numValue
    }

    func setNumValue(value:Int) -> () {
        self.numValue = value
    }

    func getNumValue() -> Int {
        return self.numValue!
    }
}

Since you are now returning an optional instance of Card, you can use if let to safely unwrap the optional and do something with the instance, or handle the fact that an instance was not created successfully (you have a nil).

let card = Card(numValue: 12)
if let card = card {
    print(card)
} else {
    print("Unable to create a Card instance")
}

In the above example, the "Unable to create a Card instance" string would be printed (in addition to "Card value must be between 0 and 11").

More information about how failable initialization works in Swift can be found here.

Upvotes: 1

Andreas Oetjen
Andreas Oetjen

Reputation: 10199

Well, you'll have to check the returned Optional whether it is nil or not, and then unwrap it into a non-Optional:

class Card {
    var numValue : Int
    init?(numValue:Int) {
        guard (numValue > 0 && numValue < 11) else {
            print("Card value must be between 0 and 11")
            return nil
        }

        self.numValue = numValue
    }
    func setNumValue(value:Int) -> () {
        guard (numValue > 0 && numValue < 11) else {
            print("Card value must be between 0 and 11")
            return
        }
        self.numValue = value
    }
    func getNumValue() -> Int {
        return self.numValue
    }
}


let cOk = Card(numValue:1)
let cFails = Card(numValue:-1)

if let c = cOk {
    // now work with c because it's a Card,
    // not an Optional<Card> 
}

Upvotes: 2

Related Questions