Search code examples
iosswiftinitializer

Assigning let variable in fallible initializer swift 1.2


I have a struct with a fallible initializer, not an instance method, but an initializer. After updating to 1.2, when I try to assign a let property inside the initializer, I receive the following error Cannot assign to 'aspectRatio' in self. My code below:

import Foundation

public struct MediaItem
{
public let url: NSURL!
public let aspectRatio: Double

public var description: String { return (url.absoluteString ?? "no url") + " (aspect ratio = \(aspectRatio))" }

// MARK: - Private Implementation

init?(data: NSDictionary?) {
    var valid = false
    if let urlString = data?.valueForKeyPath(TwitterKey.MediaURL) as? NSString {
        if let url = NSURL(string: urlString as String) {
            self.url = url
            let h = data?.valueForKeyPath(TwitterKey.Height) as? NSNumber
            let w = data?.valueForKeyPath(TwitterKey.Width) as? NSNumber
            if h != nil && w != nil && h?.doubleValue != 0 {
                aspectRatio = w!.doubleValue / h!.doubleValue
                valid = true
            }
        }
    }
    if !valid {
        return nil
    }
}

struct TwitterKey {
    static let MediaURL = "media_url_https"
    static let Width = "sizes.small.w"
    static let Height = "sizes.small.h"
}
}

My question is what do I do to fix this?


Solution

  • Swift 1.2 has closed a loophole having to do with let properties:

    The new rule is that a let constant must be initialized before use (like a var), and that it may only be initialized, not reassigned or mutated after initialization.

    That rule is exactly what you are trying to violate. aspectRatio is a let property and you have already given it a value in its declaration:

    public let aspectRatio: Double = 0
    

    So before we ever get to the initializer, aspectRatio has its initial value — 0. And that is the only value it can ever have. The new rule means that you can never assign to aspectRatio ever again, not even in an initializer.

    The solution is (and this was always the right way): assign it no value in its declaration:

    public let aspectRatio: Double
    

    Now, in the initializer, either assign it 0 or assign it w!.doubleValue / h!.doubleValue. In other words, take care of every possibility in the initializer, once. That will be the only time, one way or another, that you get to assign aspectRatio a value.

    If you think about it, you'll realize that this is a much more sensible and consistent approach; previously, you were sort of prevaricating on the meaning of let, and the new rule has stopped you, rightly, from doing that.


    In your rewrite of the code, you are failing to initialize all properties in the situation where you intend to bail out and return nil. I know it may seem counterintuitive, but you cannot do that. You must initialize all properties even if you intend to bail out. I discuss this very clearly in my book:

    A failable class initializer cannot say return nil until after it has completed all of its own initialization duties. Thus, for example, a failable subclass designated initializer must see to it that all the subclass’s properties are initialized and must call super.init(...) before it can say return nil. (There is a certain delicious irony here: before it can tear the instance down, the initializer must finish building the instance up.)

    EDIT: Please note that starting in Swift 2.2, this requirement will be lifted. It will be legal to return nil before initializing properties. This will put class initializers on a par with struct initializers, where this was already legal.