为什么Swift没有用更具体的类型调用我的重载方法?

zou*_*oul 6 json overloading swift

我使用Decodable从JSON解码一个简单的结构.这符合Decodable协议:

extension BackendServerID: Decodable {

    static func decode(_ json: Any) throws -> BackendServerID {
        return try BackendServerID(
            id: json => "id",
            name: json => "name"
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望能够decode用a 来打电话String,所以我添加了一个扩展名:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我想像这样解码对象:

XCTAssertNoThrow(try BackendServerID.decode("{\"id\": \"foo\", \"name\": \"bar\"}"))
Run Code Online (Sandbox Code Playgroud)

但是,这并不像预期的那样工作,因为某种decode(Any)方式调用该方法而不是decode(String).我究竟做错了什么?(当我通过重命名我的自定义方法来澄清调用时decodeString,它可以正常工作.)

Ham*_*ish 6

我同意这种行为是令人惊讶的,你可能想要提出一个错误.

从快速查看CSRanking.cpp的源代码,这是类型检查器实现的一部分,它处理重载决策时不同声明的"排名" - 我们可以看到在执行:

/// \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) {
Run Code Online (Sandbox Code Playgroud)

类型检查器认为具体类型中的重载比协议扩展()中的重载更"专用" :

  // 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;
  }
Run Code Online (Sandbox Code Playgroud)

当执行重载分辨率时,类型检查器将跟踪每个潜在过载的"分数",选择具有最高过载的分数.当给定的重载被认为比另一个更"专业"时,其分数将增加,因此意味着它将受到青睐.还有其他因素会影响超载的分数,但isDeclAsSpecializedAs在这种特殊情况下似乎是决定性因素.

所以,如果我们考虑一个最小的例子,类似于@Sulthan给出的一个:

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")
Run Code Online (Sandbox Code Playgroud)

在调用时BackendServerID.decode("foo"),具体类型BackendServerID中的重载优先于协议扩展中的BackendServerID重载(过载在具体类型的扩展中的事实在这里没有区别).在这种情况下,无论在函数签名本身是否更加专业化,这都是一样的.该位置更重要.

(虽然功能签名确实问题,如果仿制药参与-见下文切线)

值得注意的是,在这种情况下,我们可以通过在调用时强制转换方法来强制Swift使用我们想要的重载:

let str = try (BackendServerID.decode as (String) throws -> BackendServerID)("foo")
Run Code Online (Sandbox Code Playgroud)

现在,这将调用协议扩展中的重载.

如果重载都定义在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")
Run Code Online (Sandbox Code Playgroud)

类型检查器实现中的上述条件将不会被触发,因为它们都不在协议扩展中 - 因此,当涉及到重载解析时,更"专门"的重载将仅基于签名.因此,String将为String参数调用重载.


(关于通用过载的轻微切线......)

值得注意的是,类型检查器中存在(许多)其他规则,以确定一个重载是否比另一个更"专业".其中一个是优选非泛型重载到泛型重载():

  // 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();
  }
Run Code Online (Sandbox Code Playgroud)

此条件的实现高于协议扩展条件 - 因此,如果您要更改decode(_:)协议中的要求,使其使用通用占位符:

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")
Run Code Online (Sandbox Code Playgroud)

String超载现在将调用,而不是普通的一个,尽管在协议扩展之中.


实际上,正如您所看到的,有许多复杂的因素可以决定调用哪个重载.正如其他人已经说过的,在这种情况下,真正最好的解决方案是通过为您的String重载提供参数标签来明确消除重载的歧义:

extension Decodable {
    static func decode(jsonString: String) throws -> Self {
        // ...
    }
}

// ...

let str = try BackendServerID.decode(jsonString: "{\"id\": \"foo\", \"name\": \"bar\"}")
Run Code Online (Sandbox Code Playgroud)

这不仅可以清除重载决策,还可以使API更加清晰.只是decode("someString"),不清楚字符串应该采用什么格式(XML?CSV?).现在很清楚它需要一个JSON字符串.