神刀安全网

Using Custom Types as Enumeration Case Raw Values in Swift

Using Custom Types as Enumeration Case Raw Values in Swift

Using Custom Types as Enumeration Case Raw Values in Swift

As mentioned in myprevious post, the raw values used with enumerations in Swift are limited to either String , Character , Int , Float or Double values. In certain cases though we might want to use our own custom types. In this post I thought I’d investigate whether it was possible to use custom types of our own as raw values.

Let’s start off with an example. Say we had a Vehicle struct with two properties (a UInt8 to hold the number of wheels the vehicle has and a Bool to indicate whether the vehicle has a motor) – yeah I know, silly example but go with it for now:

struct Vehicle {     let wheelCount: UInt8     let motorized: Bool } 

Also imagine that we wanted to use that struct as the raw value in an enumeration. The bad news is that if we wrote the following it wouldn’t compile:

enum Transportation :Vehicle {     case bike = Vehicle(wheelCount: 2, motorized: false)     case car = Vehicle(wheelCount: 4, motorized: true)     case truck = Vehicle(wheelCount: 6, motorized: true) } 

Ok, so that doesn’t work, but we do know that enumerations in Swift support raw values that are of type String so what if we could represent our Vehicle as a String instead? One string representation might be JSON:

struct Vehicle {     let wheelCount: UInt8     let motorized: Bool       func JSONString() -> String {         var dict : Dictionary<String, AnyObject> = [:]         dict["wheelCount"] = Int(self.wheelCount)         dict["motorized"] = self.motorized           let jsonData = try! NSJSONSerialization.dataWithJSONObject(dict, options:NSJSONWritingOptions())         return NSString(data: jsonData, encoding: NSUTF8StringEncoding) as! String     } }   let v = Vehicle(wheelCount: 2, motorized: false) print(v.JSONString())  // prints - {"wheelCount":2,"motorized":false} 

Ok, good start. That gets us a string representation of our struct but we also need to be able to recreate our Vehicle struct from a given JSON string.

Let’s deal with that next.

Built in to Swift is a protocol called StringLiteralConvertable . As the documentation for this protocol states, the StringLiteralConvertable protocol allows any conforming type to be ”initialised from an arbitrary string” . This sound like exactly what we want.

The StringLiteralConvertable protocol requires any type that conform to the protocol to implement three initialisation methods:

init(stringLiteralvalue: String.StringLiteralType) init(extendedGraphemeClusterLiteralvalue: String.ExtendedGraphemeClusterLiteralType) init(unicodeScalarLiteralvalue: String.UnicodeScalarLiteralType) 

Although this might look a little daunting, is not as complicated as it seems. In reality, String.StringLiteralType , String. ExtendedGraphemeClusterLiteralType and String.UnicodeScalarLiteralType are all actually type aliases for the good old String type. This simplifies things somewhat and means that we can implement a single init function that accepts a String as a parameter and then call that function from these others initialisers as long as we cast their value parameters to a String value e.g.:

init(string value: String) {     // Do initialisation here.... }   init(stringLiteralvalue: String.StringLiteralType) {     self.init(string: String(value)) }   init(extendedGraphemeClusterLiteralvalue: String.ExtendedGraphemeClusterLiteralType) {     self.init(string: String(value)) }   init(unicodeScalarLiteralvalue: String.UnicodeScalarLiteralType) {     self.init(string: String(value)) } 

Ok, we now need to make our Vehicle type actually conform to the protocol:

extension Vehicle :StringLiteralConvertible {       internalinit(string value: String) {         guardlet data = value.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) else {             self.init(wheelCount: 0, motorized: false)             return         }           do {             let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject]             guardlet wheelCount = json["wheelCount"] as? Int else {                 self.init(wheelCount: 0, motorized: false)                 return             }               guardlet motorized = json["motorized"] as? Bool else {                 self.init(wheelCount: 0, motorized: false)                 return             }               self.init(wheelCount: UInt8(wheelCount), motorized: motorized)           } catch let error as NSError {             print("Failed to load: /(error.localizedDescription)")             self.init(wheelCount: 0, motorized: false)         }     }       init(stringLiteralvalue: String.StringLiteralType) {         self.init(string: String(value))       }       init(extendedGraphemeClusterLiteralvalue: String.ExtendedGraphemeClusterLiteralType) {         self.init(string: String(value))     }       init(unicodeScalarLiteralvalue: String.UnicodeScalarLiteralType) {         self.init(string: String(value))     } } 

This works, but isn’t quite as tight as I would like. For example, in the case of an invalid JSON string we have to return a default struct (not ideal – it feels like we should really use an optional return value) and even with a valid JSON string, it works for any JSON string as long as it contains a boolean motorized value and a numeric wheelCount value. For example, given the implementation above the following would work:

let testVehicle = Vehicle(jsonString: "{/"name/":/"Honda/",/"wheelCount/":2,/"motorized/":false}") 

Again, this is not ideal and something I probably need to revisit.

Anyway, putting those issues aside, we can now represent values of type Vehicle as String values and also re-create those Vehicle instances from a corresponding String . At this point then we could potentially re-write our enumeration as:

enum Transportation :String {     case bike = "{/"wheelCount/":2,/"motorized/":false}"     case car = "{/"wheelCount/":4,/"motorized/":true}"     case truck = "{/"wheelCount/":6,/"motorized/":true}" } 

And we could even create enumeration values from instances of our Vehicle struct and reconstitute those values from the enumeration cases raw value:

// From Vehicle to JSON to Enum let v1String = Vehicle(wheelCount: 4, motorized: true).JSONString() let transport1 = Transportation(rawValue: v1String)   // From Enum to JSON to Vehicle if let transport1 = transport1 {     let vehicle1 = Vehicle(jsonString: transport1.rawValue)     print(vehicle1) } 

But it’s still a little clunky. Notice that I’ve had to indicate the enumerations raw values are String values not Vehicles . I think we can do better.

Let’s see what happens if we change the raw value type to Vehicle :

enum Transportation :Vehicle {     case bike = "{/"wheelCount/":2,/"motorized/":false}"     case car = "{/"wheelCount/":4,/"motorized/":true}"     case truck = "{/"wheelCount/":6,/"motorized/":true}" } // RawRepresentable 'init' cannot be synthesized because the raw type 'Vehicle' is not Equatable. 

Ok, so the compilers not happy and it’s giving us a hint about what we need to do to fix it. Essentially we need to make our type conform to the Equatable protocol.

Making our Vehicle type conform to the Equatable protocol is actually pretty simple. The protocol requires two things. Firstly, any conforming type should declare that it is going to conform to the protocol (our Vehicle type doesn’t currently). Secondly the protocol requires any conforming type to define the equality operator ( == ) for that type. These are both things we can cope with:

extension Vehicle :Equatable {}   func ==(lhs : Vehicle, rhs: Vehicle) -> Bool {     return lhs.wheelCount == rhs.wheelCount &&         lhs.motorized == rhs.motorized } 

With that done the compiler is happy!

Let’s test it out. First, let’s define a value of type Vehicle and create a corresponding associated enumeration case:

let transport : Transportation? = Transportation(rawValue: Vehicle(wheelCount: 2, motorized: false)) // transport is now Optional(Transportation.bike) 

Ok, so far so good. Let’s see if we can take an enumeration case and access it’s raw value:

let vehicle: Vehicle = Transportation.bike.rawValue // vehicle is now equal to Vehicle(wheelCount : 2, motorized: false) 

Again, it works! It’s not pretty but it does work. Anyway, I’m going to leave my investigation there for now but if you have any suggestions for improvements, it’d love to hear them.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Using Custom Types as Enumeration Case Raw Values in Swift

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址