func getLineArrayFromFile(fileName: String) throws -> Array<String>
- The function does not get lines from an arbitrary file, but from a resource file.
- The "get" prefix is usually not used in Objective-C or Swift.
Array<String>
can be shortened to [String]
.
So my suggestion for the function name would be
func linesFromResource(fileName: String) throws -> [String]
let path = NSBundle.mainBundle().pathForResource(fileName, ofType: nil)
if path == nil {
throw ImportError.FileNotFound
}
- As you are obviously using Swift 2, this can be simplified with the
guard
statement. Note that path
is no longer an optional.
- There are pre-defined
NSError
codes which can be used here instead of
defining your own. This also gives better error descriptions for free.
Example:
guard let path = NSBundle.mainBundle().pathForResource(fileName, ofType: nil) else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: [ NSFilePathErrorKey : fileName ])
}
do {
let content = try String(contentsOfFile: path!, encoding: NSUTF8StringEncoding)
lineArray = content.componentsSeparatedByString("\n")
} catch {
throw ImportError.CouldntGetArray
}
You are catching the error and throw your own error code in the failure case,
so the actual error information is lost.
Better just call the try String(..)
and let an error propagate to the
caller of your function:
let content = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
Again, this gives better error descriptions for free.
So the complete method would now look like this (no optionals anymore,
no forced unwrapping with !
):
func linesFromResource(fileName: String) throws -> [String] {
guard let path = NSBundle.mainBundle().pathForResource(fileName, ofType: nil) else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: [ NSFilePathErrorKey : fileName ])
}
let content = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
return content.componentsSeparatedByString("\n")
}
And a typical usage would be
do {
let lines = try linesFromResource("file.txt")
print(lines)
} catch let error as NSError {
print(error.localizedDescription)
} catch let error {
print(error)
}
The reason for the final catch let error
is that it is required that the catch
statements are exhaustive.
Even if we know that the function throws only NSError
s, the compiler doesn't know
that. (There are exceptions but that is a different topic .)
Now to your question how an error should be handled in general, and I would
say: it depends. There are tree different scenarios:
- Loading the strings can fail, and you want to present or log an error message
in that case. Then the above method of throwing and catching an error is appropriate.
- Loading the strings can fail, but the particular reason is of no interest
to the caller. In that case I would change the function to return an optional.
Example:
func linesFromResource(fileName: String) -> [String]? {
guard let path = NSBundle.mainBundle().pathForResource(fileName, ofType: nil) else {
return nil
}
do {
let content = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
return content.componentsSeparatedByString("\n")
} catch {
return nil
}
}
// Usage:
if let lines = linesFromResource("file.txt") {
print(lines)
}
- Finally, if failing to load the strings is a programming error then the function should abort in
the error case, and return the (non-optional) strings otherwise.
As an example, if this function is only used to load strings from fixed compiled-in
resource files which are supposed to exist, then failing to load a file would
be a programming error and should be detected early:
func linesFromResource(fileName: String) -> [String] {
guard let path = NSBundle.mainBundle().pathForResource(fileName, ofType: nil) else {
fatalError("Resource file for \(fileName) not found.")
}
do {
let content = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
return content.componentsSeparatedByString("\n")
} catch let error {
fatalError("Could not load strings from \(path): \(error).")
}
}
let lines = linesFromResource("file.txt")
print(lines)
fatalError()
prints a message before stopping execution of the program, which can
be helpful to locate the programming error. Otherwise you could shorten the
"forced" version to
func linesFromResourceForced(fileName: String) -> [String] {
let path = NSBundle.mainBundle().pathForResource(fileName, ofType: nil)!
let content = try! String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
return content.componentsSeparatedByString("\n")
}
which is the "three liner" that you were looking for in the introduction to
your question.