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

后端 未结 5 436
有刺的猬
有刺的猬 2021-01-02 15:38

I use Decodable to decode a simple struct from JSON. This works by conforming to a Decodable protocol:

extension BackendServerID: Decodable {

          


        
5条回答
  •  再見小時候
    2021-01-02 16:07

    I would agree that this behaviour is surprising, and you may well want to file a bug over it.

    From quickly looking through the source of CSRanking.cpp, which is the part of type checker implementation that deals with the "rankings" for different declarations when it comes to overload resolution – we can see that in the implementation of:

    /// \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) {
    

    The type checker considers an overload in a concrete type to be more "specialised" than an overload in a 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 overload resolution, the type checker will keep track of a "score" for each potential overload, picking the one with the highest. When a given overload is considered more "specialised" than another, its score will be incremented, therefore meaning that it will be favoured. There are other factors that can affect an overload's score, but isDeclAsSpecializedAs appears to be the deciding factor in this particular case.

    So, if we consider 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"), the overload in the concrete type BackendServerID is preferred to the overload in the protocol extension (the fact that the BackendServerID overload is in an extension of the concrete type doesn't make a difference here). In this case, this is regardless of whether one is more specialised when it comes to the function signature itself. The location matters more.

    (Although the function signature does matter if generics are involved – see tangent below)

    It's worth noting that in this case we can force Swift to use the overload we want by casting the method at the call:

    let str = try (BackendServerID.decode as (String) throws -> BackendServerID)("foo")
    

    This will now call the overload in the protocol extension.

    If the overloads were both 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 the above condition in the type checker implementation won't be triggered, as neither are in a protocol extension – therefore when it comes to overload resolution, the more "specialised" overload will be solely based on signatures. Therefore the String overload will be called for a String argument.


    (Slight tangent regarding generic overloads...)

    It's worth noting that there are (lots of) other rules in the type checker for whether one overload is considered more "specialised" than another. One of these is preferring non-generic overloads to generic overloads (source):

      // A non-generic declaration is more specialized than a generic declaration.
      if (auto func1 = dyn_cast(decl1)) {
        auto func2 = cast(decl2);
        if (func1->isGeneric() != func2->isGeneric())
          return func2->isGeneric();
      }
    

    This condition is implemented higher up than the protocol extension condition – therefore if you were to change the decode(_:) requirement in the protocol such that it used a generic placeholder:

    protocol Decodable {
        static func decode(_ 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(_ json: T) throws -> BackendServerID {
            return BackendServerID()
        }
    }
    
    let str = try BackendServerID.decode("foo")
    

    The String overload will now be called instead of the generic one, despite being in a protocol extension.


    So really, as you can see, there are lots of complicated factors that determine which overload to call. Really the best solution in this case, as others have already said, is to explicitly disambiguate the overloads by giving your String overload an argument label:

    extension Decodable {
        static func decode(jsonString: String) throws -> Self {
            // ...
        }
    }
    
    // ...
    
    let str = try BackendServerID.decode(jsonString: "{\"id\": \"foo\", \"name\": \"bar\"}")
    

    Not only does this clear up the overload resolution, it also makes the API clearer. With just decode("someString"), it wasn't clear exactly what format the string should be in (XML? CSV?). Now it's perfectly clear that it expects a JSON string.

提交回复
热议问题