Why doesn't Swift call my overloaded method with a more specific type?

I am using Decodable to decode a simple structure from JSON. This works according to the Decodable protocol:

 extension BackendServerID: Decodable { static func decode(_ json: Any) throws -> BackendServerID { return try BackendServerID( id: json => "id", name: json => "name" ) } } 

Id like to be able to call decode using String , however, so I added the extension:

 extension Decodable { static func decode(_ string: String) throws -> Self { let jsonData = string.data(using: .utf8)! let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) return try decode(jsonObject) } } 

Then I would like to decode the objects as follows:

 XCTAssertNoThrow(try BackendServerID.decode("{\"id\": \"foo\", \"name\": \"bar\"}")) 

This does not work as expected, since the decode(Any) method is called instead of decode(String) . What am I doing wrong? (When I clarify the call by renaming my custom method to decodeString , it works correctly.)

+5
source share
5 answers

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 { // ... } } // ... let str = try BackendServerID.decode(jsonString: "{\"id\": \"foo\", \"name\": \"bar\"}") 

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.

+5
source

Consider a minimal example:

 protocol Decodable { static func decode(_ json: Any) throws -> Self } struct BackendServerID { } extension Decodable { static func decode(_ string: String) throws -> Self { return try decode(string) } } extension BackendServerID : Decodable { static func decode(_ json: Any) throws -> BackendServerID { return BackendServerID() } } 

The decode implementation in BackendServerId replaces the default implementation of Decodable.decode (parameters are covariant, a similar case as overriding). Your use case will only work if both functions were declared at the same level, for example:

 extension BackendServerID : Decodable { static func decode(_ json: Any) throws -> BackendServerID { return BackendServerID() } static func decode(_ string: String) throws -> Self { return try decode(string as Any) } } 

Also pay attention to as Any , which is necessary to prevent recursion.

To prevent confusion, you should name functions that take string and Any differently, like decode(string:) and decode(json:) .

+1
source

I think you should override decode(Any) , or you can do something like this

 extension Decodable { static func decode(String string: String) throws -> Self { let jsonData = string.data(using: .utf8)! let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) return try decode(jsonObject) } } 

Here you define a new decode(String string: String) method decode(String string: String) , so the decode(Any) method will not be called.

0
source

Swift should name the most specific implementation, you can try it on the playground to confirm; therefore your expectations are true.

In your case, I suspect that the problem is access control levels.

In this Decodable library Decodable the func decode(_ json: Any) method is declared as public , so it is available in your test code.

On the other hand, your own func decode(_ string: String) method does not look like public , and then, by default, is internal and is not available in your test code.

To fix this, either import your application framework using @testable (which makes all internal characters available) or declare a public method.

0
source

It seems you are adding your functions to two different “things”, the first function is added to the BackendServerID and returns the BackendServerID , the second function is added to the Decodable protocol and returns Decodable . The following commands will work on the playground:

 protocol Decodable { static func decode(_ json: Any) } extension Decodable { static func decode(_ json: String) { print("Hi, I am String-Json: ", json) } static func decode(_ json: Int8) { print("Hi, I am Int8-Json: ", json) } static func decode(_ json: Any) { print("Hi, I am Any-Json: ", "(I do not know how to print whatever you gave me)") } } extension Decodable { static func decode(_ json: Int) { print("Hi, I am Int-Json: ", json) } } class JSONParser : Decodable { } let five : Int8 = 5 JSONParser.decode(Int(five)) JSONParser.decode(five) JSONParser.decode("five") JSONParser.decode(5.0) 

He will print

 Hi, I am Int-Json: 5 Hi, I am Int8-Json: 5 Hi, I am String-Json: five Hi, I am Any-Json: (I do not know how to print whatever you gave me) 

and I think it should be what you expect.

However, your two static functions do not have exactly the same signatures, and even if they are not considered "overloaded" by the same function. To paraphrase @Sulthan a bit, I tried

 protocol Decodable { static func decode(_ json: Any) throws -> Self } struct BackendServerID { } extension Decodable { static func decode(_ string: String) throws -> BackendServerID { print("decoding as String: ", string) return BackendServerID() } } extension BackendServerID : Decodable { static func decode(_ json: Any) throws -> BackendServerID { print("decoding as Any: ", "(no idea what I can do with this)") return BackendServerID() } } try BackendServerID.decode("hello") 

and i got

 decoding as Any: (no idea what I can do with this) 

(as you would probably expect by now). The Decodable function Decodable "obscured" and the static functions are not available by protocol type, but if I rename it as

 extension Decodable { static func decodeS(_ string: String) throws -> BackendServerID { print("decoding as String: ", string) return BackendServerID() } } 

I can do

 try BackendServerID.decode("hello") try BackendServerID.decodeS("hello") 

and get the expected result

 decoding as Any: (no idea what I can do with this) decoding as String: hello 

On the other hand, you can do

 extension BackendServerID : Decodable { static func decode(_ json: Any) throws -> BackendServerID { print("decoding as Any: ", "(no idea what I can do with this)") return BackendServerID() } } extension BackendServerID { static func decode(_ string: String) throws -> BackendServerID { print("decoding as String: ", string) return BackendServerID() } } try BackendServerID.decode("hello") try BackendServerID.decode(5) 

and get

 decoding as String: hello decoding as Any: (no idea what I can do with this) 

with an overloaded function (but it will not accept another : Decodable on the second extension ). However, extensions specific types and protocols do not mix, which is very good (TM).

Btw: I tried to persuade him, but for now

 extension Decodable { static func decode(_ string: String) throws -> Self { print("decoding as String: ", string) return try BackendServerID.decode(string as Any) as! Self } } try BackendServerID.decode("hello") try BackendServerID.decode(5) 

will only compile it returned

 decoding as Any: (no idea what I can do with this) decoding as Any: (no idea what I can do with this) 

as the String version on Decodable remained buried. But in any case, it's nice to see how flexible Swift can include parameter types.

However, you are likely to be disappointed.

 let five : Any = "five" try BackendServerID.decode(five) 

Print

 decoding as Any: (no idea what I can do with this) 

therefore, all your dispatching occurs statically. If you are given Any , there seems to be no way to avoid switch ing to define a dynamic type.

0
source

All Articles