55

I want to use Lazy initialization for some of my properties in Swift. My current code looks like this:

lazy var fontSize : CGFloat = {
  if (someCase) {
    return CGFloat(30)
  } else {
    return CGFloat(17)
  }
}()

The thing is that once the fontSize is set it will NEVER change. So I wanted to do something like this:

lazy let fontSize : CGFloat = {
  if (someCase) {
    return CGFloat(30)
  } else {
    return CGFloat(17)
  }
}()

Which is impossible.

Only this works:

let fontSize : CGFloat = {
  if (someCase) {
    return CGFloat(30)
  } else {
    return CGFloat(17)
  }
}()

So - I want a property that will be lazy loaded but will never change. What is the correct way to do that? using let and forget about the lazy init? Or should I use lazy var and forget about the constant nature of the property?

Bill
  • 44,502
  • 24
  • 122
  • 213
YogevSitton
  • 10,068
  • 11
  • 62
  • 95
  • 4
    I agree - Swift needs lazy lets. Another problem with the lazy var pattern (when the value is certain not to change) is that simply reading the property for the first time counts as mutating the object (internally it does mutate - the lazy value gets stored), but that means external code has to declare the object as var, even though from the external code point of view, the object doesn't change. – Daniel Howard Nov 23 '15 at 18:37
  • 2
    the only `lazy` you can get is with `var` (as of Swift 2.1), if it's only a float assignment I wouldn't worry about the lazy, especially if you absolutely know it will be accessed – bshirley Jan 28 '16 at 21:57

4 Answers4

29

This is the latest scripture from the Xcode 6.3 Beta / Swift 1.2 release notes:

let constants have been generalized to no longer require immediate initialization. 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.

This enables patterns like:

let x: SomeThing
if condition {
    x = foo()
} else {
    x = bar()
}

use(x)

which formerly required the use of a var, even though there is no mutation taking place. (16181314)

Evidently you were not the only person frustrated by this.

Chris Conover
  • 8,889
  • 5
  • 52
  • 68
  • @iGodric - I think I misinterpreted the statement in the documentation. I will remove my comment. – rghome May 19 '16 at 20:47
  • How does this prohibit lazily-loading it just once? – Ky - Nov 17 '16 at 17:22
  • This is actually very confusing, because when you set a breakpoint at a let constant in a view controller it always gets executed before `viewDidLoad()` - meaning before it's used. So with this feature the let constants actually become variables. – turingtested Apr 05 '19 at 07:05
25

Swift book has the following note:

You must always declare a lazy property as a variable (with the var keyword), because its initial value might not be retrieved until after instance initialization completes. Constant properties must always have a value before initialization completes, and therefore cannot be declared as lazy.

This makes sense in the context of implementing the language, because all constant stored properties are computed before initialization of an object has finished. It does not mean that the semantic of let could have been changed when it is used together with lazy, but it has not been done, so var remains the only option with lazy at this point.

As far as the two choice that you presented go, I would decide between them based on efficiency:

  • If accessing the value of a property is done rarely, and it is expensive to compute upfront, I would use var lazy
  • If the value is accessed in more than 20..30% of cases or it is relatively inexpensive to compute, I would use let

Note: I would further optimize your code to push the conditional into CGFloat initializer:

let fontSize : CGFloat = CGFloat(someCase  ? 30 : 17)
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • @godmoney Yes. Once the `if` statement is replaced with a conditional expression, a lot of syntax around it needed to use statements can be removed. Of course if your initializer is more complex - say, when it uses a loop, you would have to go back to your syntax. – Sergey Kalinichenko Jan 11 '15 at 10:45
  • 3
    You don't need `: CGFloat` because the type will be inferred. – jowie Apr 06 '18 at 09:08
  • 2
    I don't like it when language implementation specifics affect the abstract's logic. There are quite a few annoying things in Swift and missing `lazy let` is one of them. – Robo Robok Feb 06 '19 at 02:04
14

As dasblinkenlight points out lazy properties should always be declared as variables in Swift. However it is possible make the property read-only so it can only be mutated from within the source file that the Entity was defined in. This is the closest I can get to defining a "lazy let".

private(set) lazy var fontSize: CGFloat = {
    if someCase {
        return 30
    } else {
        return 17
    }
}()
sdduursma
  • 2,651
  • 3
  • 16
  • 16
3

You can use Burritos for lazy constant properties. This library provides different property wrappers for Swift 5.1. Install it with CocoaPods by adding the following line to your Podfile:

pod 'Burritos'

With this library you can replace

lazy var fontSize : CGFloat = {
  if (someCase) {
    return CGFloat(30)
  } else {
    return CGFloat(17)
  }
}()

with

@LazyConstant var fontSize : CGFloat = {
    if (someCase) {
        return CGFloat(30)
    } else {
        return CGFloat(17)
    }
}()

And then self.fontSize = 20 leads to compilation error.

Roman Podymov
  • 4,168
  • 4
  • 30
  • 57