Scope of static properties and methods

Hi.
I'm trying to understand how Swift uses static properties and methods.

The example below is hopefully self explanatory. This applies for properties as well as methods.

class Outer {
    static func outerStaticMethod() {
        print("outer static method")
    }
    class Inner {
        static func innerStaticMethod() {
            print("inner static method")
        }
        func instanceMethod() {
            print("can call outer static method directly without specifying scope")
            outerStaticMethod()
            print("need scope to call inner static method")
            Inner.innerStaticMethod()
        }
    }
}

let inner = Outer.Inner()

inner.instanceMethod()
// prints:
// can call outer static method directly without specifying scope
// outer static method
// need scope to call inner static method
// inner static method

This example uses methods, but the scope is the same when using properties.

Just wondering the idea behind why we cannot access a static method or property without specifying the class scope, but we CAN call a static method from the outer enclosing class without any scope.

This is not super clear to me and at first glance this seems inconsistent. I was wondering if it might be good to write some kind of proposal to unify static method/property behaviour.

However there is a good chance I am missing the entire reason behind this, so I would super appreciate any help here.

7 Likes

I don’t have any historical insight here, but I suspect this is because a bare innerStsticMethod() could be confused with an instance method on Inner, but the same doesn’t go for outerStaticMethod() since there is no Outer instance on which to call the method.

Of course, also possible that this was just an oversight :)

Yes, if it is an oversight, maybe we could formalize the behavior.
Is there a reason why a static method could not be accessed without using the full scope?
Seems to me like if there was an instance method with the same signature, it should override the static method.
This is kind of similar to how variable scope is handled with same-named variables.

1 Like

Each type scope has an implicit “self” binding. Unqualified member references are looked up as members of this “self” binding. For outer types the implicit “self” is always a metatype because there’s no “self” instance. For the innermost type scope, the “self” is an instance if the method is an instance method. Instance members are visible on instances, and static members are visible on metatypes. However static members are not visible on an instance. So it’s consistent, just maybe a bit confusing at first; let me know if me explanation made any sense.

12 Likes

Now I can call Outer's "outerStaticMethod" unprefixed, but only until I introduce an Inner's method with the same name (in which case the name would be misleading, but that's separate thing)... if I do so - the code will call that Inner's method instead.

Swift could have been designed differently, e.g. it could have allowed to call static methods without prefixing them so long as there is no instance method with the same name/signature. And only complain if there's possible ambiguity. I don't believe the current behaviour will change though.

Just want to point out that's true in other cases too. For example, uncommenting out Test.foo() in the code below generates a different result.

func foo() {
    print("This is a global function.")
}

struct Test {
    // func foo() {
    //     print("This is a method.")
    // }

    func test() {
        foo()
    }
}

Test().test()

I think the confusion with static method was because many people (including me) thought static methods should be handled specially when searching for a function invoked from within an instance. But if I understand Slava's explanation correctly, that's not the case in Swift.

Is this also why this code is valid?

@MainActor
final class LifetimeLogger {
    @MainActor
    static var lifetimeLoggerID = 0
    
    let id = lifetimeLoggerID // no need to use Self.lifetimeLoggerID, presumably because implicit self == Self
    
    init() {
        Self.lifetimeLoggerID += 1
        print("LifetimeLogger \(id) initialized")
    }
    
    deinit {
        print("LifetimeLogger \(id) deinitialized")
    }
}
1 Like

Yeah. The initial value expression for a stored property is a "static context" because there's no instance (it hasn't been initialized yet when the initial value expression runs!)

3 Likes

You hit the nail on the head. This is exactly what caused me to ask the question.

I want to replace "magic numbers" with static let constant values in my code. If I use a static let then I have to fully qualify the scope.

So what I end up doing is making global functions (albeit I scope them private to the file) and using those for the magic numbers/values.

For code beauty, I would like to have them scoped inside the class instead of having them outside. The sort-of-never-used-but-possible added value of having them inside the class as static lets is that they could be referenced from another class if needed. Although this like never happens for me.

I thought I read somewhere that global functions were not preferred. But typing out the long names of static scopes just does not give readable code. :(

I was kind of hoping that static variable scope was not yet fully hashed out and maybe we could make a proposal to allow static values to be accessed in an instance without full scoping.

In fact it seems like it would be fine to do as shadowing happens all the time in swift.

There are probably quite a few opinions on this.

You don’t have to make them functions. You can have private let constant = 0 at the top level in a file. That’s perfectly idiomatic Swift.

Thank you.
I thought I read somewhere that global constants were not preferred. I can and do use these.

I find many sources saying that I should instead put the constant in the scope and use private static let constant = 0 because it is the proper scope. I do agree also that it is the proper scope, but it is so verbose and non-swift that I do not use it.

Top level constants are available to all of the code in that file and not limited to just a specific scope.

In practise this is not a big problem and things work fine.

But IMO it would be nice if the scope could be "correct" and the amount of boiler plate was reduced.

1 Like

You can also use Self.theConstant in place of MyVeryLongClassNameThatIHateSeeingEverywhere.theConstant.

Don't feel pressured not to use file scope private let. That's what former Java devs say, because they're not used to it (in my experience).

2 Likes