Swift类中的函数作为参数

5

我是一名Swift初学者,请温柔点...

我在给函数赋值参数时遇到了麻烦。

我已经定义了这个结构体:

struct dispatchItem {
   let description: String
   let f: ()->Void

   init(description: String, f: @escaping ()->()) {
      self.description = description
      self.f = f
   }
}

我在一个名为 MasterDispatchController 的类中使用它,如下所示:

class MasterDispatchController: UITableViewController {

   let dispatchItems = [
      dispatchItem(description: "Static Table", f: testStaticTable),
      dispatchItem(description: "Editable Table", f: testEditableTable)
   ]

    func testEditableTable() {
        //some code
    }

    func testStaticTable() {
        //some code
    }

然后,我在代码中有一个表格视图,可以分发给被单击的任何函数(不仅仅是上面代码中展示的两个函数,但这不重要),就像这样:

   override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      dispatchItems[indexPath.row].f()
   }

所以...编译器对此不满意。在我定义dispatchItems的let语句时,它说:

无法将'(MasterDispatchController) -> () -> ()'类型的值转换为预期的参数类型'() -> ()'

我想...好吧...我不确定我完全理解这个错误,但看起来像是编译器想知道回调函数将来自哪个类。我能理解它为什么需要这个信息。因此,我有点盲目地按照编译器给我的模式进行更改,将我的struct更改为:

struct dispatchItem {
   let description: String
   let f: (MasterDispatchController)->()->Void

   init(description: String, f: @escaping (MasterDispatchController)->()->()) {
      self.description = description
      self.f = f
   }
}

很好,编译器很高兴,但是当我尝试使用dispatchItems[indexPath.row].f()调用函数时,它会显示:

缺少第1个参数

这个函数没有参数,所以我感到困惑了...

我想也许它正在询问我有关对象实例的问题,这在某种程度上是有意义的... 在我的示例中,这将是"self",因此我尝试了dispatchItems[indexPath.row].f(self),但是然后我得到了一个错误:

表达式解析为未使用的函数

所以我有点卡住了。

如果这是一个愚蠢的问题,请原谅。谢谢您的帮助。


所有这些代码背后的想法是什么?因为你可能做错了。 - JuicyFruit
这是一个用于我学习iOS和Swift的测试应用程序。我有一个表视图,其中填充了大量小测试。例如,在一个小测试中,我制作了一个显示自定义绘图视图的新视图控制器。在另一个测试中,我试图创建一个收藏视图。基本上,我正在编写一个大型应用程序,尝试许多不同的东西。这段代码是调度程序,用于调用所有这些小测试。在运行时,当我点击表元素时,我会运行那个小测试。 - Erik Westwig
如果你刚开始测试,那么只需编写 switch indexPath.row {case 0: callFunc1() case1: callFunc2() default: CallSomeDefault()} 并以这种方式调用所需的函数。因为将函数传递给模型是错误的。 - JuicyFruit
那就是我以前在做的事情...我对于有多个开关语句感到烦恼。其中一个用于显示表格文本,另一个用于分发函数。我希望将所有逻辑放在一个地方(这就是为什么有dispatchItems数组的原因)。 - Erik Westwig
问题到底是什么?你想要知道/做什么? - matt
显示剩余3条评论
3个回答

2
问题在于您正在尝试在实例属性的初始化器中引用实例方法testStaticTabletestEditableTable,此时self还没有完全初始化。因此编译器无法使用self作为隐含参数部分应用这些方法,只能提供其柯里化版本——类型为(MasterDispatchController) -> () -> ()
因此,您可能会想将dispatchItems属性标记为lazy,以便在访问该属性时第一次运行属性初始化器,此时self已经完全初始化。
class MasterDispatchController : UITableViewController {

    lazy private(set) var dispatchItems: [DispatchItem] = [
        DispatchItem(description: "Static Table", f: self.testStaticTable),
        DispatchItem(description: "Editable Table", f: self.testEditableTable)
    ]

    // ...
}

请注意,我将您的结构体重命名以符合 Swift 命名惯例。
现在它可以编译,因为您现在可以引用部分应用的方法(即类型为 () -> Void),并且可以像下面这样调用它们:
dispatchItems[indexPath.row].f()

然而,现在你有一个保留循环,因为你正在将闭包存储在 self 上,这些闭包强烈捕获了 self。这是因为当作为一个值使用时,self.someInstanceMethod 解析为一个部分应用的闭包,它强烈地捕获了 self。
解决这个问题的一种方法,你已经接近实现了,就是改为使用方法的柯里化版本 - 这些版本不会强烈捕获 self,而是必须应用于给定的实例才能操作。
struct DispatchItem<Target> {

    let description: String
    let f: (Target) -> () -> Void

    init(description: String, f: @escaping (Target) -> () -> Void) {
        self.description = description
        self.f = f
    }
}

class MasterDispatchController : UITableViewController {

    let dispatchItems = [
        DispatchItem(description: "Static Table", f: testStaticTable),
        DispatchItem(description: "Editable Table", f: testEditableTable)
    ]

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        dispatchItems[indexPath.row].f(self)()
    }

    func testEditableTable() {}
    func testStaticTable() {}
}

这些函数现在以给定的MasterDispatchController实例作为参数,并返回正确的实例方法以供调用。因此,您需要首先使用self应用它们,通过f(self)来获取要调用的实例方法,然后使用()调用结果函数。
尽管不断使用self可能会很麻烦(或者您甚至可能无法访问self),但更一般的解决方案是将self存储为DispatchItem上的weak属性,同时存储柯里化函数,然后可以“按需”应用它们。
struct DispatchItem<Target : AnyObject> {

    let description: String

    private let _action: (Target) -> () -> Void
    weak var target: Target?

    init(description: String, target: Target, action: @escaping (Target) -> () -> Void) {
        self.description = description
        self._action = action
    }

    func action() {
        // if we still have a reference to the target (it hasn't been deallocated),
        // get the reference, and pass it into _action, giving us the instance
        // method to call, which we then do with ().
        if let target = target {
            _action(target)()
        }
    }
}

class MasterDispatchController : UITableViewController {

    // note that we've made the property lazy again so we can access 'self' when
    // the property is first accessed, after it has been fully initialised.
    lazy private(set) var dispatchItems: [DispatchItem<MasterDispatchController>] = [
        DispatchItem(description: "Static Table", target: self, action: testStaticTable),
        DispatchItem(description: "Editable Table", target: self, action: testEditableTable)
    ]

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        dispatchItems[indexPath.row].action()
    }

    func testEditableTable() {}
    func testStaticTable() {}
}

这确保了您没有保留循环,因为DispatchItem没有对self持有强引用。

当然,您可以使用unowned引用来引用self,例如在这个Q&A中所示。但是,只有在您可以保证您的DispatchItem实例不会超出self的生命周期时才应该这样做(您可能希望将dispatchItems设为private属性)。

讲解得非常清晰明了。非常感谢! - Erik Westwig
很高兴能帮助 @ErikWestwig - Hamish

1
这里的问题在于,Swift在底层处理类方法和函数时有所不同。类方法会得到一个隐藏的self参数(类似于Python),它允许它们知道在哪个类实例上被调用。这就是为什么即使你将testEditableCode声明为() -> (),实际函数的类型为(MasterDispatchController) -> () -> ()。它需要知道它在哪个对象实例上被调用。
做你想做的正确方法是创建一个闭包来调用正确的方法,像这样:
class MasterDispatchController: UITableViewController {

   let dispatchItems = [
      dispatchItem(description: "Static Table", f: {() in
        self.testStaticTable()
      }),
      dispatchItem(description: "Editable Table", f: {() in
        self.testEditableTable()
      })
   ]

    func testEditableTable() {
        //some code
    }

    func testStaticTable() {
        //some code
    }

如果您熟悉JavaScript,那么{() in ...code...}表示与function() { ...code... }或Python中的lambda: ...code...相同。请注意保留HTML标签。

1
你说的 "You can't pass self to your methods either" 是什么意思?如果有一个 (MasterDispatchController) -> () -> () 的函数,只要 selfMasterDispatchController 实例,你完全可以将其应用于 self。你只需要调用结果函数,例如 f(self)() - Hamish
我不确定那是否有效,但我现在无法测试。我将删除关于无法传递self的部分。我仍然认为在这种情况下使用闭包会更符合惯用法/可读性。 - Pedro Castilho
@Hamish - 但我不知道那个语法!我只写了f(self)。我没有写f(self)()...顺便说一句,当我这样做时,它起作用了! - Erik Westwig
我承认我不知道类方法的行为就像柯里化函数,但这确实解释了你的错误。当你执行f(self)时,它返回了一个类型为() -> ()的函数,但你从未调用它,这就是编译器抱怨的原因。 - Pedro Castilho
谢谢你的所有帮助。f(self)()是我缺少的东西。现在这很清楚了。 - Erik Westwig

0

这是实现你想要的方式。

使用你想要的参数实现协议:

protocol Testable {
    func perfromAction()
    var description: String { get set }
    weak var viewController: YourViewController? { get set } //lets assume it is fine for testing
}

像这样访问您的UIViewController并不完全正确,但现在还可以。您可以访问标签、转场等。

为您想要的每个测试创建一个Class

class TestA: Testable {
    var description: String
    weak var viewController: YourViewController?
    func perfromAction() {
        print(description) //do something
        viewController?.testCallback(description: description) //accessing your UIViewController
    }
    init(viewController: YourViewController, description: String) {
        self.viewController = viewController
        self.description = description
    }
}

class TestB: Testable {
    var description: String
    weak var viewController: YourViewController?
    func perfromAction() {
        print(description) //do something
        viewController?.testCallback(description: description) //accessing your UIViewController
    }
    init(viewController: YourViewController, description: String) {
        self.viewController = viewController
        self.description = description
    }
}

你可以为每个Class添加一些自定义参数,但协议中的这3个是必需的。

然后你的UIViewController会像这样

class YourViewController: UIViewController {
    var arrayOfTest: [Testable] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        arrayOfTest.append(TestA(viewController: self, description: "testA"))
        arrayOfTest.append(TestB(viewController: self, description: "testB"))
        arrayOfTest[0].perfromAction()
        arrayOfTest[1].perfromAction()
    }

    func testCallback(description: String) {
        print("I am called from \(description)")
    }
}

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