【问题标题】:Why can't I pass a Protocol.Type to a generic T.Type parameter?为什么我不能将 Protocol.Type 传递给通用 T.Type 参数?
【发布时间】:2018-09-13 05:36:17
【问题描述】:

我正在使用 Swinject,但有一个问题困扰着我。我几乎一整天都被这个问题困住了。我怀疑这是因为 Swift 是一种静态类型语言,但我不完全确定。

我在这个操场上总结了我的问题

protocol Protocol {}

class Class: Protocol {}

let test: Protocol.Type = Class.self

func printType(confromingClassType: Protocol.Type) {
    print(confromingClassType)
}

func printType<Service>(serviceType: Service.Type) {
    print(serviceType)
}

print(Class.self) // "Class"
printType(serviceType: Class.self) // "Class"
print(test) // "Class"
printType(confromingClassType: test) // "Class"

printType(serviceType: test) // "note: expected an argument list of type '(serviceType: Service.Type)'"

我尝试了不同的解决方案,例如 test.self 或 type(of: test),但它们都不起作用。

所以我想我不能调用带有作为变量提供的泛型参数的函数?

【问题讨论】:

  • 你到底想做什么?
  • 您是否试图拥有一个接受 Class 实例作为参数的函数,换句话说,Swift 等效于 Java 的 String myFunc(Class&lt;?&gt; classInstance)
  • 您面临的问题是,使用通用占位符T,当T 是协议PT.TypeP.Protocol(描述协议本身的元类型)和 not P.Type(描述符合 P 的具体类型的元类型)。您在这里要解决的具体问题是什么?
  • 我不确定完全理解您的意思。我的例子应该有效。我可以打电话给printType(confromingClassType: ) 唯一的区别是服务类型的通用性
  • @GuillaumeL。您可以使用test 的参数调用printType(confromingClassType:),因为它有一个Protocol.Type 类型的参数。 printType(serviceType:)no such 参数。如前所述,通用占位符TT.Type 参数不能接受存在元类型(P.Type 用于某些协议P)。如果是这样,您将无法说出printType(serviceType: Protocol.self),因为P.Protocol 不是P.Type。再说一次,您要在这里解决的实际问题是什么?

标签: swift generics protocols


【解决方案1】:

P.TypeP.Protocol

有两种协议元类型。对于某些协议P,以及符合类型C

  • P.Protocol 描述了协议本身的类型(它可以保存的唯一值是 P.self)。
  • P.Type 描述了符合协议的具体类型。它可以保存C.self 的值,但不能 P.self 因为protocols don't conform to themselves(尽管此规则的一个例外是Any,因为Anytop type,所以任何元类型值可以输入为Any.Type;包括Any.self)。

您面临的问题是,对于给定的通用占位符 T,当 T 是某个协议时 PT.Type 不是 P.Type - 它是 @ 987654342@.

如果我们回到你的例子:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
    print(serviceType)
}

let test: P.Type = C.self

// Cannot invoke 'printType' with an argument list of type '(serviceType: P.Type)'
printType(serviceType: test)

我们不能将test 作为参数传递给printType(serviceType:)。为什么?因为testP.Type;并且没有替代 T 使 serviceType: 参数采用 P.Type

如果我们用P 替换T,则参数采用P.Protocol

printType(serviceType: P.self) // fine, P.self is of type P.Protocol, not P.Type

如果我们用 concrete 类型替换T,例如C,则参数采用C.Type

printType(serviceType: C.self) // C.self is of type C.Type

使用协议扩展来破解

好的,所以我们知道如果我们可以用 concrete 类型替换T,我们可以将C.Type 传递给函数。我们可以用P.Type 包装的动态类型替换吗?不幸的是,这需要一个名为 opening existentials 的语言功能,目前用户无法直接使用该功能。

然而,当访问协议类型实例或元类型上的成员时,Swift 确实隐式打开存在(即它挖掘出运行时类型并使其以通用占位符的形式访问)。我们可以在协议扩展中利用这一事实:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
  print("T.self = \(T.self)")
  print("serviceType = \(serviceType)")
}

extension P {
  static func callPrintType/*<Self : P>*/(/*_ self: Self.Type*/) {
    printType(serviceType: self)
  }
}

let test: P.Type = C.self
test.callPrintType()
// T.self = C
// serviceType = C

这里发生了很多事情,所以让我们稍微解开一下:

  • P 上的扩展成员 callPrintType() 具有隐式通用占位符 Self,该占位符被限制为 P。使用此占位符键入隐式 self 参数。

  • 当在 P.Type 上调用 callPrintType() 时,Swift 隐式挖掘出 P.Type 包装的动态类型(这是存在的开头),并使用它来满足 Self 占位符.然后它将这个动态元类型传递给隐式的self 参数。

  • 因此,C 将满足 Self,然后可以将其转发到 printType 的通用占位符 T


为什么T.Type 不是P.TypeT == P

您会注意到上述解决方法的工作原理,因为我们避免用P 替换通用占位符T。但是为什么当用协议类型P 替换T 时,T.Type 不是 P.Type

好吧,考虑一下:

func foo<T>(_: T.Type) {
    let t: T.Type = T.self
    print(t)
}

如果我们用P 替换T 会怎样?如果T.TypeP.Type,那么我们得到的是:

func foo(_: P.Type) {
    // Cannot convert value of type 'P.Protocol' to specified type 'P.Type'
    let p: P.Type = P.self
    print(p)
}

这是非法的;我们不能将P.self 分配给P.Type,因为它的类型是P.Protocol,而不是P.Type

因此,结果是,如果您想要一个函数参数,该函数参数采用描述符合P任何具体类型的元类型(而不仅仅是一个特定的具体符合类型)——您只需要P.Type 参数,而不是泛型。泛型不为异构类型建模,这就是协议类型的用途。

这正是printType(conformingClassType:) 所拥有的:

func printType(conformingClassType: P.Type) {
    print(conformingClassType)
}

printType(conformingClassType: test) // okay

您可以将test 传递给它,因为它有一个P.Type 类型的参数。但是你会注意到这意味着我们不能将P.self 传递给它,因为它不是P.Type 类型:

// Cannot convert value of type 'P.Protocol' to expected argument type 'P.Type'
printType(conformingClassType: P.self) 

【讨论】:

  • 感谢您非常完整的回答,至少可以说并非易事。所以我想我不能做我想做的事,这有点难过。你认为有什么变通办法或应该以不同的方式完成的事情来使这成为可能吗?
  • @GuillaumeL。我确实同意元类型的当前状态远非理想,并且我完全希望它们在该语言的未来版本中得到彻底检查。不幸的是,我对 Swinject 一点也不熟悉,因此无法为您的问题建议当前的解决方法(我不知道 metatype 参数的用途)。可能值得提出一个新问题,重点关注您尝试使用 Swinject 实现的目标(您始终可以链接回该问题以获取上下文);希望熟悉该框架的人可以为您提供解决方法。
  • @GuillaumeL。只是想到了一种使用协议扩展的鲜为人知的技巧的解决方法;可能对你有用。
  • 哇,感谢您的更新。我不得不再次阅读我的问题以记住我在说什么。从那以后我们放弃了这个想法,但也许我们可以用你的解决方法再试一次!
【解决方案2】:

我在操场上运行了你的代码,看来这就是它无法编译的原因

let test: Protocol.Type = Class.self

如果您删除test 的类型声明,代码将起作用并在15 行打印出Class.Type

所以下面的代码编译运行:

protocol Protocol {}

class Class: Protocol {}

let test = Class.self

func printType<Service>(serviceType: Service.Type) {
    print(serviceType)
}

print(Class.Type.self) // "Class.Type"
printType(serviceType: Class.Type.self) // "Class.Type"
print(type(of: test)) // "Class.Type"

printType(serviceType: type(of: test)) // "Class.Type"

我希望这对您的问题有所帮助。


编辑

我在使用原始代码的操场上遇到的错误如下:

Playground execution failed: error: Untitled Page 2.xcplaygroundpage:9:1: error: cannot invoke 'printType' with an argument list of type '(serviceType: Protocol.Type.Type)'
printType(serviceType: type(of: test)) // "Class.Type"

这意味着您调用了Type 2 次,这就是代码无法编译的原因,因为您已经调用了带有Protocol.Type 类型参数的方法。

如果您像这样更改方法的签名:

让测试:Protocol.Type = Class.self

func printType<Service>(serviceType: Service) {
    print(serviceType)
}

一切都会编译并正常工作,打印Class.type

这也是我的第一个版本的答案将编译的原因,因为它将为test 分配正确的类型,只能调用一次.Type

【讨论】:

  • 我很好奇到底是什么阻止了这个let test: Protocol.Type = Class.self
  • 这是一个不错的尝试,但它并没有真正解决问题,因为在应用程序中我们不知道测试的类型,我们唯一知道的是它符合协议。这就是为什么测试必须实现协议以反映真实情况。
  • 我无法更改函数签名,它是来自 Swinject 框架的函数public func register&lt;Service, Arg1&gt;( _ serviceType: Service.Type, name: String? = nil, factory: @escaping (Resolver, Arg1) -&gt; Service) -&gt; ServiceEntry&lt;Service&gt;
猜你喜欢
  • 2018-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-28
  • 1970-01-01
  • 1970-01-01
  • 2022-01-23
相关资源
最近更新 更多