Memory implications Static constructs vs Singletons

Hello folks :wave:t2:

Let's put aside the controversy about singletons as it's not really important for this question.

I'm more interested on the memory and/or runtime (if any) implications of the following two cases:

Let's say I have a typical singleton setup

class MySuperSingleton {
    static let shared = MySuperSingleton()

    var state: String = "default value"
    private init() {}

    func doStuff() {
        print("State from Singleton \(state)")
    }
}

Then I can just interact with it as you would expect:

var foo = MySuperSingleton.shared

foo.state = "new value"

MySuperSingleton.shared.doStuff() // prints "new value"

What is the difference then if I were to have something like:

class MyStaticObject {
    static var state = "default value"
    static func doStuff() {
        print("State from static \(state)")
    }
}

let bar = MyStaticObject.self

bar.state = "new value"

MyStaticObject.doStuff() // prints "new value"

At first glance they look like they do behave exactly the same, the only difference I could see in Playgrounds is __lldb_expr_44.MyStaticObject.Type but that looks more like debugger information.

Could anyone explain to me what would be the underlying key differences if there's any?

Thanks!

In the first, you have an instance. In the second you have no instance, but rather global stuff huddled under a namespace.

(The class keyword in your second example actually causes an implicit init() and permits subclassing; when you use this pattern, make it enum instead to remove those unexpected abilities.)

The first is generally much more flexible. For example, only the first can conform to protocols. Try to make each example conform to CustomStringConvertible and the essence of the difference will quickly become apparent.

Hey @SDGGiesbrecht

Thanks for your reply!

The first two points make sense to me, I knew one was an instance & the other no instance but I actually did not see it as a namespace sort of thing for the latter, nice observation.

However, nothing prevents me from conforming to protocols in both cases:

class MySuperSingleton: CustomStringConvertible {
    var description: String = "default description"

    static let shared = MySuperSingleton()

    var state: String = "default value"
    private init() {}

    func doStuff() {
        print("State from Singleton \(state)")
    }
}

class MyStaticObject: CustomStringConvertible {

    var description: String = ""
    // I could hide init if I wanted 
    private init() {}
    static var state = "default value"
    static func doStuff() {
        print("State from static \(state)")
    }
}

The description property would just be not usable within the non-instance context, right?

I'm also able (not that is any useful) to arbitrarily define static/instance signature in protocols, like so:

protocol InstanceProtocol {
    func doProtocolStuff()
}

protocol StaticProtocol {
    static func doStaticProtocolStuff()
}

class MySuperSingleton: InstanceProtocol, StaticProtocol {
    // Compiles 
    static func doStaticProtocolStuff() { }
    func doProtocolStuff() { }
    

    static let shared = MySuperSingleton()

    var state: String = "default value"
    private init() {}

    func doStuff() {
        print("State from Singleton \(state)")
    }
}

class MyStaticObject: InstanceProtocol, StaticProtocol {
     // Compiles 
    static func doStaticProtocolStuff() {}
    func doProtocolStuff() { }
    
    static var state = "default value"
    static func doStuff() {
        print("State from static \(state)")
    }
}

So by looking again at this, feels like both could provide a similar functionality. But then again I'm not sure if there any behind the scenes implications I'm not seeing.

Bringing back the name-space point of view, both actually look like falling into that only difference being that one exposes an object instance which contains instance properties & the other provides no instance but rather a bundle of static objects (singletons?) under the same name-space.

Yes, I meant conform to and use as. You can add instance methods to an uninhabitable type (that is the technical term for a type that has no instances), but you cannot get an instance to call those methods on.

Swift’s Never type is a perfect example. You can conform it to anything to satisfy type constraints in order to let it propagate through the type system from method to method, and give it instance methods in order to do that. But since you can never generate an actual instance of Never, all those methods are unreachable dead code.

Swift’s Unicode is another example of an uninhabitable type, and it’s reason to exist is to provide a namespace.

Exactly.


Okay, protocols complicate things and require a more technical answer. static means the method or variable belongs once to the metatype, not separately to each instance. A protocol entry that uses static means that a conforming type’s metatype must have it once, not each instance separately. Each conforming type has a different metatype, and each metatypes has a separate implementation. This pattern certainly has its uses. Swift’s Equatable has static requirements.


Another complication is class. It is like static except that it is also inherited by subclasses and allows overriding. (A protocol requirement that is static can be fulfilled by class.)

2 Likes
Terms of Service

Privacy Policy

Cookie Policy