"Swift is a type safe language. Swift helps you to be clear about the types of values your code can work with. If part of your code expects a String, type safety prevents you from passing it an Int by mistake. This restriction enables you to catch and fix errors as early as possible in the development process." (Apple Developer)
The problem
In the following example the type Dictionary<String, NSObject> is inferred and this allows us an incredible amount of freedom but it is not type safe.var dict = ["one":1,"two":"two","three":3,"four":4] let cc = dict["one"] // 1 dict["three"] = "three"
The reason it is not type safe is because we can change values however we like as long as ultimately the value's type can be cast to a foundation object. This means that we never know the type that any given key is going to return, which in turn means the compiler cannot help us with regard to ensuring that any methods or functions called with the values will work at run time.
The solution
It is possible to solve this problem by ensuring we use Swift's native types and don't lean on NSObject or AnyObject to circumvent coding obstacles. But sometimes our starting point isn't a Swift type and we are presented with an object by Cocoa (or Objective-C). So how do we resolve this?
Well suppose we knew the Dictionary we were receiving contained strings and numbers, then we could very quickly create an instance in Swift that split the single dictionary into two. The code for which is provided below.
struct TwinTypes { private var stringDict = Dictionary<String,String>() private var numDict = Dictionary<String,NSNumber>() init (dict:Dictionary<String,AnyObject>) { for (k,v) in dict { if let v = v as? String { stringDict[k] = v } else if let v = v as? NSNumber { numDict[k] = v } } } subscript (key:String) -> String? { get { return stringDict[key] } set(newValue) { stringDict[key] = newValue } } subscript (key:String) -> NSNumber? { get { return numDict[key] } set(newValue) { numDict[key] = newValue } } } var twins = TwinTypes(dict: ["one":1,"two":"two","three":3,"four":4]) var s:String? = twins["one"] var n:NSNumber? = twins["three"] twins["three"] = 6
Now if we are expecting a String then we can simply tell the compiler our expectations and if the value in the dictionary is not a string then our value will be nil and we will never be caught out, and the compiler will help us in our coding quest, and we will live happily ever after.
The problem with the solution
There are a couple "problems" with the solution. First is that it intertwines us in the world of optionals and the way we have to predict the outcome of our calls, second we have more code than before. And while we could probably do ingenious things to strip the code back, optionals we are stuck with because they are part and parcel of being type safe, for as Apple tells us:
"Optionals are an example of the fact that Swift is a type safe language." (Apple Developer)
So rather than being problems optionals are part of the solution and any Dictionary no matter which types it contains will always return an optional because it is unknown whether a key is present in a dictionary and whether it has a value (until it seeks it out). The difference here is that the compiler cannot infer without our help, but it isn't such a burden given the safety it provides.
Note: with the Twitter API, for example, there are certain values that are optional and will at times be null when no value is present. And we can imagine this to be the case in other instances too, meaning that optionals might always play some part.
Of course, every solution has its compromises and your use case will impact on how you deal with receiving dictionaries and arrays that are not strongly typed. But hopefully some of that which at first glance appears onerous can be accepted and perhaps even adored for the added security it provides.
Optionals are not the only option
Dictionaries are not our only option, a dictionary for which we knew all the keys that were going to be present could very easily be transformed into a Swift struct and it could be the case that no optionals were involved whatsoever. There is in theory no problem with this but for large dictionaries we would be hard coding a data structure into an app, and every time the received data changed the code of the app would need to change, whereas by simply splitting the dictionary into two or more pieces there's a greater freedom and flexibility, as well as being far more reusable.Note: with the Twitter API, for example, there are certain values that are optional and will at times be null when no value is present. And we can imagine this to be the case in other instances too, meaning that optionals might always play some part.
Of course, every solution has its compromises and your use case will impact on how you deal with receiving dictionaries and arrays that are not strongly typed. But hopefully some of that which at first glance appears onerous can be accepted and perhaps even adored for the added security it provides.
Finishing up
To conclude I want to write a simple method, or rather computed property, for reconstituting the original dictionary.
struct TwinTypes { private var stringDict = Dictionary<String,String>() private var numDict = Dictionary<String,NSNumber>() var dictionary:Dictionary<String,NSObject> { var dictionary = Dictionary<String,NSObject>() for (k,v) in stringDict { dictionary[k] = v as NSObject } for (k,v) in numDict { dictionary[k] = v as NSObject } return dictionary } init (dict:Dictionary<String,AnyObject>) { for (k,v) in dict { if let v = v as? String { stringDict[k] = v } else if let v = v as? NSNumber { numDict[k] = v } } } subscript (key:String) -> String? { get { return stringDict[key] } set(newValue) { numDict.removeValueForKey(key) stringDict[key] = newValue } } subscript (key:String) -> NSNumber? { get { return numDict[key] } set(newValue) { stringDict.removeValueForKey(key) numDict[key] = newValue } } }Now all we need to do in order to retrieve the reconstituted dictionary is to do the following:
twins.dictionaryThe dictionary property is read-only, so cannot be changed directly but values can be accessed directly using the regular subscript approach. (Note: The dictionary can also be transformed into JSON data using the NSJSONSerialization class, because it conforms to the requirement of that class.)
An end to conflict
For good measure I made sure in the above code that there is never a conflict of keys and values between the two dictionaries. This means that we can do exactly the same thing as we did with the dictionary that wasn't type safe, switching type from string to number, but that we do so in a type safe manner thanks to the power of optionals. For example:twins["three"] = 6 twins["three"] = "three" let num:NSNumber? = twins["three"] // nil
Expanding our options
To mimic the unsafe approach isn't our only option. It is possible that we would choose the setting of a subscript as an opportunity to make sure that no type changes occur instead. This could be done in at least two ways: one would be to simply refuse to make changes of the wrong type,subscript (key:String) -> String? { get { return stringDict[key] } set(newValue) { if numDict[key] == nil { stringDict[key] = newValue } } } subscript (key:String) -> NSNumber? { get { return numDict[key] } set(newValue) { if stringDict[key] == nil { numDict[key] = newValue } } }and the other would be to transform the assigned value into a value of the correct type. (The latter being a solution that can be used to circumvent the need for optionals should this be desired, but which is more suited to numbers needing to be changed into strings than vice versa.)
A lockdown approach
One other scenario that it might be necessary to control is the adding of new keys and values to a dictionary. It might be the case that the dictionary should not contain any additions (only the changing of values in line with their original type). But again this is a simple task:subscript (key:String) -> String? { get { return stringDict[key] } set(newValue) { if numDict[key] == nil && stringDict[key] != nil { stringDict[key] = newValue } } } subscript (key:String) -> NSNumber? { get { return numDict[key] } set(newValue) { if stringDict[key] == nil && numDict[key] != nil { numDict[key] = newValue } } }
Comments
Post a Comment