After perusing through forums and Swift documentation (not completely, I admit), it appears that instead of try-catch mechanisms, in Swift we are encouraged to write code that is more safe from exceptions. In light of that, I have a question about a sample API, and would like to learn how to more safely handle this situation:
For example, I can create the following class using the NSDecimalNumberHandler:
class MathWhiz {
init() {
let defaultBehavior: NSDecimalNumberHandler =
NSDecimalNumberHandler.defaultDecimalNumberHandler()
}
func add(op1: String, op2: String) ->NSDecimalNumber {
return NSDecimalNumber.decimalNumberWithString(op1).decimalNumberByAdding(NSDecimalNumber.decimalNumberWithString(op2))
}
}
If I use the following, I get a number:
let brain = MathWhiz()
brain.add("1", op2: "1e127")
However, if I cause an overflow exception,:
brain.add("1", op2: "1e128")
I will crash the program as expected.
So, my question is, the API raises exceptions, but I don't handle them here. There are other posts out there of people pointing out that Swift does not have exception handling, but this question is seeking a nice way to handle this problem in the way that the language creators were thinking it should be done.
Is there a recommended way to handle this without having to write my own code to check for overflow, underflow, loss of precision, ect...? I am wanting the NSDecimalNumberHandler to do that for me.
If you are designing a function (or method) in Swift, you have at least 3 choices for dealing with errors:
Choice 1: Return an Optional Type
If your function might fail, and this happens on a regular basis, then consider returning an optional type variable. For example, in your case, your method add
could return an NSDecimalNumber?
instead of a plain NSDecimalNumber
. In that case, your method would check for everything that could go wrong, and return nil
in those situations. Overflow and underflow would return nil
, and all other cases would return an NSDecimalNumber
. The callers would have to check for and unwrap the optional NSDecimalNumber like this:
let brain = MathWhiz()
if let sum = brain.add("1", op2: "1e127") {
println("the result was \(sum)")
} else
println("something went wrong with MathWhiz add")
}
Choice 2: Return an Enumerated Type
If you want to return more information about the thing that went wrong, you could create an enumerated type with a value for each error and one for success that embeds the answer. For example, you could do:
enum MathWhizResult {
case Overflow
case Underflow
case Success(NSDecimalNumber)
}
Then add would be defined to return MathWhizResult
:
func add(op1: String, op2: String) -> MathWhizResult
In the case of an error, add
would return .Overflow
or .Underflow
. In the case of success, add
would return Success(result)
. The caller would have to check the enumeration and unpack the result. A switch
could be used for this:
switch (brain.add("1", op2: "1e128")) {
case .Overflow
println("darn, it overflowed")
case .Underflow
println("underflow condition happened")
case .Success(let answer)
println("the result was \(answer)"
}
Choice 3: Choose not to handle errors explicitly
Unpacking the result in the first two choices might be too much overhead for an error that is very rare. You could chose to just return a result, and let the caller deal with the possibility of an underflow or overflow condition. In that case, they would have to check for these conditions themselves before calling add
. The benefit is, it they know that their program will never cause an underflow or overflow (because they are dealing with single digit numbers for instance), they are not burdened with unpacking the result.
I created a small app to demonstrate how you could do this with NSDecimalNumbers
. I created a Single View Application
in Xcode
. In the ViewController
in the StoryBoard
I added 3 TextField
s (one each for operand 1, operand 2, and the result) and a Button
that I labelled +
.
ViewController.swift
import UIKit
class ViewController: UIViewController {
@IBOutlet var operand1 : UITextField!
@IBOutlet var operand2 : UITextField!
@IBOutlet var result : UITextField!
var brain = MathWhiz()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func addButton(sender : UIButton) {
var op1 = operand1.text
var op2 = operand2.text
// Perform the add with the contents of the operand fields.
// Print the answer, or "No Result" if add returns nil.
if let answer = brain.add(op1, op2: op2)?.description {
result.text = answer
} else {
result.text = "No Result"
}
}
}
MathWhiz.swift
import UIKit
// Declare that we implement NSDecimalNumberBehaviors so that we can handle
// exceptions without them being raised.
class MathWhiz: NSDecimalNumberBehaviors {
var badException = false
// Required function of NSDecimalNumberBehaviors protocol
func roundingMode() -> NSRoundingMode {
return .RoundPlain
}
// Required function of NSDecimalNumberBehaviors protocol
func scale() -> CShort {
return CShort(NSDecimalNoScale)
}
// Required function of NSDecimalNumberBehaviors protocol
// Here we process the exceptions
func exceptionDuringOperation(operation: Selector, error: NSCalculationError, leftOperand: NSDecimalNumber, rightOperand: NSDecimalNumber) -> NSDecimalNumber? {
var errorstr = ""
switch(error) {
case .NoError:
errorstr = "NoError"
case .LossOfPrecision:
errorstr = "LossOfPrecision"
case .Underflow:
errorstr = "Underflow"
badException = true
case .Overflow:
errorstr = "Overflow"
badException = true
case .DivideByZero:
errorstr = "DivideByZero"
badException = true
}
println("Exception called for operation \(operation) -> \(errorstr)")
return nil
}
// Add two numbers represented by the strings op1 and op2. Return nil
// if a bad exception occurs.
func add(op1: String, op2: String) -> NSDecimalNumber? {
let dn1 = NSDecimalNumber(string: op1)
let dn2 = NSDecimalNumber(string: op2)
// Init badException to false. It will be set to true if an
// overflow, underflow, or divide by zero exception occur.
badException = false
// Add the NSDecimalNumbers, passing ourselves as the implementor
// of the NSDecimalNumbersBehaviors protocol.
let dn3 = dn1.decimalNumberByAdding(dn2, withBehavior: self)
// Return nil if a bad exception happened, otherwise return the result
// of the add.
return badException ? nil : dn3
}
}
Well, you're using an ObjC API. So just handle the exceptions inside ObjC. Write an ObjC class that handles the exceptions and returns values for Swift code to consume.
One possibility would be to write MathWhiz
as an ObjC class and return an inout NSError
parameter (ie do it the way Core Data does, take an **NSError
) and fill it with the proper value when you hit a recoverable error. Then you can eat exceptions from NSDecimalNumber
and convert them into NSError
values.
You could also write an entire NSDecimalNumber
wrapper for Swift consumption that you then use in place of NSDecimalNumber
in your Swift code. Perhaps you could overload operator +
and its siblings for this class, and then work out how to represent the various possible errors without exceptions.
update answer for swift 2.0
As you mentioned, now Swift 2.0 support with try
, throw
, and catch
keywords.
here is official announcement.
Error handling model: The new error handling model in Swift 2.0 will
instantly feel natural, with familiar try, throw, and catch keywords.
Best of all, it was designed to work perfectly with the Apple SDKs and
NSError. In fact, NSError conforms to a Swift’s ErrorType. You’ll
definitely want to watch the WWDC session on What’s New in Swift to
hear more about it.
e.g
func loadData() throws { }
func test() {
do {
try loadData()
} catch {
print(error)
}
}