【问题标题】:Why do properties modified by @optional become immutable?为什么@optional 修改的属性会变成不可变的?
【发布时间】:2022-01-11 08:07:18
【问题描述】:

我有一个 Objective-C 协议,其中包含如下属性:

#import <Foundation/Foundation.h>

@protocol Playback <NSObject>

@optional

@property (nonatomic, nonnull) NSURL *assetURL;

@end

PlayerController 有一个id&lt;Playback&gt; 类型的属性:

@interface PlayerController: NSObject

@property (nonatomic, strong, nonnull) id<Playback> currentPlayerManager;

@end

我尝试在 Swift 中编写以下代码,但出现错误

var player = PlayerController()
var pla = player.currentPlayerManager

pla.assetURL = URL(string: "123") // ❌ Cannot assign to property: 'pla' is immutable

如果我为Playback 协议注释掉@optional,那么它编译得很好。

这让我想知道为什么@optional 会导致这个错误?

【问题讨论】:

  • 这实际上可能值得在Swift forums 上询问,在那里您可能会得到从事编译器工作的人来插话。这似乎是一个非常小众的边缘案例,其编译器诊断是也不是特别有帮助;这可能是一个无法编译的错误(这在 Obj-C 中确实有效),或者至少可以优化消息。 FWIW,您可以在纯 Swift 中定义完全相同的协议 + 属性(使用 @objc 注释以允许 optional var),您将看到相同的行为。
  • 另一个问题是 URL 和 NSURL 之间的类型不匹配。
  • @ItaiFerber 我也想在 Swift 论坛上问一个问题,但很遗憾,由于某些客观原因,我无法访问该站点...
  • @ItaiFerber 如果您能帮助我,将不胜感激,您可以在有明确结论时给我发送电子邮件或在此问题下回复我。这可能需要您花费一些时间,但如果您没有时间,那就直接问我吧,只要这个问题能帮助更多的人,我会很高兴。
  • @Rakuyo 绝对。我会在论坛上提问并在此处转发(希望是确定的)回复。

标签: swift objective-c objective-c-swift-bridge objective-c-protocol


【解决方案1】:

来自 Jordan Rose(在实现 SE-0070 时从事 Swift 工作)on the forums

通常可选要求会增加额外的可选性:

  • 方法本身成为可选的 (f.bar?())
  • 属性获取器将值包装在Optional (if let bar = f.bar) 的额外级别中

但是对于属性设置器来说,没有地方可以放置额外的 Optional 级别。这就是整个故事:我们从未想过如何以安全的方式公开可选的属性设置器,并且不想选择任何特定的不安全解决方案。如果有人能想到一些很棒的东西!

所以答案似乎是:当时optional 协议要求被故意限制为 Swift 中的 Objective-C 协议 (SE-0070),没有决定明确实现的拼写,而且看起来这个功能太不常见了,从那以后就没有真正出现过。

直到(如果)这被支持,有两种潜在的解决方法:

  1. Playback 引入一个显式方法,为assetURL 赋值

    • 遗憾的是,这个方法不能命名为-setAssetURL:,因为它将被导入到 Swift 中,就好像它是属性设置器而不是方法一样,你仍然无法调用它。 (如果您将assetURL 标记为readonly,这仍然是正确的)
    • 同样遗憾的是,这个方法不能有一个默认实现,因为 Objective-C 不支持默认协议实现,你不能在 Swift extension 中给这个方法一个实现,因为你仍然无法分配给协议
  2. 像你在 Swift 中所做的那样,引入一个协议层次结构,例如,AssetBackedPlayback 协议继承自 Playback 并提供 assetURL 作为非@optional-改为属性:

    @protocol Playback <NSObject>
    // Playback methods
    @end
    
    @protocol AssetBackedPlayback: Playback
    @property (nonatomic, nonnull) NSURL *assetURL;
    @end
    

    然后,您需要找到一种方法将PlayerController.currentPlayerManager 公开为AssetBackedPlayback,以便分配assetURL


约旦的一些其他替代品:

我认为最初推荐的解决方法是“在 Objective-C 中编写一个 static inline 函数来为你做这件事”,但这也不是很好。 setValue(_:forKey:) 如果不在热门路径中,在实践中也足够好。

static inline 函数推荐的功能与默认协议实现类似,但您需要记住调用该函数而不是直接访问属性。

setValue(_:forKey:) 也可以工作,但会导致明显的性能损失,因为它通过 Objective-C 运行时支持很多动态,并且比简单的赋值要复杂得多。根据您的用例,为了避免复杂性,成本可能是可以接受的!

【讨论】:

    【解决方案2】:

    由于它是可选的,Swift 不能保证 setter 被实现。

    【讨论】:

    • 那么为什么我尝试从中获取值时它没有报错呢? getter方法一定存在吗?为什么?
    • 检索值可以返回一个可选值。它不需要存在。
    • 我不同意这个推理,但理论上,setter 也可以像实现optional 方法一样实现:如果setter 不存在,则操作为否-操作。我仍然会对更权威的推理感到好奇——我认为这在任何地方都没有真正的记录,但同样,它可能值得在 Swift 论坛上提出(或深入研究编译器)。
    猜你喜欢
    • 2019-09-10
    • 2011-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-14
    相关资源
    最近更新 更多