在Swift中,我如何声明一个特定类型的变量,该变量符合一个或多个协议?

105
在Swift中,我可以通过以下方式声明变量来明确设置变量的类型:
var object: TYPE_NAME

如果我们想更进一步地声明一个符合多个协议的变量,我们可以使用 protocol 声明:

var object: protocol<ProtocolOne,ProtocolTwo>//etc

如果我想声明一个符合一个或多个协议并且还是特定基类类型的对象,该怎么办?在Objective-C中,相应代码如下:
NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;

在Swift中,我期望它看起来像这样:

var object: TYPE_NAME,ProtocolOne//etc

这使得我们具有灵活性,能够处理基本类型的实现以及协议中定义的附加接口。

我是否忽略了另一种更明显的方法?

示例

举个例子,假设我有一个UITableViewCell工厂,负责返回符合协议的单元格。我们可以轻松设置一个通用函数,返回符合协议的单元格:

class CellFactory {
    class func createCellForItem<T: UITableViewCell where T:MyProtocol >(item: SpecialItem,tableView: UITableView) -> T {
        //etc
    }
}

稍后我希望能够使用类型和协议的优势来取消排队这些单元格。
var cell: MyProtocol = CellFactory.createCellForItem(somethingAtIndexPath) as UITableViewCell

这会返回一个错误,因为表格视图单元格不符合协议...

我想在变量声明中指定cell是UITableViewCell并符合MyProtocol,你能帮我翻译吗?

解释

如果您熟悉工厂模式,那么在能够返回实现特定接口的特定类的对象的上下文中,这将是有意义的。

就像我的例子一样,有时我们喜欢定义适用于特定对象的接口。我的表格视图单元格示例就是这样的理由之一。

虽然提供的类型不完全符合所提到的接口,但工厂返回的对象确实符合,因此我希望在与基类类型和声明的协议接口交互时具有灵活性。


抱歉,但在 Swift 中这样做的意义是什么?类型已经知道它们符合哪些协议。为什么不直接使用类型呢? - Kirsteins
1
@Kirsteins 只有当类型是从工厂返回的,因此是具有共同基类的通用类型时,才不会。 - Daniel Galasko
请提供一个例子,如果可能的话。 - Kirsteins
NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...; 这个对象似乎相当无用,因为 NSSomething 已经知道它符合哪些协议。如果它不符合 <> 中的任何一个协议,你将会遇到 unrecognised selector ... 崩溃。这完全没有提供类型安全性。 - Kirsteins
@Kirsteins,请再次查看我的示例,它用于当您知道工厂提供的对象是符合指定协议的特定基类时。 - Daniel Galasko
显示剩余2条评论
5个回答

83

在Swift 4中,现在可以声明一个变量,它是一个类型的子类并同时实现一个或多个协议。

var myVariable: MyClass & MyProtocol & MySecondProtocol

执行可选变量:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

或者作为方法的参数:

func shakeEm(controls: [UIControl & Shakeable]) {}

苹果在WWDC 2017上宣布了这个消息,会话402:Swift的新功能

其次,我想谈谈组合类和协议。在这里,我介绍了用于UI元素的可抖动协议,可以为自身提供一些震动效果以吸引人们的注意力。然后,我已经扩展了一些UIKit类来提供这种抖动功能。现在,我想写一个似乎很简单的函数。我只想写一个函数,它接受一堆可抖动的控件并抖动启用的控件以引起关注。我应该在这个数组中写什么类型?这实际上很令人沮丧和棘手。因此,我可以尝试使用UI控件。但是,并不是所有的UI控件在这个游戏中都是可抖动的。我可以尝试可抖动,但并不是所有可抖动的都是UI控件。在Swift 3中实际上没有好的方法来表示这一点。 Swift 4引入了将类与任意数量的协议进行组合的概念。


3
只需添加一个链接到 Swift 进化提案 https://github.com/apple/swift-evolution/blob/master/proposals/0156-subclass-existentials.md - Daniel Galasko
谢谢你,Philipp! - Omar Albeik
如果需要一个可选的此类型变量怎么办? - Vyachaslav Gerchicov
2
@VyachaslavGerchicov:你可以在它周围加上括号,然后像这样加上问号:var myVariable: (MyClass&MyProtocol&MySecondProtocol)? - Philipp Otto

30
您不能这样声明变量:
var object:Base,protocol<ProtocolOne,ProtocolTwo> = ...

不要像这样声明函数返回类型。
func someFunc() -> Base,protocol<MyProtocol,Protocol2> { ... }

你可以像这样声明为函数参数,但基本上是向上转型。
func someFunc<T:Base where T:protocol<MyProtocol1,MyProtocol2>>(val:T) {
    // here, `val` is guaranteed to be `Base` and conforms `MyProtocol` and `MyProtocol2`
}

class SubClass:BaseClass, MyProtocol1, MyProtocol2 {
   //...
}

let val = SubClass()
someFunc(val)

目前,你只能点赞:

class CellFactory {
    class func createCellForItem(item: SpecialItem) -> UITableViewCell {
        return ... // any UITableViewCell subclass
    }
}

let cell = CellFactory.createCellForItem(special)
if let asProtocol = cell as? protocol<MyProtocol1,MyProtocol2> {
    asProtocol.protocolMethod()
    cell.cellMethod()
}

因此,从技术上讲,cellasProtocol 是相同的。

但是,对于编译器而言,cell 仅具有 UITableViewCell 的接口,而 asProtocol 则只有协议接口。因此,当您想调用 UITableViewCell 的方法时,必须使用 cell 变量。当您想调用协议方法时,请使用 asProtocol 变量。

如果您确定单元格符合协议,就不必使用 if let ... as? ... {}。例如:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>

由于工厂指定了返回类型,我在技术上不需要执行可选转换吗?我可以依赖Swift的隐式类型来执行类型化,在那里我明确声明协议? - Daniel Galasko
我不明白你的意思,抱歉我的英语水平不好。如果你是在说 -> UITableViewCell<MyProtocol>,那么这是无效的,因为 UITableViewCell 不是一个泛型类型。我认为这甚至无法编译。 - rintaro
2
我不这么认为。即使你能这样做,cell只有协议方法(对于编译器而言)。 - rintaro
不,那实际上是我一直在做的方式 :) Swift 推断协议符合性和类型。 - Daniel Galasko
@rintaro 这是最好的方法吗?虽然它可以工作,但感觉有点“不自然”? - Just a coder
显示剩余6条评论

2
很遗憾,Swift不支持对象级协议一致性。但是,有一个有点尴尬的解决方法可能能满足您的需求。
struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

    init<T: UIViewController>(vc: T) where T: SomeProtocol {
        self.protocol = vc
        self.viewController = vc
    }
}

那么,无论您需要执行UIViewController的任何操作,都可以访问结构体的.viewController部分;如果您需要使用协议方面的内容,则应引用.protocol。

例如:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

   init<T: UIViewController>(injectedViewController: T) where T: SomeProtocol {
       self.mySpecialViewController = VCWithSomeProtocol(vc: injectedViewController)
   }
}

现在,每当您需要mySpecialViewController执行任何UIViewController相关的操作时,只需引用mySpecialViewController.viewController,每当您需要它执行某些协议函数时,引用mySpecialViewController.protocol即可。
希望Swift 4将来能够允许我们声明附有协议的对象。但现在,这个方法是可行的。
希望这可以帮到您!

1

编辑:我搞错了,但如果有人像我一样误解了这个问题,我将保留这个答案。OP问的是如何检查给定子类对象的协议符合性,而被接受的答案则讨论了基类的协议符合性。

也许我错了,但你不是在谈论如何向UITableCellView类添加协议符合性吗? 在这种情况下,协议是扩展到基类而不是对象。请参阅苹果的使用扩展声明协议采用文档,您的情况将类似于:

extension UITableCellView : ProtocolOne {}

// Or alternatively if you need to add a method, protocolMethod()
extension UITableCellView : ProcotolTwo {
   func protocolTwoMethod() -> String {
     return "Compliant method"
   }
}

除了已经提到过的Swift文档,还可以参考Nate Cook的文章 Generic functions for incompatible types ,其中包含更多示例。
这使我们有了处理基础类型实现以及协议中定义的添加接口的灵活性。
是否有其他更明显的方法我可能会错过?
采用协议采纳将实现此功能,使对象遵守所给定的协议。然而,请注意负面影响,即给定协议类型的变量不知道协议之外的任何内容。但是,可以通过定义具有所有所需方法/变量/...的协议来避免这种情况。
虽然提供的类型不完全符合所提到的接口,但工厂返回的对象确实如此,因此我希望在与基类类型和声明的协议接口交互方面具有灵活性。
如果你希望一个通用的方法或变量符合协议和基类类型,可能会很困难。但看起来你需要定义一个足够广泛的协议,以拥有所需的一致性方法,并且同时足够狭窄,以便可以轻松地将其适用于基类(即只需声明一个类符合该协议)。

1
这不是我想要的,但还是谢谢 :) 我想要能够通过类和特定协议与对象进行接口交互。就像在 obj-c 中我可以做 NSObject<MyProtocol> obj = ... 不用说,在 Swift 中无法实现这一点,你必须将对象转换为其协议。 - Daniel Galasko

0

我曾经遇到过类似的情况,当我试图在Storyboards中链接我的通用交互器连接时(IB不允许您将输出连接到协议,只能连接到对象实例),我通过简单地使用私有计算属性来掩盖基类公共ivar来解决这个问题。虽然这并不能防止某些人从根本上进行非法分配,但它确实提供了一种方便的方法来安全地防止与不符合规范的实例发生任何不必要的交互(即防止调用不符合协议的对象的委托方法)。

例如:

@objc protocol SomeInteractorInputProtocol {
    func getSomeString()
}

@objc protocol SomeInteractorOutputProtocol {
    optional func receiveSomeString(value:String)
}

@objc class SomeInteractor: NSObject, SomeInteractorInputProtocol {

    @IBOutlet var outputReceiver : AnyObject? = nil

    private var protocolOutputReceiver : SomeInteractorOutputProtocol? {
        get { return self.outputReceiver as? SomeInteractorOutputProtocol }
    }

    func getSomeString() {
        let aString = "This is some string."
        self.protocolOutputReceiver?.receiveSomeString?(aString)
    }
}

“outputReceiver”被声明为可选项,同样的,“protocolOutputReceiver”也是私有的可选项。通过始终通过后者(计算属性)访问outputReceiver(又名委托),我有效地过滤掉任何不符合协议的对象。现在,我可以简单地使用可选链接来安全地调用委托对象,无论它是否实现协议或存在。
要将此应用于您的情况,您可以将公共ivar的类型设置为“YourBaseClass?”(而不是AnyObject),并使用私有计算属性来强制执行协议一致性。顺便说一下。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接