Литые замыкания / блоки

#objective-c #closures #swift #block

#objective-c #замыкания #swift #блок

Вопрос:

В Objective-C я часто обхожу блоки. Я использую их очень часто для реализации шаблонов, которые помогают избежать хранения данных в переменных экземпляра, тем самым избегая проблем с потоками / синхронизацией.

Например, я назначаю их CAAnimation через -[CAAnimation setValue:forKey:] , чтобы я мог выполнить блок после завершения анимации. (Objective-C может обрабатывать блоки как объекты; вы также можете выполнять [someBlock copy] и [someBlock release] .)

Однако попытка использовать эти шаблоны в Swift вместе с Objective-C кажется очень сложной. (Редактировать: и мы видим, что язык все еще находится в процессе изменения: адаптировали код, чтобы он работал на Xcode6-beta2, предыдущая версия работала на Xcode6-beta1.)

Например, я не могу преобразовать AnyObject обратно в блок / замыкание. Следующее выдает ошибку компилятора:

 override func animationDidStop(anim: CAAnimation!, finished flag: Bool)
{
    let completion : AnyObject! = anim.valueForKey("completionClosure")
    (completion as (@objc_block ()->Void))()
    // Cannot convert the expression's type 'Void' to type '@objc_block () -> Void'
}
  

Я нашел обходной путь, но он довольно уродливый, ИМХО: в моем связующем заголовке у меня есть:

 static inline id blockToObject(void(^block)())
{
    return block;
}

static inline void callBlockAsObject(id block)
{
    ((void(^)())block)();
}
  

И теперь я могу сделать это в Swift:

 func someFunc(completion: (@objc_block ()->Void))
{
    let animation = CAKeyframeAnimation(keyPath: "position")
    animation.delegate = self
    animation.setValue(blockToObject(completion), forKey: "completionClosure")
    …
}

override func animationDidStop(anim: CAAnimation!, finished flag: Bool)
{
    let completion : AnyObject! = anim.valueForKey("completionClosure")
    callBlockAsObject(completion)
}
  

Это работает, но мне понадобилась бы новая функция для каждого типа блока, который я хотел бы использовать, и я взламываю компилятор, который тоже не может быть хорошим.

Итак, есть ли способ решить это чисто быстрым способом?

Комментарии:

1. Строка animation.setValue(completion, forKey: "completionClosure") не компилируется в моем проекте Xcode 6 beta 2.

2. @MartinR: Спасибо, все еще использовал бета-версию 1. Обновили вопрос… становилось все хуже.

Ответ №1:

Как насчет универсального, Block параметризованного с типом функции?

 class Block<T> {
  let f : T
  init (_ f: T) { self.f = f }
}
  

Выделите одно из них; это будет подтип AnyObject и, таким образом, его можно будет назначить в словари и массивы. Это не кажется слишком обременительным, особенно с синтаксисом завершающего замыкания. Используется:

   5> var b1 = Block<() -> ()> { print ("Blocked b1") }
b1: Block<() -> ()> = {
  f = ...
}
  6> b1.f()
Blocked b1
  

и еще один пример, в котором выводится Block тип:

  11> var ar = [Block { (x:Int) in print ("Block: (x)") }]
ar: [Block<(Int) -> ()>] = 1 value {
  [0] = {
    f = ...
  }
}
 12> ar[0].f(111)
Block: 111
  

Комментарии:

1. Извините за поздний ответ. Это действительно работает! let completion = anim.valueForKey("completionClosure") as? Block<()->Void> ; completion?.f() Однако нет способа перейти к Objective-C, но пока это лучшее решение.

2. Это отличное решение. Большое вам спасибо

Ответ №2:

Мне нравится решение Гозонера — обернуть блок в пользовательский класс — но поскольку вы просили фактический «Быстрый способ» для выполнения приведения между блоком и AnyObject, я просто дам ответ на этот вопрос: приведение с помощью unsafeBitCast . (Я предполагаю, что это более или менее то же самое, что у Брайана Чена reinterpretCast , которого больше не существует.)

Итак, в моем собственном коде:

 typealias MyDownloaderCompletionHandler = @objc_block (NSURL!) -> ()
  

Примечание: в Swift 2 это было бы:

 typealias MyDownloaderCompletionHandler = @convention(block) (NSURL!) -> ()
  

Вот приведение в одном направлении:

 // ... cast from block to AnyObject
let ch : MyDownloaderCompletionHandler = // a completion handler closure
let ch2 : AnyObject = unsafeBitCast(ch, AnyObject.self)
  

Вот приведение обратно в другом направлении:

 // ... cast from AnyObject to block
let ch = // the AnyObject
let ch2 = unsafeBitCast(ch, MyDownloaderCompletionHandler.self)
// and now we can call it
ch2(url)
  

Комментарии:

1. Это все еще работает? Я получил сообщение об ошибке «неустранимая ошибка: не удается отключить перенаправление между типами разных размеров». Для ch var у меня было вот что: ch = {()-> Строка в ответ «работает»}. Сработало ли это у вас?

2. @Jai Я не знаю; я не использовал unsafeBitCast для этого долгое время. Я принял решение Гозонера об универсальной оболочке. Обратите внимание, что вам все еще может потребоваться ввести ваш тип обработчика завершения с помощью @convention(block) , чтобы получить управление памятью Objective-C.

3. хорошо, теперь это работает. В вашем коде у вас было let ch = // a completion handler closure , и я не знал, что туда вставить. Итак, у меня было это -> `let ch = {()-> String в ответ «да, это работает»}` Теперь это завершается ошибкой «неустранимая ошибка: не удается отключить передачу между типами разных размеров». Это сработало только тогда, когда я вставил это let ch: MyTypeDefToReturnAString = {()->String in return "yes"} . Кажется, это работает только тогда, когда вы явно вводите свои пользовательские typedefalias. Жаль, что у вас не было этого в вашем коде выше, чтобы избавить меня от головной боли. В любом случае спасибо. 1

4. У меня действительно есть пользовательский typealias в моем коде. Это первая строка моего кода. Видишь это?

5. да, я видел это там, но не здесь let ch = // a completion handler closure .. простите мой нубизм, но я не знал, какой код писать после // a completion handler closure

Ответ №3:

Вот еще одно решение, позволяющее нам выполнять приведение для обмена значениями с Objective-C. Он основан на идее Гозонера о том, чтобы обернуть функцию в класс; разница в том, что наш класс является подклассом NSObject и, таким образом, может предоставить функции Objective-C управление блочной памятью без каких-либо взломов и может быть непосредственно использован как AnyObject и передан Objective-C:

 typealias MyStringExpecter = (String) -> ()
class StringExpecterHolder : NSObject {
    var f : MyStringExpecter! = nil
}
  

Вот как использовать это для переноса функции и передачи туда, где ожидается AnyObject:

 func f (s:String) {println(s)}
let holder = StringExpecterHolder()
holder.f = f

let lay = CALayer()
lay.setValue(holder, forKey:"myFunction")
  

И вот как извлечь функцию позже и вызвать ее:

 let holder2 = lay.valueForKey("myFunction") as StringExpecterHolder
holder2.f("testing")
  

Ответ №4:

Все, что вам нужно сделать, это использовать reinterpretCast для выполнения принудительного приведения.

 (reinterpretCast(completion) as (@objc_block Void -> Void))()
  

из REPL

   1> import Foundation
  2> var block : @objc_block Void -> Void = { println("test")}
block: @objc_block Void -> Void =
  3> var obj = reinterpretCast(block) as AnyObject // this is how to cast block to AnyObject given it have @objc_block attribute
obj: __NSMallocBlock__ = {}
  4> var block2 = reinterpretCast(obj) as (@objc_block Void -> Void)
block2: (@objc_block Void -> Void) =
  5> block2()
test
  6>  
  

Комментарии:

1. атрибут ‘@objc_block’ удален; вместо него следует использовать ‘@convention(блок)’