如何在Swift协议中声明可选方法?

430

在Swift中是否有可能实现?如果不能,是否有解决方法可以实现?


如果未来的读者正在寻找它,这里是来自《Swift编程语言》的可选协议要求的讨论。 - Rob
19个回答

651

1. 使用默认实现(首选)。

protocol MyProtocol {
    func doSomething()
}

extension MyProtocol {
    func doSomething() {
        /* return a default value or just leave empty */
    }
}

struct MyStruct: MyProtocol {
    /* no compile error */
}

优点

  • 无需使用 Objective-C 运行时环境(至少不需要明确地指定)。这意味着您可以将结构体、枚举和非NSObject类符合该协议。此外,这意味着您可以利用强大的泛型系统。

  • 当遇到符合此类协议的类型时,您始终可以确定所有要求都得到满足。它总是具体实现或默认实现。这就是其他语言中“接口”或“协议”的行为方式。

缺点

  • 对于非Void要求,您需要有一个合理的默认值,这并非总是可能的。但是,当您遇到此问题时,这意味着要么这种要求确实没有默认实现,要么您在 API 设计过程中犯了错误。

  • 您无法区分默认实现和根本没有实现,至少不需要通过特殊的返回值来解决该问题。请考虑以下示例:

    protocol SomeParserDelegate {
        func validate(value: Any) -> Bool
    }
    

    如果您提供了一个默认实现,它只返回true - 乍一看没问题。现在,请考虑以下伪代码:

    final class SomeParser {
        func parse(data: Data) -> [Any] {
            if /* delegate.validate(value:) is not implemented */ {
                /* parse very fast without validating */
            } else {
                /* parse and validate every value */
            }
        }
    }
    

    没有办法实现这样的优化,因为您无法知道委托是否实现了某个方法。

    尽管有许多不同的方法可以克服这个问题(例如使用可选闭包、为不同操作使用不同的委托对象等),但该示例清楚地呈现了问题。


2. 使用@objc optional

@objc protocol MyProtocol {
    @objc optional func doSomething()
}

class MyClass: NSObject, MyProtocol {
    /* no compile error */
}

优点

  • 无需实现默认方法,只需声明一个可选的方法或变量即可使用。

缺点

  • 它严重限制了你的协议能力,因为要求所有符合类型与 Objective-C 兼容。这意味着,只有继承自 NSObject 的类可以符合这样的协议。没有结构体、枚举、关联类型。

  • 你必须经常检查是否实现了可选方法,通过可选调用或检查符合类型是否实现它。如果经常调用可选方法,这可能会引入很多模板代码。


2
因为“可选要求”(这本身就是个矛盾)违背了契约的理念,阻止了静态分派。Swift核心团队成员之一(我不记得确切是谁了)说,Swift只有“可选协议要求”,因为它需要与Obj-C互操作。此外,在swift-evolution的早期阶段,本地“可选要求”的想法很快被放弃了。 - akashivskyy

416
在Swift 2及以后的版本中,可以为协议添加默认实现。这为协议中的可选方法创建了一种新的方式。

在Swift 2及以后的版本中,可以为协议添加默认实现。这为协议中的可选方法创建了一种新的方式。

protocol MyProtocol {
    func doSomethingNonOptionalMethod()
    func doSomethingOptionalMethod()
}

extension MyProtocol {
    func doSomethingOptionalMethod(){ 
        // leaving this empty 
    }
}

这不是一种创建可选协议方法的很好方式,但它使您有可能在协议回调中使用结构体。

我在这里写了一个小总结: https://www.avanderlee.com/swift-2-0/optional-protocol-methods/


2
这可能是在Swift中最干净的方法。太遗憾了,在Swift 2.0之前它不起作用。 - Entalpi
16
我发现你实际上需要在协议定义中声明函数,否则空操作扩展函数不会被符合该协议的类重写覆盖。 - Ian Pearce
5
@IanPearce 的说法是正确的,并且这似乎是有意为之的。在 WWDC 的“面向协议编程”演讲(408)中,他们谈到主协议中的方法是提供给符合类型的“定制点”。一个必要的定制点不会在扩展中接收定义;可选的定制点则会。通常不应该被定制的协议方法完全在扩展中声明/定义,以防止符合类型进行定制,除非你明确地向下转换到它的 dynamicType 来展示你想要符合者的自定义实现。 - matthias
4
@FranklinYu,你可以这样做,但这样会在实际上不需要的地方污染API设计中的“throws”。我更喜欢“微协议”的想法。例如,每个方法都符合一个协议,然后你可以检查:对象是否符合协议。 - Darko
5
我认为你应该将扩展中的函数设为公共函数,因为协议中的函数在定义时就是公共的。如果协议在其模块外被使用,你的解决方案将无法正常工作。 - Vadim Eisenberg
显示剩余4条评论

43

以下是使用委托模式的具体示例。

设置您的协议:

@objc protocol MyProtocol:class
{
    func requiredMethod()
    optional func optionalMethod()
}

class MyClass: NSObject
{
    weak var delegate:MyProtocol?

    func callDelegate()
    {
        delegate?.requiredMethod()
        delegate?.optionalMethod?()
    }
}

将委托设置为一个类,并实现协议。请注意,可选方法无需实现。

class AnotherClass: NSObject, MyProtocol
{
    init()
    {
        super.init()

        let myInstance = MyClass()
        myInstance.delegate = self
    }

    func requiredMethod()
    {
    }
}

重要的一点是可选方法是可选的,在调用时需要使用“?”。提到第二个问号。

delegate?.optionalMethod?()

2
这应该是正确的答案。简单、清晰且易于理解。 - Echelon

40

由于有一些关于如何使用可选修饰符@objc属性定义可选要求协议的答案,因此我将提供一个示例,展示如何使用协议扩展定义可选协议。

以下代码是Swift 3.*。

/// Protocol has empty default implementation of the following methods making them optional to implement:
/// `cancel()`
protocol Cancelable {

    /// default implementation is empty.
    func cancel()
}

extension Cancelable {

    func cancel() {}
}

class Plane: Cancelable {
  //Since cancel() have default implementation, that is optional to class Plane
}

let plane = Plane()
plane.cancel()
// Print out *United Airlines can't cancelable*

请注意,协议扩展方法无法被Objective-C代码调用,更糟糕的是,Swift团队不会修复它。 https://bugs.swift.org/browse/SR-492

1
干得好!这应该是正确的答案,因为它解决了问题而不涉及Objective-C运行时。 - Lukas
在 Swift 文档中,由扩展提供默认实现的协议要求与可选协议要求是不同的。尽管符合类型不必提供它们自己的任何一个实现,但具有默认实现的要求可以在不使用可选链接的情况下调用。 - Mec Os

36

当使用 Swift 特定类型时,将协议标记为 "@objc" 的其他答案在此不起作用。

struct Info {
    var height: Int
    var weight: Int
} 

@objc protocol Health {
    func isInfoHealthy(info: Info) -> Bool
} 
//Error "Method cannot be marked @objc because the type of the parameter cannot be represented in Objective-C"
为了声明可选协议并与 Swift 兼容,应将函数声明为变量而非 func。
protocol Health {
    var isInfoHealthy: (Info) -> (Bool)? { get set }
}

然后按照以下步骤实现该协议

class Human: Health {
    var isInfoHealthy: (Info) -> (Bool)? = { info in
        if info.weight < 200 && info.height > 72 {
            return true
        }
        return false
    }
    //Or leave out the implementation and declare it as:  
    //var isInfoHealthy: (Info) -> (Bool)?
}
你可以使用 "?" 来检查函数是否已经被实现。
func returnEntity() -> Health {
    return Human()
}

var anEntity: Health = returnEntity()

var isHealthy = anEntity.isInfoHealthy(Info(height: 75, weight: 150))? 
//"isHealthy" is true

3
使用这个解决方案,你无法从协议函数内部访问self。这可能会在某些情况下引起问题! - George Green
1
@GeorgeGreen 你可以访问self。将函数变量标记为lazy,并在闭包内使用捕获列表 - Zag
只有类、协议、方法和属性可以使用 @objc。如果在 @objc 协议方法定义中使用枚举参数,那么你就会遇到麻烦。 - khunshan
1
@khunshan,这个方法不需要标记 @objc,你指的是什么? - Zag
这是关于枚举在Swift和Objective-C之间无法使用的信息,而其他语句可以通过@objc关键字进行桥接。 - khunshan
对于你提到的第一个错误,你需要将协议作为NSObject的子类:@objc protocol Health: AnyObject {... - tontonCD

35

Swift 3.0

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

它会节省您的时间。


6
现在需要在所有成员上使用@objc的原因是什么? - DevAndArtist
@Gautam Sareriya:你认为哪种方法更好,是采用这种方法还是创建空扩展方法? - Sentry.co
我尝试使用您的方法并添加了 required 标志,但是出现了错误:required 只能用于 'init' 声明。 - Ben

29
  • 在每个方法之前需要添加optional关键字。
  • 请注意,为了使此功能正常工作,您的协议必须标记有@objc属性。
  • 这进一步意味着该协议适用于类而不是结构体。

小修正(太小了不值得编辑!)但应该是“optional”而不是“@optional”。 - Ali Beadle
7
这还需要您将实现协议的类标记为 @objc,而不仅仅是协议本身。 - Johnathon Sullinger

17

你可以使用两种方法在Swift协议中创建可选方法。

1 - 第一种选择是使用@objc属性标记您的协议。虽然这意味着它只能被类采用,但它确实意味着您可以将个别方法标记为可选项,如下所示:

@objc protocol MyProtocol {
    @objc optional func optionalMethod()
}

2 - 一种更快的方法:这个选项更好。编写默认实现可选方法,什么也不做,就像这样。

protocol MyProtocol {
    func optionalMethod()
    func notOptionalMethod()
}

extension MyProtocol {

    func optionalMethod() {
        //this is a empty implementation to allow this method to be optional
    }
}

Swift有一个叫做扩展(extension)的特性,它允许我们为那些想要成为可选方法的方法提供默认实现。


14

使用纯Swift的协议继承方法:

//Required methods
protocol MyProtocol {
    func foo()
}

//Optional methods
protocol MyExtendedProtocol: MyProtocol {
    func bar()
}

class MyClass {
    var delegate: MyProtocol
    func myMethod() {
        (delegate as? MyExtendedProtocol).bar()
    }
}

11

为了说明安东尼的答案的原理:

protocol SomeProtocol {
    func aMethod()
}

extension SomeProtocol {
    func aMethod() {
        print("extensionImplementation")
    }
}

class protocolImplementingObject: SomeProtocol {

}

class protocolImplementingMethodOverridingObject: SomeProtocol {
    func aMethod() {
        print("classImplementation")
    }
}

let noOverride = protocolImplementingObject()
let override = protocolImplementingMethodOverridingObject()

noOverride.aMethod() //prints "extensionImplementation"
override.aMethod() //prints "classImplementation"

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