Can I force the compiler to fail when some protocol functions change their signature?

121 views Asked by At

Is there a way to mark some Swift functions as implementing some protocol functions so that if the protocol's signature changes, the compiler can mark implementations as an error.

For instance, consider this example where I have a default implementation of the Foo protocol for UIViewController instances, and I want to override it for a custom Bar class that is a subclass of UIViewController.

// V1
protocol Foo {
    func number() -> Int
}

extension Foo where Self: UIViewController {
    func number() -> Int {
        return 0
    } 
}

// Overrides the default implementation
extension Bar: Foo {
    func number() -> Int {
        return 1
    }
}

Now, the protocol evolves to:

// V2
protocol Foo {
    func numberV2() -> Int
}

extension Foo where Self: UIViewController {
    func numberV2() -> Int {
        return 0
    } 
}

// I think I override the default implementation but not anymore.
// => error prone, no error from the compiler
extension Bar: Foo {
    func number() -> Int {
        return 1
    }
}

How can I help my Bar extension be aware that the number function is no longer relevant for the Foo protocol?

3

There are 3 answers

0
Ole Begemann On BEST ANSWER

No, there's currently (Swift 4.0) no way to get help from the compiler for catching these "near misses". This topic has come up several times on the swift-evolution mailing list, though, so people are definitely aware that it's a problem that should be solved.

In fact, Doug Gregor recently made a change to the Swift compiler that will able to catch some "near misses". The change is not part of the current Swift release, but will be in Swift 4.1.

From the mailing list post introducing the change:

A “near-miss” warning fires when there is a protocol conformance for which:

1) One of the requirements is satisfied by a “default” definition (e.g., one from a protocol extension), and

2) There is a member of the same nominal type declaration (or extension declaration) that declared the conformance that has the same name as the requirement and isn’t satisfying another requirement.

These are heuristics, of course, and we can tune the heuristics over time.

If I understand this correctly, the current heuristics won't catch your exact issue because they only match methods with the exact same name (but differing types), but as Doug said the heuristics may be improved in the future.

0
GetSwifty On

This is one of the reasons default protocol implementations should be as specific as possible if the functionality will ever be overridden.

As previously mentioned there isn't a direct way to do this, but I'll give a few work arounds.

  • Have 2 protocols, one with the default implementation and one without. e.g. AutoFoo: Foo and Foo. Have the default implementation on AutoFoo, and have Bar only adhere to Foo so as to not be affected by AutoFoo's default implementation.
  • Use a refactoring engine when changing names (the built in Xcode engine should work)
  • Change the name in the protocol, build, then find all the places with errors.
0
Artheyn On

Instead of defining an extension of your protocol that manages objects of type UIViewController, you could instead, provide a UIViewController root class that implements your Foo protocol and uses subclasses for all other UIViewController that needs to implements Foo.

Explaining that with code:

Your Foo protocol :

protocol Foo {
    func number() -> Int
}

Here is how the Root class looks like:

class Root: UIViewController, Foo {
    func number() -> Int {
        return 1
    }
}

And your overrided implementation of the subclass:

class Bar: Root {
    override func number() -> Int {
        return 5
    }
}

Each time you change the method signature of your Foo protocol the compiler will throw an error, against the Root class because it doesn't conforms anymore to the Foo protocol and against the Bar subclass (once you've fixed this error) because it doesn't override any method of Root anymore.

And for classes for which you don't need to override Foo you can just inherit from Root and it will use the default implementation:

class Joe: Root {
}

Full code (Before):

protocol Foo {
    func number() -> Int
}

extension Foo where Self: UIViewController {
    func number() -> Int {
        return 0
    }
}

// Class with an overrided implementation of the number() method.
class Bar: UIViewController, Foo {
  func number() -> Int {
      return 1
  }
}

// Class uses the default implementation of the number() method.
class Joe: UIViewController, Foo {

}

Full code (After):

protocol Foo {
    func number() -> Int
}

class Root: UIViewController, Foo {
    func number() -> Int {
        return 1
    }
}

// Class with an overrided implementation of the number() method.
class Bar: Root {
    override func number() -> Int {
        return 5
    }
}

// Class uses the default implementation of the number() method.
class Joe: Root {
}

Hope it helps.