【问题标题】:Why doesn’t Swift call my overloaded method with a more specific type?为什么 Swift 不使用更具体的类型调用我的重载方法?
【发布时间】:2017-05-05 09:31:06
【问题描述】:

我使用Decodable 从 JSON 解码一个简单的结构。这通过遵守Decodable 协议来工作:

extension BackendServerID: Decodable {

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

不过,我希望能够使用String 拨打decode,所以我添加了一个分机:

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)
    }
}

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

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

但这并没有按预期工作,因为不知何故调用了 decode(Any) 方法而不是 decode(String)。我究竟做错了什么? (当我通过将自定义方法重命名为 decodeString 来澄清调用时,它可以正常工作。)

【问题讨论】:

  • 如何将decode(_ string: String) 重命名为decode(string: String) 并显式调用BackendServerID.decode(string: "...")
  • 谢谢!这会行得通,但我还是希望类型系统为我找出参数类型。
  • 考虑使用Swift3 naming conventions在每个弱类型参数前加上一个描述其作用的名词,例如:static func decode(string: String)
  • 我不想涉及命名参数(老实说!),但在这种情况下,我认为参数名称没有太多需要澄清的地方——显然是要解码的东西。有关类似情况,请参阅UserDefaults。那里的set 方法都没有在第一个参数名称前加上它的类型。
  • 您可能误读了命名约定?再次阅读其示例并将其与 UserDefaults 进行比较。 UserDefaults 尊重它及其所有set 方法,与约定示例几乎相同。 UserDefault 的set 方法不需要精确第一个参数标签,因为Bool 无法解析为String。在你的情况下,String 可以解析为Any,那么如果你给他一个String 作为第一个参数,编译器很难知道他是否应该调用decode(Any)decode(String)

标签: json swift overloading


【解决方案1】:

我同意这种行为令人惊讶,您可能想file a bug over it

通过快速查看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) {

类型检查器认为具体类型中的重载比协议扩展中的重载更“专门” (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;
  }

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

所以,如果我们考虑一个最小的例子,类似于@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")

当调用BackendServerID.decode("foo") 时,具体类型BackendServerID 中的重载首选协议扩展中的重载(事实上BackendServerID 重载在具体的扩展中类型在这里没有区别)。在这种情况下,这与函数签名本身是否更专业无关。 位置更重要。

(尽管如果涉及泛型,函数签名确实很重要——见下面的切线)

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

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

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

如果重载都在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")

类型检查器实现中的上述条件不会被触发,因为在协议扩展中也不会触发 - 因此当涉及重载解决方案时,更“专门”的重载将完全基于签名。因此,String 重载将针对 String 参数调用。


(与泛型重载略有不同...)

值得注意的是,类型检查器中还有(很多)其他规则,用于判断一个重载是否比另一个重载更“专业化”。其中之一是更喜欢非泛型重载而不是泛​​型重载 (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();
  }

此条件的实现比协议扩展条件更高 - 因此,如果您要更改协议中的 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")

String 重载现在将被调用而不是通用重载,尽管它在协议扩展中。


确实,如您所见,有 很多 复杂因素决定了调用哪个重载。正如其他人已经说过的那样,在这种情况下,真正的最佳解决方案是通过给您的 String 重载一个参数标签来明确消除重载的歧义:

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

// ...

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

这不仅清除了重载决议,还使 API 更清晰。仅使用decode("someString"),尚不清楚字符串应该采用什么格式(XML?CSV?)。现在很清楚它需要一个 JSON 字符串。

【讨论】:

  • 我很高兴我或多或少是正确的。关于BackendServerID.decode 演员的非常有趣的想法。我找不到允许我直接调用扩展方法的语法。
【解决方案2】:

让我们考虑最小的例子:

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()
    }
}

BackendServerIddecode 的实现替换了Decodable.decode 的默认实现(参数是协变的,类似于覆盖的情况)。仅当两个函数都在同一级别上声明时,您的用例才有效,例如:

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)
    }
}

还要注意防止递归所必需的as Any

为防止混淆,您应该以不同的方式命名接受stringAny 的函数,例如decode(string:)decode(json:)

【讨论】:

  • 我明白了!谢谢!如何验证我的版本是否替换了默认实现?
  • @zoul 我不认为你可以。这就是 static 函数的问题,与 class 函数相反。 class 方法需要显式的 overridestatic 函数只是隐藏了它们的祖先。
  • 它不会“替换”默认实现——static func decode(_ string: String) throws -&gt; Self 首先不是要求static func decode(_ json: Any) throws -&gt; Self 的实现。要求说该方法可以接受 anything 作为参数,但实现只说字符串。据我所知,编译器应该将它们视为两个单独的重载——因此我认为这只是一个带有重载解析的奇怪极端情况。
  • @Hamish 恰恰相反。要求说String,实现说Any
  • 协议声明中列出的要求为Any@Sulthan。
【解决方案3】:

我认为你应该覆盖 decode(Any) 或者你可以做类似的事情

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)
    }
}

在这里你定义新方法decode(String string: String) 所以decode(Any) 方法不会被调用。

【讨论】:

    【解决方案4】:

    Swift应该调用最具体的实现,你可以在操场上尝试确认;所以你的期望是正确的。

    在您的情况下,我怀疑问题在于访问控制级别。

    在这个Decodable 库中,方法func decode(_ json: Any) 被声明为public,因此它可以在您的测试代码中使用。

    另一方面,您自己的方法func decode(_ string: String) 似乎不是public,默认情况下是internal,并且无法在您的测试代码中访问。

    要解决这个问题,要么使用@testable(这使得所有内部符号都可用)导入应用程序的框架,要么声明方法public

    【讨论】:

      【解决方案5】:

      您似乎将函数添加到两个不同的“事物”,第一个函数添加到 BackendServerID 并返回 BackendServerID,第二个函数添加到 Decodable 协议并返回 Decodable .以下内容适用于 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)
      

      它会打印出来

      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)
      

      我认为这应该是你所期望的。

      但是,您的两个静态函数没有完全相同的签名,即使它们有,它们也不会被视为“重载”相同的函数。稍微解释一下@Sulthan 我试过了

      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")
      

      我得到了

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

      (您现在可能已经预料到了)。 Decodable 函数被“隐藏”了,静态函数不能通过它们的协议类型访问,但是如果我将它重命名为

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

      我可以的

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

      得到预期的结果

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

      另一方面,你可以这样做

      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)
      

      得到

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

      具有重载函数(但它不会在第二个 extension 上接受另一个 : Decodable)。然而,extensions 的具体类型和协议不会混合,这很可能是一件好事 (TM)。

      顺便说一句:我试图哄它,但同时

      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)
      

      只会编译返回

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

      因为Decodable 上的String 版本仍然被埋没。但无论如何,很高兴看到 Swift 可以灵活地切换参数类型。

      但是你可能会失望

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

      打印

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

      所以您的整个调度都是以静态方式进行的。如果给你一个Any,似乎没有办法避免switch在它上面确定动态类型。

      【讨论】:

        猜你喜欢
        • 2011-08-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-04
        相关资源
        最近更新 更多