ctietze
ctietze

Reputation: 2932

Satisfy Swift Protocol Methods with Closure Attributes?

I have a protocol with a method. I have thought that methods can be replaced with closures by the same name, but it doesn't seem to work:

protocol Foo {
    func bar() // Type:  Void -> Void
}

class X: Foo {
    func bar() { }
}

class Y: Foo { // Compiler: doesn't conform to protocol Foo
    let bar: Void->Void = {}
}

Is there a way to make this work? I want to override the methods behavior for a Test Stub implementation. Currently, I'd have to do this, which I'd like to shorten:

class Z: Foo {
    var barClosure: Void -> Void = {}

    func bar() {
        barClosure()
    }
}

let baz = Z()
baz.barClosure = { /* ... */ }
baz.bar() // Calls the closure replacement

Upvotes: 2

Views: 3615

Answers (3)

Airspeed Velocity
Airspeed Velocity

Reputation: 40963

The func keyword does a little bit more magic behind the scenes that you can’t replicate with properties – especially in the case of classes, where functions can be overridden, so vtables need to be built etc.

That said, if you were going to replace methods using closure expressions, you’d need to do more than the code you gave. The equivalent of this:

struct A {
    let x: Int

    func f() {
        println("In f(), x is \(a.x)")
    }
}

would be more like this:

struct A {
    let x: Int

    // returns a function that takes A objects, and 
    // returns a function that captures them
    static let f: (A)->()->() = { a in 
      { ()->() in println("In f(), x is \(a.x)") } 
    }

    // equivalent of the dot notation call of f
    var f: ()->() {
        return A.f(self)
    }
}

This replicates how struct methods actually work, and allows you to do all the same things an f method does:

let a = A(x: 5)

// static version of f
let A_f = A.f

// that static version bound to a self:
let f = A_f(a)
f()

// the above is equivalent to:
a.f()

But this still isn’t enough for A to conform to a protocol that requires an f() method.

Upvotes: 2

Dániel Nagy
Dániel Nagy

Reputation: 12035

You declared the protocol to have a function, bar(), but in class Y, you just have a constant instead of a function, this is the problem. But if you want to have something like in class Y, you should change the protocol to:

protocol Foo {
    var bar: () -> () {get set}
}

And implement like that:

class Test: Foo {
    private var _bar: (() -> ())?

    var bar: () -> () {
        get {
            return {}
        }
        set {
            self._bar = newValue
        }
    }
}

UPDATED

If you to shorten your class, you can use something like that:

protocol Foo {
    var barClosure: Void -> Void {get set}
}

class Z: Foo {
    var barClosure: Void -> Void = {
        //do here something
    }
}

let a = Z()
a.barClosure()

Upvotes: 2

ctietze
ctietze

Reputation: 2932

Thanks to @Dániel Nagy, I was able to figure out what options I have. The protocol should require a closure. This way, the client code won't change, as closure calls are identical to method calls.

  • make the property mutable so implementations can decide if they want to lock the value
  • require a getter only (for the same reason)
  • initialize the property as immutable (let) in production code
  • initialize the property as mutable (var) in test code to provide alternate implementations in test cases, like mock observers do

Here's a modified example which works well in a Playground by returning strings:

protocol Foo {
    var bar: () -> String { get }
}

class X: Foo {
    // cannot be overwritten
    let bar: () -> String = { return "default x" }
}

class Y: Foo {
    private let _bar: () -> String = { return "default y" }

    // Can be overwritten but doesn't have any effect
    var bar: () -> String {
        get {
            return _bar
        }
        set {
        }
    }
}

class Z: Foo {
    // Can be overwidden
    var bar: () -> String = {
        return "default z"
    }
}

let bax = X()
bax.bar() // => "default x"
// bax.bar = { /* ... */ } // Forbidden

let bay = Y()
bay.bar() // => "default y"
bay.bar = { return "YY" }
bay.bar() // => "default y"

let baz = Z()
baz.bar() // => "default z"
baz.bar = { return "ZZ" }
baz.bar() // => "ZZ"

Upvotes: 2

Related Questions