如何创建符合Swift和Objective-C共享协议的类方法?

11

最近我一直在学习Swift。

我决定编写一个混合使用Swift/Objective-C的应用程序,使用相同的算法在两种语言中实现计算密集型任务。

该程序计算了一个大数组的质数。

我定义了一个协议,Swift和Objective-C版本的calculate对象都应该符合该协议。

这些对象都是单例,所以我在Objective-C中创建了一个典型的单例访问方法:

+ (NSObject <CalcPrimesProtocol> *) sharedInstance;

整个协议看起来像这样:

#import <Foundation/Foundation.h>
@class ComputeRecord;

typedef void (^updateDisplayBlock)(void);
typedef void (^calcPrimesCompletionBlock)(void);

    @protocol CalcPrimesProtocol <NSObject>

- (void) calcPrimesWithComputeRecord: (ComputeRecord *) aComputeRecord
              withUpdateDisplayBlock: (updateDisplayBlock) theUpdateDisplayBlock
                  andCompletionBlock: (calcPrimesCompletionBlock) theCalcPrimesCompletionBlock;

    @optional //Without this @optional line, the build fails.
    + (NSObject <CalcPrimesProtocol> *) sharedInstance;

    @end

Objective-C 版本的类完全按照上述定义实现方法,无需担心。

Swift 版本有一个方法:

  class func sharedInstance() -> CalcPrimesProtocol

然而,如果我将该方法作为协议的必需方法,编译器会报错“类型"CalcPrimesSwift"不符合协议"CalcPrimesProtocol"。

但是,如果我在协议中将单例类方法sharedInstance标记为可选项,它就能工作了,我可以在我的Swift类或Objective-C类上调用该方法。

我是否在定义Swift类方法时遗漏了一些微妙之处?这似乎不太可能,因为我能够在我的Swift类或Objective-C类上调用sharedInstance()类方法。

如果您想要的话,可以从Github下载这个项目并检查它。它被称为SwiftPerformanceBenchmark。(链接)


嗯,这只是一个冒险猜测,但是你的 class func 应该返回符合 CalcPrimesProtocol 协议的 AnyObject,而不是协议对象本身吗? - ravron
我很想再有一双眼睛看看,我被难住了。 - Duncan C
为什么要在Swift中复制协议?整个重点是建立互操作性,使得Swift类/协议可以在Obj-C中使用,反之亦然。 - Duncan C
让我们在聊天中继续这个讨论 - Duncan C
@DuncanC 你更新到Xcode 6.3了吗?我今天会更新我的答案。 - nhgrif
显示剩余3条评论
1个回答

11
在Objective-C中,我们总是传递指针,指针可以为nil。许多Objective-C程序员利用了发送消息到nil时什么都不做并返回0/nil/NO的事实。Swift完全以不同的方式处理nil。对象要么存在(从不为nil),要么无法确定它们是否存在(这就是Swift可选项起作用的地方)。
因此,在Xcode 6.3之前,这意味着使用任何Objective-C代码的任何Swift代码都必须将所有对象引用视为Swift可选项。Objective-C的语言规则没有阻止对象指针为nil。
对于使用Objective-C协议、类等的Swift,这意味着一团糟。我们不得不在两个非完美的解决方案之间选择。
给定以下Objective-C协议:
@protocol ObjCProtocol <NSObject>

@required + (id<ObjCProtocol>)classMethod;
@required - (id<ObjCProtocol>)instanceMethod;
@required - (void)methodWithArgs:(NSObject *)args;

@end

我们可以接受方法定义中含有隐式解包可选项:

class MyClass: NSObject, ObjCProtocol {
    func methodWithArgs(args: NSObject!) {
        // do stuff with args
    }
}

这样做可以使代码更加简洁(我们不必在正文中解包),但我们总是有可能遇到“在解包可选项时发现了空值”错误。
或者,我们可以将该方法定义为真正的可选项:
class MyClass: NSObject, ObjCProtocol {
    func methodWithArgs(args: NSObject?) {
        // unwrap do stuff with args
    }
}

但是这会留下许多混乱的解包代码。
Xcode 6.3解决了这个问题,并为Objective-C代码添加了“Nullability Annotations”。
引入了两个关键词:nullable和nonnull。它们用于声明Objective-C代码的返回类型或参数类型的相同位置。
- (void)methodThatTakesNullableOrOptionalTypeParameter:(nullable NSObject *)parameter;
- (void)methodThatTakesNonnullNonOptionalTypeParameter:(nonnull NSObject *)parameter;
- (nullable NSObject *)methodReturningNullableOptionalValue;
- (nonnull NSObject *)methodReturningNonNullNonOptionalValue;

除了这两个注释关键字之外,Xcode 6.3还引入了一组宏,使得将大段的Objective-C代码标记为nonnull变得更加容易(没有任何注释的文件被认为是nullable)。为此,我们在该部分顶部使用NS_ASSUME_NONNULL_BEGIN,在我们希望标记的部分底部使用NS_ASSUME_NONNULL_END
例如,我们可以使用这对宏来包装整个协议。
NS_ASSUME_NONNULL_BEGIN
@protocol CalcPrimesProtocol <NSObject>

- (void) calcPrimesWithComputeRecord: (ComputeRecord *) aComputeRecord
              withUpdateDisplayBlock: (updateDisplayBlock) theUpdateDisplayBlock
                  andCompletionBlock: (calcPrimesCompletionBlock) theCalcPrimesCompletionBlock;
+ (id <CalcPrimesProtocol> ) sharedInstance;

@end
NS_ASSUME_NONNULL_END

这与将所有指针参数和返回类型标记为nonnull具有相同的效果(除了若干例外,正如苹果的Swift博客中所指出的那样)。

在Xcode 6.3之前

一个符合Objective-C协议的Swift类必须将该协议中的任何Objective-C类型视为可选项。

为了弄清楚这一点,我创建了以下的Objective-C协议:

@protocol ObjCProtocol <NSObject>

@required + (id<ObjCProtocol>)classMethod;
@required - (id<ObjCProtocol>)instanceMethod;
@required - (void)methodWithArgs:(NSObject *)args;

@end

然后,创建了一个继承自NSObject的Swift类,并声明自己符合这个ObjCProtocol

然后,我接着输入这些方法名,让Swift自动帮我补全方法,这就是我得到的结果(我加入了方法体,其余为自动补全):

class ASwiftClass : NSObject, ObjCProtocol {
    class func classMethod() -> ObjCProtocol! {
        return nil
    }
    
    func instanceMethod() -> ObjCProtocol! {
        return nil
    }
    
    func methodWithArgs(args: NSObject!) {
        // do stuff
    }
}

现在,如果我们想要的话,可以使用常规可选项(带有?)代替这些自动解包的可选项。编译器对两者都非常满意。关键是我们必须允许可能出现nil的情况,因为Objective-C协议无法防止传递nil。如果这个协议在Swift中实现,我们可以选择返回类型是否为可选项,而Swift会阻止我们向没有定义非可选项返回类型的方法返回nil

非常出色(被投票选中)。感谢您解决了这个问题。现在是时候改变我的Swift方法,以尝试遵守Obj-C协议了。 - Duncan C
我猜我说得太早了。将函数结果设置为“符合CalcPrimesProtocol协议”的可选类型并不能解决问题。 - Duncan C
编译器要求我将其声明为 class func sharedInstance() -> NSObject!。然而,结果不符合我的协议。 - Duncan C
哦,我明白了。你不能将协议的方法返回类型声明为(id<CalcPrimesProtocol>)sharedInstance;吗?你需要NSObject *而不是id,有特定的原因吗? - nhgrif

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