有什么语法可以使这项工作吗?我需要一个属性可以在编译时确定其类型。
protocol P {}
struct A: P {
var onlyAHas: String
}
struct B: P {
var onlyBHas: String
}
var ins1: any P = A()
var ins2: any P = B()
ins1.onlyAHas = "a only"
ins2.onlyBHas = "b only"
在进入解决方案之前,让我们分解一下any
的含义,当我们使用它时,我们还将包括some
:
当你写:
var ins1: any P = A()
您告诉编译器要ins1
用作P
。 它是面向协议的等价物,相当于这个 OOP 代码:
class Base {
var baseProperty: String? = nil
}
class Concrete: Base {
var concreteProperty: String? = nil
}
let obj: Base = Concrete();
obj.baseProperty = "Some value" // <-- This is fine
obj.concreteProperty = "Some value" // <-- This is an error
此代码告诉编译器obj
是一个Base
。 你可以从Concrete
分配它,但因为它是Base
的子类,但obj
在本地仍然被称为Base
而不是Concrete
,所以它无法访问不是从Base
继承的Concrete
的属性。
在您的示例中也是如此。ins1
在当地被称为P
而不是A
,P
没有onlyAHas
属性。
你会得到类似的行为与some
而不是any
。 两者之间有一些区别,但让我们只谈谈主要的
:some
告诉编译器,它将是一个可以解析为一个特定具体类型的类型,但它应该在源代码中强制对协议进行抽象。 这允许它在内部生成更有效的代码,因为知道具体类型允许编译器直接调用具体实现,而不是通过其协议见证表,这是 OOP 中"vtable"的面向协议的模拟,所以当编译器去虚拟化方法调用时,效果就像在 OOP 中一样,因为尽管语法, 它知道实际的混凝土类型。 这避免了动态调度的运行时开销,同时仍然允许您使用存在类型的抽象...好吧,它更像是要求你使用存在类型的抽象,而不是允许你,因为从源代码的角度来看,抽象是强制执行的。
any
也强制执行抽象,但在编译器可以执行的优化类型方面,它却是相反的。 它说编译器必须遍历协议见证表,因为正如关键字所暗示的那样,它的值可以是符合协议的任何具体类型,即使编译器可以确定它实际上只是本地的一个特定类型。 它还允许放宽有关在协议具有Self
和associatedtype
约束时将协议用作类型的一些规则。
但无论哪种方式,您都在告诉编译器您希望将ins1
用作P
而不是A
。
解决方案
实际上,有几种解决方案:
下投
第一种是向下投向混凝土类型,正如Joakim Danielson在评论中所建议的那样:
if var ins1 = ins1 as? A {
ins1.onlyAHas = "a only"
}
向下转换是一种代码异味,但有时实际上是最清晰或最简单的解决方案。 只要它在本地包含,并且不会成为使用类型实例的广泛做法,P
,它可能没问题。
但是,该示例确实存在一个问题:A 是值类型,因此要设置其onlyAHas
属性的ins1
是您显式创建的原始ins1
的副本。 同名会稍微混淆它。 如果您只需要更改在if
正文中生效,那就太好了。 如果您需要它保留在外部,则必须分配回原始版本。 使用相同的名称可以防止这种情况,因此您需要使用不同的名称。
仅在初始化时执行特定于具体代码的代码
这仅适用于具体类型只是预先为协议配置某些内容的情况,此后可以使用仅协议代码:
var ins1: any P = A(onlyAHas: "a only")
// From here on code can only do stuff with `ins1` that is defined in `P`
或者,您可以将初始化委托给内部知道具体类型但返回any P
的函数。
func makeA(_ s: String) -> any P
{
var a = A()
a.onlyAHas = s
return a
}
var ins1 = makeA("a only");
// From here on code can only do stuff with `ins1` that is defined in `P`
声明执行工作的协议方法/计算属性。
这是使用协议的常用方法。 在协议中声明方法类似于在基类中声明方法。 在符合标准的具体类型中实现该方法类似于重写子类中的方法。 如果不在协议扩展中提供默认实现,则协议将强制符合类型实现协议 - 这是与 OOP 方法相比的一大优势。
protocol P {
mutating func setString(_ s: String)
}
struct A: P
{
var onlyAHas: String
mutating func setString(_ s: String) {
onlyAHas = s
}
}
struct B: P
{
var onlyBHas: String
mutating func setString(_ s: String) {
onlyBHas = s
}
}
var ins1: any P = A()
var ins2: any P = B()
ins1.setString("a only") // <- Calls A's setString
ins2.setString("b only") // <- Calls B's setString
我用一种setString
方法做到这一点,但你当然可以在协议中使用计算变量来做同样的事情,那会更"Swifty"。 我这样做并不是为了强调将功能放在协议中的更一般的想法,而不是挂断所讨论的功能恰好是设置属性的事实。
如果不需要所有符合类型才能设置 String,一种解决方案是在 P 上的扩展中提供无操作的默认实现:
protocol P {
mutating func setString(_ s: String)
}
extension P
{
mutating func setString(_ s: String) { /* do nothing */ }
}
// Same A and B definitions go here
struct C: P { }
var ins3: any P = C();
ins1.setString("a only") // <- Calls A's setString
ins2.setString("b only") // <- Calls B's setString
ins3.setString("c only") // <- Calls setString from extension of P
但大多数情况下,设置/获取一些具体属性是执行某些随具体类型变化的任务的实现细节。 因此,您需要在协议中声明一个方法来执行该任务:
protocol P
{
mutating func frobnicate()
}
struct A
{
var onlyAHas: String
mutating func frobnicate()
{
// Do some stuff
onlyAHas = "a only"
// Do some other stuff that uses onlyAHas
}
}
B
将被类似地定义,做任何特定于它的事情。 如果注释中的内容是通用代码,则可以将其分解为序言,主要操作和尾声。
protocol P
{
mutating func prepareToFrobnicate()
mutating func actuallyFrobnicate() -> String
mutating func finishFrobnication(result: String)
}
extension P
{
/*
This method isn't in protocol, so this exact method will be called;
however, it calls methods that *are* in the protocol, we provide
default implementations, so if conforming types, don't implement them,
the versions in this extension are called, but if they do implement
them, their versions will be called.
*/
mutating func frobnicate()
{
prepareToFrobnicate()
finishFrobnication(result: actuallyFrobnicate());
}
mutating func prepareToFrobnicate() {
// do stuff general stuff to prepare to frobnicate
}
mutating func actuallyFrobnicate() -> String {
return "" // just some default value
}
mutating func finishFrobnication(result: String) {
// define some default behavior
}
}
struct A
{
var onlyAHas: String
mutating func actuallyFrobnicate() -> String
{
// Maybe do some A-specific stuff
onlyAHas = "a only"
// Do some more A-specific stuff
return onlyAHas
}
}
struct B
{
var onlyBHas: String
mutating func actuallyFrobnicate() -> String {
"b only"
}
mutating func finishFrobnication(result: String)
{
// Maybe do some B-specific stuff
onlyBHas = result
// Do some more B-specific stuff
}
}
var ins1: any P = A()
var ins2: any P = B()
ins1.frobnicate();
ins2.frobnicate();
在此示例中,调用协议扩展中的frobnicate
,因为它仅在协议扩展中定义。
对于ins1
,frobnicate
调用扩展的prepareToFrobnicate
,因为即使它直接在协议中声明,A
也不会实现它,并且在扩展中提供了默认实现。
然后它调用A
的actuallyFrobnicate
因为它是直接在协议中定义的,并且A
确实实现了它,因此不使用默认实现。 因此,将设置onlyAHas
属性。
然后它将结果从A
的actuallyFrobnicate
传递给扩展中的finishFrobnication
,因为它是直接在协议中定义的,但A
没有实现它,并且扩展提供了一个默认实现。
对于ins2
,frobnicate
仍然调用默认的prepareToFrobnicate
,然后调用B
的actuallyFrobnicate
实现,但B
的实现没有在那里设置它的onlyBHas
属性。 相反,它只返回一个字符串,frobnicate
传递给finishFrobnication
,调用B
的实现,因为与A
不同,B
提供了自己的实现,这就是B
设置它的地方。
使用这种方法,您可以同时标准化任务的一般算法,如frobnicate
,同时允许截然不同的实现行为。 当然,在这种情况下,A
和B
都只是在各自的具体类型中设置一个属性,但它们在算法的不同阶段执行此操作,您可以想象添加其他代码,因此这两种效果确实会非常不同。
这种方法的要点是,当我们调用inst1.frobnicate()
时,它不知道或关心inst1
内部正在做什么。 它在具体类型中内部设置onlyAHas
属性这一事实是调用代码不需要关注的实现细节。
只需使用混凝土类型
在代码示例中,您将在同一上下文中创建和使用ins1
和ins2
。 因此,它们可以很容易地定义如下:
var ins1 = A()
var ins2 = B()
ins1.onlyAHas = "a only" // <- This is fine because ins1 is an A
ins2.onlyBHas = "b only" // <- This is fine because ins2 is a B
如果你有一些功能,munge
你想在A
和B
上做,你可以定义它作为协议的术语。
func munge(_ p: any P)
{
// In here you can only use `p` as defined by the protocol, `P`
}
如果munge
需要做依赖于具体属性或方法的事情,您可以使用前面描述的方法之一......
或。。。
如果您确定您只会有少量符合P
的具体类型,诚然,有时不可能真正知道,但偶尔你会知道,那么您可以为每个具体类型编写专门的重载版本munge
:
func munge(_ a: A) {
// Do `A`-specific stuff with `a`
}
func munge(_ b: B) {
// Do `B`-specific stuff with `b`
}
这种倒退到此类问题的旧解决方案。 当我说这是一个旧的解决方案时,我指的是这样一个事实,即使当C++编译器只是一个预处理器,它将C++源代码转换为 C 源代码,然后编译,没有模板,标准化甚至还没有出现,它会让你重载函数。 你也可以用 Swift 做到这一点,这是一个完全有效的解决方案。 有时它甚至是最好的解决方案。 更常见的是,它会导致代码重复,但它在您的工具箱中,可以在适当的时候使用。