Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there information somewhere on Swift protocol associatedtype using `=` versus `:`?

I'm trying to find out if there's any documentation on Swift's protocol associatedtype using a = instead of a :.

Eg. associatedtype Thing = SomeOtherThing vs associatedtype Thing: SomeOtherThing

Not to be confused with (what probably confused me) typealias Thing = SomeOtherThing which is always a = I believe.

I was trying to abstract usage of a class that included a delegate and was using = for associatedtypes until I ran into an issue where the variable that was that type wasn't exposing its properties due to the = and instead needed the usual :, which to me made sense, in a way, but then when I changed one particular = to a : it caused everything to break. I've included an example below, where the idea was to be able to get/set the delegate object freely, but more or less one protocol was saying that it's delegate must be the type of the associated type (with the =) rather than just "you must conform to this" as the : seems to suggest.

I also don't know if I've taken this one step too far and there's some other better way to express this in terms of testing. This seems to be required as I can't exactly rely on the external object to work as expected in the tests, and instead need to mock it to fail on purpose and such in some cases.

import Foundation

// Concrete objects (eg external framework)

protocol ManagerDelegate: AnyObject {
    func managerDidSomething(_ manager: Manager)
}

class Manager {
    weak var delegate: ManagerDelegate?
    func doSomething() {
        delegate?.managerDidSomething(self)
    }
}

// Custom objects using concrete objects, set up to be testable

class CustomManagerDelegate: ManagerDelegate {
    func managerDidSomething(_ manager: Manager) {
        print(#function)
    }
}

class CustomClass<ManagerType: SomeManager> {
    
    private(set) var doSomethingCustomCalled = false
    
    private let managerDelegate: ManagerType.DelegateType
    private let manager: ManagerType
    
    init(manager: ManagerType, managerDelegate: ManagerType.DelegateType) {
        self.manager = manager
        self.managerDelegate = managerDelegate
        manager.delegate = managerDelegate
    }
    
    func doSomethingCustom() {
        doSomethingCustomCalled = true
        manager.doSomething()
    }
    
}

// Example creation of custom object

class Example {
    
    static func createCustomObject() {
        let customObject = CustomClass(
            manager: Manager(),
            // if `:` used instead of `=` for `SomeManager.DelegateType`, error is:
            // Cannot convert value of type 'CustomManagerDelegate' to expected argument type 'Manager.DelegateType'
            managerDelegate: CustomManagerDelegate() // error fix: add `as! Manager.DelegateType`
        )
        customObject.doSomethingCustom()
    }
    
}

// Testable interface

protocol SomeManager: AnyObject {
    // This `=` is the only thing keeping it together
    associatedtype DelegateType = SomeManagerDelegate
    // This doesn't work
    //associatedtype DelegateType: SomeManagerDelegate
    var delegate: DelegateType? { get set }
    func doSomething()
}

protocol SomeManagerDelegate {
    associatedtype ManagerType: SomeManager
    func managerDidSomething(_ manager: ManagerType)
}

// Testable interface conformance

// if `:` used instead of `=` for `SomeManager.DelegateType`, error is:
// Type 'Manager' does not conform to protocol 'SomeManager'
extension Manager: SomeManager {
    // already conforms
}

class MockManagerDelegate: SomeManagerDelegate {
    
    typealias ManagerType = MockManager
    
    func managerDidSomething(_ manager: ManagerType) {
        print(#function)
    }
    
}

class MockManager: SomeManager {
    
    weak var delegate: MockManagerDelegate?
    
    func doSomething() {
        delegate?.managerDidSomething(self)
    }
    
}

// Tests

class CustomClassTests {
    
    func testCustomSomethingWasCalled() {
        let mockInjectedCustomClass = CustomClass(
            manager: MockManager(),
            managerDelegate: MockManagerDelegate()
        )
        mockInjectedCustomClass.doSomethingCustom()
        print("Was Called:", mockInjectedCustomClass.doSomethingCustomCalled)
        assert(mockInjectedCustomClass.doSomethingCustomCalled)
    }
    
}

CustomClassTests().testCustomSomethingWasCalled()

/* console:
 managerDidSomething(_:)
 Was Called: true
 */
like image 670
Ohifriend Avatar asked Jun 28 '21 01:06

Ohifriend


People also ask

What is Associatedtype in protocol Swift?

An associated type gives a placeholder name to a type that's used as part of the protocol. The actual type to use for that associated type isn't specified until the protocol is adopted. Associated types are specified with the associatedtype keyword.

What is associated type in protocol?

What is an associated type? An associated type can be seen as a replacement of a specific type within a protocol definition. In other words: it's a placeholder name of a type to use until the protocol is adopted and the exact type is specified.


1 Answers

The = and the : are two independent parts of an associated type declaration, rather than being mutually exclusive. This is the full syntax of protocol associated type declaration:

attributes(opt) 
access-level-modifier(opt) 
'associatedtype' 
typealias-name 
type-inheritance-clause(opt) 
typealias-assignment(opt) 
generic-where-clause(opt)

The : TypeName part is the type-inheritance-clause, and the = TypeName is the typealias-assignment.

: TypeName constrains what type the associated type can be, namely that it must inherit/conform to TypeName. This is why : SomeManagerDelegate didn't work in your case. You are saying that SomeManager.DelegateType must be some kind of SomeManagerDelegate, but for Manager, this is not true - Manager.delegate is of type ManagerDelegate, which is a totally unrelated protocol. Even if it were SomeManagerDelegate, it wouldn't work either because protocols don't conform to themselves.

= TypeName sets a default type for the associated type. If the compiler cannot infer what the type the associated type for a conformance should be and you didn't say it explicitly it either, it will use that type instead. But in your case, this fact didn't really matter. What actually caused your code to work, wasn't the addition of = SomeManagerDelegate, but the removal of the constraint : SomeManagerDelegate. You are no longer constraining what type the associated type should be (it can be anything!), so for Manager, the associated type can now be inferred to be ManagerDelegate. Note that you don't have to explicitly say:

typealias DelegateType = ManagerDelegate

In fact, you can totally remove = SomeManagerDelegate and just say:

associatedtype DelegateType

so it is far from the truth that the = is "the only thing keeping it together".

This = TypeName syntax doesn't seem very well documented. Here's a related Swift forums post.

like image 56
Sweeper Avatar answered Nov 15 '22 03:11

Sweeper