Is this the right way subclasses should handle a base class conforming to a protocol initializer?

Let's start with:

/// A type where an instance may be an independent copy of another instance,
/// with various levels of copying for sub-objects.
protocol Cloner {
    /// Creates a copy of the given instance, where sub-objects are themselves
    /// cloned to the given depth.
    init?(copying original: Self, toDepth depth: Int)
}

And I make a sample class that conforms to Cloner:

class Sample1: Cloner {
    init() {}
    required convenience init?(copying original: Sample1, toDepth depth: Int) {
        self.init()
    }
}

Where I can choose whether the initializer copied from Cloner is convenience or not.

The oddity is when I make a derived class:

class Sample2: Sample1 {
    var value: Int
    init(value: Int) {
        self.value = value
        super.init()
    }
    required convenience init?(copying original: Sample1, toDepth depth: Int) {
        guard let original2 = original as? Sample2 else { return nil }
        self.init(value: original2.value)
    }
}

Note that the first parameter is of the Sample1 type, not Sample2! In fact, if I make one that matches Sample2 for the sake of Cloner, the compiler insists on making the Sample1 variant above.

I was really stumped. Until I made a separate derived class Sample3 from Sample1, then made another one, Sample4, that derived from Sample3 instead. Neither Sample3 nor Sample4 had any members; so they inherited the ones from Sample1. When I created some sample objects in the playground, I was surprised as the suggestions.

For Sample1, I got suggestions for the empty and copying initializers. For Sample2, I got the single-Int initializer, the copying initializer with Sample1, and then an additional copying initializer with Sample2 instead. Analogous results happened when creating Sample3 and Sample4 objects. (With Sample4, only the Sample1 and Sample4 versions of the copying initializer appeared.). So the rule seems to be that for class types, the first class to incorporate a protocol with initializer requirements involving Self gets the parameter type named after itself. Derived classes must still use the first base class in the type signature. However, the function suggestions will create two versions of the initializer, one using the base class for the parameter and one with the current class for the parameter.

Am I guessing right? Why was it done this way? Why can't I fill in Self with the actual current class and have it count? My example uses a fail-able initializer, so I have a policy in place if the Sample1 parameter gets an argument of a more derived type. But what are derived classes supposed to do when the initializer is non-fail-able and but the base-class parameter is called with a different class object? (For instance, what if my initializer was non-fail-able and I created a Sample2 object, but filled in the Sample1 parameter with a Sample4 argument?)

Note that Sample2 is still a subclass of Sample1, so you can still do:

let meta: Sample1.Type = Sample2.self
let original = Sample1()
meta.init(copying: original, toDepth: 1) // nil

And meta.init needs to have original be Sample1, not Sample2 or Self.

1 Like

Note that this problem comes up because there are two Sample1 "input" types in play now: the implicit self for the class, and the original argument. You can't get rid of self, but you can get rid of the argument by making this an instance method instead:

protocol Clonable {
  func copy(toDepth depth: Int) -> Self?
}
1 Like

I tried playing around with this, but I discovered:

  1. You can't make non-initializers required. So you can't enforce the derived classes of a conforming class to themselves define a custom copy(toDepth:). Now, my tryout did specify that an initializer call for the return value requires said initializer to be required, but I got around that by forcing the issue with "as!" (or "as?").
  2. My original version of the cloning protocol had a derived protocol changing the "?" to a "!". When I refine your version with a protocol that returns "Self!", it doesn't work right. Conforming to the protocol requires the class to define both versions (copy(toDepth:) -> Self? and copy(toDepth:) -> Self!), but then the compiler complains about invalid re-declarations (since the methods only differ in the "?" vs. "!" status of the return value).

Hmm, technically I don't need the version that returns "!"; I could just make it a marker protocol without any new members.

1 Like

The ! / ? mismatch sounds like a bug, can you file that separately?

To show exactly what happened, here's my sample protocols:

protocol Clonable {
    func copy(toDepth depth: Int) -> Self?
}
protocol StronglyClonable: Clonable {
    func copy(toDepth depth: Int) -> Self!
}

and my sample classes start as:

class SampleX { init() {} }

When I add Clonable conformance to one and StronglyClonable to the other, I get the following stubs:

class Sample1: Clonable {
    func copy(toDepth depth: Int) -> Self? {
        return Sample1() as? Self
    }
    init() {}
}

class Sample2: StronglyClonable {
    func copy(toDepth depth: Int) -> Self! {
        return (Sample2() as! Self)
    }
    func copy(toDepth depth: Int) -> Self? {
        return Sample2() as? Self
    }
    init() {}
}

(I filled in the internals to the methods.). The head to the second method of Sample2 is marked with:

Invalid redeclaration of 'copy(toDepth:)'

Created SR-13496 ("Derived protocol that updates an override with a different Optional variant isn't properly handled"). The way the website wants us to input code is weird (It's not Markdown); I have never been able to figure it out so someone needs to make to code sample formatting work.

I reduced that to:

struct S {
  func f(a: Int) -> String? { return nil }
  func f(a: Int) -> String! { return "" }
}

I get the same error.

I think since SE-0054 this is expected behavior. I should probably be possible for foo() -> T! to act as a witness for foo() -> T?, though...

1 Like

Yeah, the two methods probably shouldn't be simultaneously allowed. But for protocols, a init? or init! requirement can be actually implemented by the other (or by a non-fail-able init). The problem in this sub-thread is that swap isn't allowed for non-initializers; the derived protocol's change to "!" created a new requirement instead of overlaying the base protocol's requirement.

1 Like
Terms of Service

Privacy Policy

Cookie Policy