I would agree that this behavior is amazing, and you might want to create an error over it .
From a quick look at the source CSRanking.cpp , which is part of a type-checking implementation that deals with “ranking” for various declarations, when it comes to overload resolution, we can see that when implemented:
/// \brief Determine whether the first declaration is as "specialized" as /// the second declaration. /// /// "Specialized" is essentially a form of subtyping, defined below. static bool isDeclAsSpecializedAs(TypeChecker &tc, DeclContext *dc, ValueDecl *decl1, ValueDecl *decl2) {
Type checking considers overloading in a particular type more "specialized" than overloading in protocol extension ( source ):
// Members of protocol extensions have special overloading rules. ProtocolDecl *inProtocolExtension1 = outerDC1 ->getAsProtocolExtensionContext(); ProtocolDecl *inProtocolExtension2 = outerDC2 ->getAsProtocolExtensionContext(); if (inProtocolExtension1 && inProtocolExtension2) { // Both members are in protocol extensions. // Determine whether the 'Self' type from the first protocol extension // satisfies all of the requirements of the second protocol extension. bool better1 = isProtocolExtensionAsSpecializedAs(tc, outerDC1, outerDC2); bool better2 = isProtocolExtensionAsSpecializedAs(tc, outerDC2, outerDC1); if (better1 != better2) { return better1; } } else if (inProtocolExtension1 || inProtocolExtension2) { // One member is in a protocol extension, the other is in a concrete type. // Prefer the member in the concrete type. return inProtocolExtension2; }
And when performing congestion resolution, the type controller will track the “score” for each potential congestion, choosing the one that matters the most. When a given congestion is considered more “specialized” than another, its rating will increase, so this means that it will be preferable. There are other factors that may affect the assessment of congestion, but isDeclAsSpecializedAs seems to be a decisive factor in this particular case.
So, if we look at a minimal example similar to the one @Sulthan gives :
protocol Decodable { static func decode(_ json: Any) throws -> Self } struct BackendServerID {} extension Decodable { static func decode(_ string: String) throws -> Self { return try decode(string as Any) } } extension BackendServerID : Decodable { static func decode(_ json: Any) throws -> BackendServerID { return BackendServerID() } } let str = try BackendServerID.decode("foo")
When calling BackendServerID.decode("foo") overloading in a specific type of BackendServerID preferable to overloading in a protocol extension (the fact that overloading a BackendServerID in an extension of a specific type does not matter Here). In this case, this is regardless of whether it is more specialized when it comes to the function itself. The place is bigger.
(Although the signature of the function matters if generics are involved - see tangent below)
It is worth noting that in this case we can force Swift to use the required overload by discarding the method when calling:
let str = try (BackendServerID.decode as (String) throws -> BackendServerID)("foo")
Now this will cause an overload in the protocol extension.
If overloads were defined in BackendServerID :
extension BackendServerID : Decodable { static func decode(_ json: Any) throws -> BackendServerID { return BackendServerID() } static func decode(_ string: String) throws -> BackendServerID { return try decode(string as Any) } } let str = try BackendServerID.decode("foo")
The above condition in the implementation of type checking will not be triggered, since there is no protocol extension, therefore, when it comes to resolving congestion, the more “specialized” congestion will be based solely on signatures. Therefore, String overloading is called for the String argument.
(Smooth tangent relative to general overloads ...)
It is worth noting that there are (many) other rules in type control if one overload is considered more "specialized" than another. One of them prefers non-core overloads for general overloads ( source ):
// A non-generic declaration is more specialized than a generic declaration. if (auto func1 = dyn_cast<AbstractFunctionDecl>(decl1)) { auto func2 = cast<AbstractFunctionDecl>(decl2); if (func1->isGeneric() != func2->isGeneric()) return func2->isGeneric(); }
This condition is implemented higher than the protocol extension condition - therefore, if you must change the decode(_:) requirement in the protocol so that it uses a common placeholder:
protocol Decodable { static func decode<T>(_ json: T) throws -> Self } struct BackendServerID {} extension Decodable { static func decode(_ string: String) throws -> Self { return try decode(string as Any) } } extension BackendServerID : Decodable { static func decode<T>(_ json: T) throws -> BackendServerID { return BackendServerID() } } let str = try BackendServerID.decode("foo")
String overloading will now be called instead of the general one, despite the fact that it is in the protocol extension.
So, as you can see, there are many complex factors that determine which overload needs to be caused. In fact, the best solution in this case, as others have already said, is to explicitly eliminate overloads by overloading the String label of the argument:
extension Decodable { static func decode(jsonString: String) throws -> Self {
This not only eliminates overload resolution, but also makes the API more understandable. Only with decode("someString") is it unclear what format the string should be in (XML? CSV?). Now it’s clear that he is expecting a JSON string.