Swift closure as values in Dictionary

2019-03-27 12:24发布

I'm trying to use an Objective-C library which expects a NSDictionary as its return type. Within the NSDictionary, I can return values of any type, including blocks.

I cannot figure out if there is a way to write an analogous swift method that returns a Dictionary with a closure or a string as a possible value type.

I can't use AnyObject as the value type for the dictionary so this doesn't work:

Dictionary<String,AnyObject> = ["Key":{(value:AnyObject) -> String in return value.description]

I get a Does not conform to protocol error from the compiler regarding the closure and AnyObject.

Is there a higher level type or protocol that both closures and basic types adhere to that I can use as the value type in a Dictionary?

6条回答
爷、活的狠高调
2楼-- · 2019-03-27 12:39

If you still need a workaround, here is one; usage looks like this:

var d : [String : AnyObject] = [:]
d["a"] = Blocks.voidBlockToId({ println("Doing something") })
d["b"] = "Some string"
d["c"] = Blocks.intBlockToId({ i in println("Called with integer: \(i)") })

Blocks.toIntBlock(d["c"])(1)
Blocks.toVoidBlock(d["a"])()
println(d["b"])

Output is:

  1. Called with integer: 1
  2. Doing something
  3. Some string

The Blocks class is defined like this in Objective-C (with corresponding header and bridging header, I won't put those here):

typedef void(^VoidBlock)(void);
typedef void(^IntBlock)(int);

@implementation Blocks

+ (id) voidBlockToId: (VoidBlock) block { return block; }
+ (VoidBlock) toVoidBlock: (id) block { return (VoidBlock)block; }

+ (id) intBlockToId: (IntBlock) block { return block; }
+ (IntBlock) toIntBlock:(id)block { return (IntBlock)block; }

@end

You also need to add a new xyzBlockToId and toXyzBlock method for every new closure-type you want to use. It's pretty ugly, but it works.

查看更多
做个烂人
3楼-- · 2019-03-27 12:46

Could you use an NSMutableDictionary?

Alternatively, this seemed to work for me using your example:

1> import Foundation
2> var myDict: [String: (NSObject) -> String] = ["Key":{(value:NSObject) -> String in return value.description}]
myDict: [String : (NSObject) -> String] = {
  [0] = {
    key = "Key"
    value =
  }
}
3> myDict["Key"]!("Testing")

$R2: String = "Testing"

Hmm, maybe this Swift-Code doesn't really help, because you want to have heterogenous dictionaries.

It's also not possible to put closures into an NSDictionary, it seems (as a closure does not conform to AnyObject).

查看更多
Anthone
4楼-- · 2019-03-27 12:47

Your basic problem is that in Objective-C closures (aka blocks) are represented as NSObject (or more precisely are transparently converted to NSObjects) while in Swift there is no such mapping. This means that closures can not be directly stored in a Dictionary (short of using objective-c glue)

The closest I can come up with is something along the lines of wrapping the value in an enum:

enum DataType {
    case AsString(String)
    case AsClosure((AnyObject)->String)
}

var dict:Dictionary<String,DataType> = [
    "string":DataType.AsString("value"),
    "closure":DataType.AsClosure({(argument:AnyObject) -> String in
        return "value"
        }
    )
]

Which is probably a better solution anyway, because this way you have an explicit typing associated with individual arguments instead of it being implicit using some sort of inflection.

Alternatively, you could only wrap the closure and use a dictionary of type Dictionary<String,Any>.

查看更多
走好不送
5楼-- · 2019-03-27 12:47

There is another type, Any, that object, structs and primitives all conform to but functions do not. There is no general function type, but you can describe a function type as its arguments and return value like this:

Dictionary<String, (AnyObject) -> String>

Function Types

查看更多
甜甜的少女心
6楼-- · 2019-03-27 12:51

A more "generic" solution which should work with Any object, but shown with closures and function references. Drop it into a playground and try it out!

// Wrapper for sticking non-objects in NSDictionary instances
class ObjectWrapper {
    let value: T
    init(_ value: T) {
        self.value = value
    }
}

// convenience to downcast `as! ObjectWrapper` and return its value
func getValueFromObjectWrapper(a: AnyObject) -> T {
    return (a as! ObjectWrapper).value
}

func wrappedObjectsInDictionary() -> NSDictionary {
    var dict = NSMutableDictionary()
    let appendToFoo: (String) -> String = NSString.stringByAppendingString("foo")
    let firstChar: (String) -> Character = { $0[$0.startIndex] }
    dict.setObject(ObjectWrapper(firstChar), forKey: "stringToChar")
    dict.setObject(ObjectWrapper(appendToFoo), forKey: "stringTransformer")
    return dict.copy() as! NSDictionary
}

let dict = wrappedObjectsInDictionary()
let appendToFoo: (String) -> String = getValueFromObjectWrapper(dict["stringTransformer"]!)
let strToChar: (String) -> Character = getValueFromObjectWrapper(dict["stringToChar"]!)

appendToFoo("bar") // "foobar"
strToChar("bar") // "b"
查看更多
女痞
7楼-- · 2019-03-27 13:03

You could also roll your own higher type using an enum. You need the dictionary values to be either strings or functions which return strings, so define a type to represent that:

enum MyDictVal {
  case ValString(String)
  case ValFunc(AnyObject -> String)
}

Then, you can put it in a dictionary:

let d: Dictionary<String, MyDictVal> = [
  "a": .ValString("a")
, "b": .ValFunc({ (value) in value.description })
]

Then you'll need to process the dictionary values using pattern matching:

switch d["b"] {
  case .ValString(let s):
    ...
  case .ValFunc(let f):
    ...
}
查看更多
登录 后发表回答