0

I ran into some interesting behaviour the other day. Basically I originally wrote a helper function to prevent errors while automatically typecasting a JSON property. It looked like this:

func readData<T>(inout output:T, _ input:AnyObject?, _ throwError:Bool = true) throws
{
    if (input == nil) {
        if (throwError) {
            throw ConvertError.MissingParameter
        }
    }
    else {
        if let inputObject:T = input as? T {
            output = inputObject
        }
        else if (throwError) {
            throw ConvertError.WrongType
        }
    }
}

var myProperty:String
try readData(&myProperty, myJson["data"], true)

This checks the property exists, and that it's the right type. If all goes well, the value inside myProperty changes.

A little while later, I needed to make some changes. I made a class called properties and it has a list of properties inside of it. There are 2 variables of this type of class: originalProperties and modifiedProperties Each of the properties inside these classes are optional variables now, specifically to keep track of what properties the user has changed. Basically it looks like this:

class Properties
{
    var x:Int?
    var y:Int?
}

Now when I run this:

try readData(&originalProperties.x, myJson["x"], false)

It doesn't work anymore. I looked at another question and it explained what was happening. Basically while x still had a value of nil (because it's an optional), I was passing a nil value into my readData function, so the Template type isn't set properly and that's why it fails on the input as? T code.

fortunately, I don't have to have such a creative function anymore. I can just use this:

originalProperties.x= obj["x"] as? Int

But then I'll lose my thrown error functionality if I needed it.

Does anyone have any ideas how I can make sure my Template type gets passed correctly while it still has a value of nil? I even read in another thread that I might have to use some sort of default value closure, but it would be interesting to see if there's a way to work around this.

Community
  • 1
  • 1
Glen
  • 3
  • 2

1 Answers1

0

The main problem here is that the generic T can never itself know if it is of Optional type or not, which makes successful type conversion to T tricky for the cases when T is in fact of type Optional<SomeType>. We could, ourselves, assert that T is an optional type (checking Mirror(reflecting: ...).displayStyle == .Optional etc), however this still doesn't solve conversion to T right off the bat. Instead, we could use another approach, as follows below.

We can work around the problem by creating two readData(...) functions, one taking an optional generic inout parameter, type U?, and the other one taking an implicitly non-optional generic inout parameter U (called only if U? function cannot be used, hence implicitly only called for non-optionals). These two functions, in turn, are minimal and basically only calls your "core" dataReader(..) function, where we've made the adjustment that the inout generic parameter is now explicitly optional, i.e., T?.

enum ConvertError: ErrorType {
    case MissingParameter
    case WrongType
}

/* optional inout parameter */
func readData<U>(inout output: U?, _ input: AnyObject?, _ throwError: Bool = true) throws {
    try readDataCore(&output, input, throwError)
}

/* non-optional inout parameter */
func readData<U>(inout output: U, _ input: AnyObject?, _ throwError: Bool = true) throws {
    var outputOpt : U? = output
    try readDataCore(&outputOpt, input, throwError)
    output = outputOpt!
    /* you could use a guard-throw here for the unwrapping of 'outputOpt', but
       note that 'outputOpt' is initialized with a non-nil value, and that it can
       never become 'nil' in readDataHelper; so "safe" forced unwrapping here.    */
}

/* "core" function */
func readDataCore<T>(inout output: T?, _ input: AnyObject?, _ throwError: Bool = true) throws
{
    if (input == nil) {
        if (throwError) {
            throw ConvertError.MissingParameter
        }
    }
    else {
        if let inputObject: T = input as? T {
            output = inputObject
        }
        else if (throwError) {
            throw ConvertError.WrongType
        }
    }
}

As we try this out, we see that we now get the behaviour we're looking for, even if the argument sent as inout parameter is nil or Optional.


Example 1: using optional inout param with value nil

class Properties
{
    var x:Int?
    var y:Int?
}

var myJson : [String:Int] = ["data":10]
var originalProperties = Properties()

do {
    try readData(&originalProperties.x, myJson["data"], true)
    print("originalProperties.x = \(originalProperties.x ?? 0)")
} catch ConvertError.MissingParameter {
    print("Missing parameter")
} catch ConvertError.WrongType {
    print("Wrong type")
} catch {
    print("Unknown error")
}
/* Prints: 'originalProperties.x = 10', ok! */

// try some non-existing key 'foo'
do {
    try readData(&originalProperties.x, myJson["foo"], true)
    print("originalProperties.x = \(originalProperties.x ?? 0)")
} catch ConvertError.MissingParameter {
    print("Missing parameter")
} catch ConvertError.WrongType {
    print("Wrong type")
} catch {
    print("Unknown error")
}
/* Prints: 'Missing parameter', ok! */

Example 2: using optional inout param with, however, with a non-optional value

class Properties
{
    var x:Int? = 1
    var y:Int? = 1
}

var myJson : [String:Int] = ["data":10]
var originalProperties = Properties()

// ...

/* Same results as for Example 1: 
   'originalProperties.x = 10' and 'Missing parameter' */

Example 3: using non-optional inout parameter

class Properties
{
    var x:Int = 1
    var y:Int = 1
}

var myJson : [String:Int] = ["data":10]
var originalProperties = Properties()

// ...

/* Same results as for Example 1: 
   'originalProperties.x = 10' and 'Missing parameter' */
dfrib
  • 70,367
  • 12
  • 127
  • 192