草庐IT

swift - 我怎样才能使这个 Swift 事件处理程序样板更简洁?

coder 2023-09-10 原文

我在 Java 中看到过一种模式,它允许您以类型安全的方式实现回调列表的子集,并与使用回调的类内联:

registerHandlers(new ClassWithNoOpMethods() {
    @override
    public void onFooEvent(FooEvent event) { ... }
    @override
    public void onBarEvent(BarEvent event) { ... }
}

一切都很好而且类型安全。我想在 Swift 中做同样的事情,但是谷歌搜索没有找到任何(恕我直言)优雅的解决方案。所以我想到了这个:

let registrar = EventSource.getEventRegistrar()
registrar.onFooEvent = { event in doSomethingFoo(event) }
registrar.onBarEvent = { event in doSomethingBar(event) }
...
EventSource.removeEventCallbacks(registrar)

这对于消费事件来说效果很好——它只是我感兴趣的事件子集,它让我可以定义内联代码,等等。

但是,当我实际实现它时,我得到了很多重复的、样板的、非 DRY 代码。这冒犯了我,但我想不出更好的方法来实现上面显示的方案。我想向 StackOverflow 上的 Swift 大神们求助,向我展示一种更简洁的实现方法。

这是现在的样子:

public class Phone {
    ...phone stuff...
    public class PhoneEventRegistrar {

        let phone : Phone

        init(phone : Phone) {
            self.phone = phone
        }

        public typealias OnErrorCallback = (PhoneErrorType, String) -> Void
        private var onErrorValue : OnErrorCallback?
        public var onError : OnErrorCallback {
            get { return onErrorValue != nil ? onErrorValue! : {_,_ in} }
            set {
                assert(onErrorValue == nil, "onError cannot be set twice")
                onErrorValue = newValue
            }
        }
        func invokeErrorCallback(type : PhoneErrorType, message : String) {
            if let onErrorValue = onErrorValue {
                onErrorValue(type, message)
            }
        }

        public typealias OnCallStateChangeCallback = (CallState) -> Void
        private var onCallStateChangeValue : OnCallStateChangeCallback?
        public var onCallStateChange : OnCallStateChangeCallback {
            get { return onCallStateChangeValue != nil ? onCallStateChangeValue! : {_ in} }
            set {
                assert(onCallStateChangeValue == nil, "onCallStateChange cannot be set twice")
                onCallStateChangeValue = newValue
            }
        }
        func invokeCallStateChangeCallback(state : CallState) {
            if let onCallStateChangeValue = onCallStateChangeValue {
                onCallStateChangeValue(state)
            }
        }

        // and the mostly-similar code shown twice above is repeated for
        // each possible callback
    }

    func invokeErrorCallbacks(type : PhoneErrorType, message : String) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        registrars.forEach({$0.invokeErrorCallback(type, message: message)})
    }

    func invokeCallStateChangeCallbacks(state : CallState) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        registrars.forEach({$0.invokeCallStateChangeCallback(state)})
    }

    // again, the mostly similar block of code shown twice above is
    // repeated for each possible callback

    private var registrars : [PhoneEventRegistrar] = []
    public func getPhoneEventRegistrar() -> PhoneEventRegistrar {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        let registrar = PhoneEventRegistrar(phone: self)
        registrars.append(registrar)
        return registrar
    }

    public func removeRegistrarCallbacks(registrar : PhoneEventRegistrar) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        assert(registrars.contains({$0 === registrar}), "cannot remove callbacks, no matching PhoneEventRegistrar found")
        registrars = registrars.filter({$0 !== registrar})
    }
}

如果人们看到替代实现对事件消费者具有相同的可用性优势,我也很乐意看到这些实现。以下是我想到或尝试过的一些其他选项:

  • 实现定义回调的协议(protocol)。让事件消费者实现协议(protocol)并注册回调。否定:要求实现所有方法 - 强制执行过多的无操作回调。
  • 同上,但使用默认空白实现扩展协议(protocol)。否定:仍然需要事件消费者为回调实现一个额外的类。此外,缺少协议(protocol)扩展的动态调度意味着注册协议(protocol)将强制生成的事件转到无操作实现而不是真正的事件处理程序。
  • 将消费类设置为事件生产者的委托(delegate)。否定:仍然必须实现无操作回调。我只见过每个事件制作人有一个代表,我需要几个。 @Darko 下面的评论促使我进一步考虑这个选项。

最佳答案

NSNotificationCenter

您的问题有多种解决方案。一种可能是简单地使用 NSNotificationCenter 而不是实现您自己的事件机制。

使用 NSNotificationCenter 你可以注册事件:

NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(MyClass.myMethod),
name: "com.mycompany.myEvent1",
object: userData)

并从任何地方发送事件:

let userData: [NSObject: AnyObject] = [anyObject: anyDataToSend]
NSNotificationCenter.defaultCenter.postNotificationName("com.mycompany.myEvent1", object: userData)

您会在 SO 和 Google 上找到大量关于 NSNotificationCenter 的教程。

委托(delegate)可选协议(protocol)

另一种方法是使用委托(delegate)模式。创建一个协议(protocol)并将其标记为@objc,这样协议(protocol)方法就可以标记为“可选”。

@objc protocol MyEventProtocol: class {
  optional func foo() -> Bool
  func bar()
}

现在每个符合此协议(protocol)的对象都必须实现 bar(),但可以可选地实现 foo()。

class MyEventReceiver: MyEventProtocol {
    func bar() { // do something }
}

然后你的事件发送者类可以做这样的事情:

class MyEventSender {
     weak var delegate: MyEventProtocol? // the name "delegate" is just convention, you can use any other name

     init(receiver: MyEventReceiver) {
         self.delegate = receiver
     }

     func sendEventToReceiver() {
         if let delegate = self.delegate {
             delegate.func() // guaranteed by the protocol
             delegate.foo?() // foo is marked as optional, so you have to check it with ?. If foo is implemented on the receiver it will be called, otherwise not.
         }
     }
}

这是基本原则。通过这种方式,您可以为所有可能的事件定义一个协议(protocol),但协议(protocol)的实现者只需实现未标记为 optional 的方法。它们是“必需的”。

使用默认方法的协议(protocol)扩展

第三种方法是使用默认方法创建协议(protocol)扩展。

protocol MyEventProtocol: {
  func bar()
}

extension MyEventProtocol {
    func foo() -> Bool {
        return true
    }
}

然后 MyEventProtocol 的实现者不必实现 foo(),因为已经有一个实现。 (这是一种带有可选方法的“伪造”@objc 协议(protocol))如果您向该解决方案添加一些通用机制,您还可以防止大量代码重复。 (协议(protocol)中的泛型在 Swift 2.2 中使用 associatedtype 完成,请参阅其他教程)

关于swift - 我怎样才能使这个 Swift 事件处理程序样板更简洁?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36368656/

有关swift - 我怎样才能使这个 Swift 事件处理程序样板更简洁?的更多相关文章

  1. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  2. ruby-on-rails - 如果我将 ruby​​ 版本 2.5.1 与 rails 版本 2.3.18 一起使用会怎样? - 2

    如果我使用ruby​​版本2.5.1和Rails版本2.3.18会怎样?我有基于rails2.3.18和ruby​​1.9.2p320构建的rails应用程序,我只想升级ruby的版本,而不是rails,这可能吗?我必须面对哪些挑战? 最佳答案 GitHub维护apublicfork它有针对旧Rails版本的分支,有各种变化,它们一直在运行。有一段时间,他们在较新的Ruby版本上运行较旧的Rails版本,而不是最初支持的版本,因此您可能会发现一些关于需要向后移植的有用提示。不过,他们现在已经有几年没有使用2.3了,所以充其量只能让更

  3. ruby-on-rails - 事件管理员日期过滤器日期格式自定义 - 2

    是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s

  4. ruby - 在 Ruby 中构建长字符串的简洁方法 - 2

    在编写Ruby(客户端脚本)时,我看到了三种构建更长字符串的方法,包括行尾,所有这些对我来说“闻起来”有点难看。有没有更干净、更好的方法?变量递增。ifrender_quote?quote="NowthatthereistheTec-9,acrappyspraygunfromSouthMiami."quote+="ThisgunisadvertisedasthemostpopularguninAmericancrime.Doyoubelievethatshit?"quote+="Itactuallysaysthatinthelittlebookthatcomeswithit:themo

  5. 怎样用一台手机做自媒体? - 2

    其实做自媒体的成本并不高,入门只需要一部手机即可!在手机上找视频素材、使用手机剪辑视频、最后使用手机发布视频作品获得收益!方法并不难,今天这期内容就来给粉丝们分享一种小方法,每天稳定收益100-300,抓紧点赞收藏!1、找素材(1)使用手机拍摄自己喜欢的经典段落,使用程序把文案内容提取出来(2)也可以在豆瓣、知乎、微博等网站中找一些自己需要的文案素材(3)把文案进行润色修改,可以加入一些自己的观点(4)视频素材可以使用软件中自带的素材,也可以在素材网站中下载完整版的素材2、文案配音(1)把复制好的文案直接导入小程序中(2)调整音色、音调后一键合成音频即可(3)可以选择自己朗读配音,需要花一点时

  6. ruby-on-rails - 事件记录 : Select max of limit - 2

    我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).

  7. ruby - 这个 ruby​​ 注入(inject)魔术是如何工作的? - 2

    我今天看到了一个ruby​​代码片段。[1,2,3,4,5,6,7].inject(:+)=>28[1,2,3,4,5,6,7].inject(:*)=>5040这里的注入(inject)和之前看到的完全不一样,比如[1,2,3,4,5,6,7].inject{|sum,x|sum+x}请解释一下它是如何工作的? 最佳答案 没有魔法,符号(方法)只是可能的参数之一。这是来自文档:#enum.inject(initial,sym)=>obj#enum.inject(sym)=>obj#enum.inject(initial){|mem

  8. ruby - 我怎样才能只写一次 "Text"并同时检查 path_info 是否包含 'A' ? - 2

    -if!request.path_info.include?'A'%{:id=>'A'}"Text"-else"Text"“文本”写了两次。我怎样才能只写一次并同时检查path_info是否包含“A”? 最佳答案 有两种方法可以做到这一点。使用部分,或使用content_forblock:如果“文本”较长,或者是一个重要的子树,您可以将其提取到一个部分。这会使您的代码变干一点。在给出的示例中,这似乎有点矫枉过正。在这种情况下更好的方法是使用content_forblock,如下所示:-if!request.path_info.inc

  9. ruby-on-rails - 事件管理员和自定义方法 - 2

    这是我在ActiveAdmin中的自定义页面ActiveAdmin.register_page"Settings"doaction_itemdolink_to('Importprojects','settings/importprojects')endcontentdopara"Text"endcontrollerdodefimportprojectssystem"rakedataspider:import_projects_ninja"para"OK"endendend我想做的是,当我单击“导入项目”按钮时,我想在Controller中执行rake任务。但是我无法访问该方法。可能是什

  10. Ruby-vips 图像处理库。有什么好的使用示例吗? - 2

    我对图像处理完全陌生。我对JPEG内部是什么以及它是如何工作一无所知。我想知道,是否可以在某处找到执行以下简单操作的ruby​​代码:打开jpeg文件。遍历每个像素并将其颜色设置为fx绿色。将结果写入另一个文件。我对如何使用ruby​​-vips库实现这一点特别感兴趣https://github.com/ender672/ruby-vips我的目标-学习如何使用ruby​​-vips执行基本的图像处理操作(Gamma校正、亮度、色调……)任何指向比“helloworld”更复杂的工作示例的链接——比如ruby​​-vips的github页面上的链接,我们将不胜感激!如果有ruby​​-

随机推荐