Gregory Higley
Gregory Higley

Reputation: 16558

Why is the subclass type not available when an instance property is initialized by a static member?

When the following code is run, the self inside of defaultModuleName is ReactViewController when one would expect it to be FooViewController. Why?

class ReactViewController: UIViewController {

    var moduleName: String = defaultModuleName

    static var defaultModuleName: String {
        let t = String(reflecting: self) // Also tried NSStringFromClass
        guard let s = t.split(separator: ".").last else { return "" }
        guard let r = s.range(of: "ViewController") else { return "" }
        return String(s.prefix(upTo: r.lowerBound))
    }

}

class FooViewController: ReactViewController {

   override func viewDidLoad() {
       super.viewDidLoad();
       print(moduleName); // Prints "React"
   }

}

Upvotes: 1

Views: 81

Answers (2)

Hamish
Hamish

Reputation: 80781

This is pretty interesting; it appears that the self available in a property initialiser is merely the type that the property is defined in, rather than the dynamic type of the instance being constructed.

A more minimal example would be:

class C {
  static var foo: String { return "\(self)" }
  let bar = foo // the implicit 'self' in the call to 'foo' is always C.
}

class D : C {}

print(D().bar) // C

In the property initialiser for bar, the implicit self is C.self, not D.self; despite the fact that we're constructing a D instance. So that's what the call to foo sees as self.

This also prevents class member overrides from being called from property initialisers:

class C {
  class var foo: String { return "C" }
  let bar = foo 
}

class D : C {
  override class var foo: String { return "D" }
}

print(D().bar) // C

Therefore I regard this as a bug, and have filed a report here.

Until fixed, a simple solution is to use a lazy property instead, as now self is the actual instance (upon the property being accessed for the first time), which we get can get the dynamic type of with type(of: self).

For example:

class C {
  static var foo: String { return "\(self)" }
  // private(set) as the property was a 'let' in the previous example.
  lazy private(set) var bar = type(of: self).foo
}

class D : C {}

print(D().bar) // D

Applied to your example:

class ReactViewController : UIViewController {

  lazy var moduleName = type(of: self).defaultModuleName

  static var defaultModuleName: String {
    let t = String(reflecting: self) // Also tried NSStringFromClass
    guard let s = t.split(separator: ".").last else { return "" }
    guard let r = s.range(of: "ViewController") else { return "" }
    return String(s.prefix(upTo: r.lowerBound))
  }
}

class FooViewController : ReactViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    print(moduleName) // Prints "Foo"
  }
}

Upvotes: 2

Connor Neville
Connor Neville

Reputation: 7351

You just need to pass self instead of type(of: self), and use the String(describing:) initializer.

class ClassA {

    static var className: String {
        return String(describing: self)
    }

}

class ClassB: ClassA { }

print(ClassB.className) // prints "ClassB"

EDIT: clarification on the var moduleName: String = defaultModuleName update. Suppose I add this line to the above example (same idea):

class ClassA {

    // This is a property of ClassA -> it gets implicitly initialized
    // when ClassA does -> it uses ClassA.className for its value
    var instanceClassName = className

    static var className: String {
        return String(describing: self)
    }

}

class ClassB: ClassA { }

print(ClassB().instanceClassName) // prints "ClassA"

This new instanceClassName is not static, so it is an instance property on ClassA. It is therefore initialized when ClassA is initialized (not when ClassB is initialized). Ergo, a property being set within ClassA, using a reference to className, will print out ClassA.

Upvotes: 0

Related Questions