Swift optional value type processing method details

  • 2020-06-12 10:44:43
  • OfStack

preface

When we use ES3en-ES4en to represent string information, we can write in the following way.


NSString *str = @" In the autumn of hate snow "; 
str = nil; 

Because ES9en-ES10en is a weakly typed language, str here can be either a concrete string or nil. This is not the case with Swift, because Swift is a type-safe language, and a variable of type String cannot be both a concrete string and nil (more strictly, the contents of type String can only be strings). Thus, in Swift there is the concept of optional types. (The concept is "borrowed" from other programming languages, such as C#, but referred to as nullable in C#).

Optionals is very strange at first sight. When I first saw it, I was thinking... What the hell is this... But if you think about it, the introduction of the optional Optionals type also brings convenience.

The optional value (optionals) is arguably one of the most important features of the swift language, and the biggest difference from other languages such as ES29en-ES30en. By enforcing where nil is likely to occur, we can write more predictable and robust code.

However, there are times when optional values can lead to awkward situations, especially as you as a developer know (and even guess) that a particular variable is always non-null (ES34en-ES35en), even if it is an optional type. For example, when we process a view in a view controller:


class TableViewController: UIViewController {
 var tableView: UITableView?
 override func viewDidLoad() {
  super.viewDidLoad()
  tableView = UITableView(frame: view.bounds)
  view.addSubview(tableView!)
 }
 func viewModelDidUpdate(_ viewModel: ViewModel) {
  tableView?.reloadData()
 }
}

This is an area of intense debate for many Swift programmers, as much as the use of tabs and spaces. Some people would say,

Since it is an optional value, you should always unpack using if let or guard let.

Others, however, take the opposite approach and say:

Since you know this variable will not be nil when used, use! How nice to force unpacking. Crashing is better than leaving your program in an unknown state.

Essentially, we're talking about defensive programming (defensive programming) or not. Are we trying to get the program to recover from an unknown state or are we simply giving it up and crashing?

If I have to give one answer to this question, I prefer the latter. Unknown states are really hard to track bug, which leads to a lot of logic that you don't want to execute, and defensive programming makes your code hard to track and hard to track when things go wrong.

But I don't like to give a two-for-one answer. Instead, we can look for techniques to solve the problems mentioned above in more subtle ways.

Is it really optional?

Variables and attributes of optional types that are actually required by code logic are actually a manifestation of architectural flaws. If you really need it somewhere, but it's not there, leaving your code logic in an unknown state, then it shouldn't be optional.

Of course, optional values are hard to avoid in certain scenarios (especially when interacting with a particular system, API), but for the most part, there are techniques we can use to avoid optional values.

lazy is better than non-optional optional values

The value of some properties needs to be regenerated after the parent class is created (such as those views in the view controller that should be created in the loadView() or viewDidLoad() method), and the way to avoid its optional type for this property is to use the lazy property. An lazy property can be of non-optional type and is not required in the initializer method of its parent class. It is created the first time it is fetched.

Let's change the above code by using lazy to modify the tableView property:


class TableViewController: UIViewController {
 lazy var tableView = UITableView()
 override func viewDidLoad() {
  super.viewDidLoad()
  tableView.frame = view.bounds
  view.addSubview(tableView)
 }
 func viewModelDidUpdate(_ viewModel: ViewModel) {
  tableView.reloadData()
 }
}

In this way, there is no alternative value, and there is no unknown state.

Proper dependency management is better than optional values that are not optional

Alternative value types Another common scenario is to break cyclic dependencies (circular dependencies). Sometimes you fall into the situation where A depends on B and B depends on A as follows:


class UserManager {
 private weak var commentManager: CommentManager?
 func userDidPostComment(_ comment: Comment) {
  user.totalNumberOfComments += 1
 }
 func logOutCurrentUser() {
  user.logOut()
  commentManager?.clearCache()
 }
}
class CommentManager {
 private weak var userManager: UserManager?
 func composer(_ composer: CommentComposer
     didPostComment comment: Comment) {
  userManager?.userDidPostComment(comment)
  handle(comment)
 }
 func clearCache() {
  cache.clear()
 }
}

As we can see from the above code, there is a circular dependency problem between UserManager and CommentManager. Neither of them can assume ownership of the other, but they both depend on each other in their respective code logic. So bug is very easy to produce here.

To solve the above problem, we created an CommentComposer to act as a coordinator, responsible for notifying UserManager and CommentManager2 people that a comment was generated.


class CommentComposer {
 private let commentManager: CommentManager
 private let userManager: UserManager
 private lazy var textView = UITextView()
 init(commentManager: CommentManager,
   userManager: UserManager) {
  self.commentManager = commentManager
  self.userManager = userManager
 }
 func postComment() {
  let comment = Comment(text: textView.text)
  commentManager.handle(comment)
  userManager.userDidPostComment(comment)
 }
}

In this form, UserManager can strongly hold CommentManager without creating any dependency cycles.


class UserManager {
 private let commentManager: CommentManager
 init(commentManager: CommentManager) {
  self.commentManager = commentManager
 }
 func userDidPostComment(_ comment: Comment) {
  user.totalNumberOfComments += 1
 }
}

Once again we removed all the optional types and the code was more predictable. .

The Graceful crash (Crashing gracefully)

Through the above examples, we eliminated the uncertainty by making a few adjustments to the code to remove the optional types. Sometimes, however, it is not possible to remove an optional type. For example, if you are loading an JSON file that contains a local configuration item for your App, the operation itself 1 is bound to fail and we need to add error handling.

Continuing with the above scenario, continuing to execute the code if the configuration file fails to load will leave your app in an unknown state, in which case it is best to crash. In this way, we will get a crash log and hopefully the problem will be addressed by our testers and QA long before the user is aware of it.

So, how we break down... The easiest way is to add! The operator, which forces unpacking for this optional value, crashes when it is nil:


let configuration = loadConfiguration()!

Although this method is relatively simple, it has a big problem. Once the code crashes, we can only get 1 error message:


fatal error: unexpectedly found nil while unwrapping an Optional value

This error message doesn't tell us why or where the error occurred, and it doesn't give us any clues to solve it. At this point, we can use the guard keyword, combined with the preconditionFailure() function, to give a custom message when the program exits.


guard let configuration = loadConfiguration() else {
 preconditionFailure("Configuration couldn't be loaded. " +
      "Verify that Config.JSON is valid.")
}

When the above code crashes, we get more and more effective error messages:


fatal error: Configuration couldn't be loaded. Verify that Config.JSON is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17

In this way, we now have a clearer way to solve the problem and know exactly which unknown problem occurred in our code.

Introducing Require library

The guard-ES149en-ES150en scheme above is still a bit verbose and does make our code a bit more difficult to navigate. We really don't want to spend a lot of space in our code, we want to focus more on the logic of our code.

My solution is to use Require. It simply adds a simple require() method to an optional value, but it makes the place of the call cleaner. To handle the code above loading the JSON file this way:


class TableViewController: UIViewController {
 var tableView: UITableView?
 override func viewDidLoad() {
  super.viewDidLoad()
  tableView = UITableView(frame: view.bounds)
  view.addSubview(tableView!)
 }
 func viewModelDidUpdate(_ viewModel: ViewModel) {
  tableView?.reloadData()
 }
}
0

When an exception occurs, the following error message is given:


class TableViewController: UIViewController {
 var tableView: UITableView?
 override func viewDidLoad() {
  super.viewDidLoad()
  tableView = UITableView(frame: view.bounds)
  view.addSubview(tableView!)
 }
 func viewModelDidUpdate(_ viewModel: ViewModel) {
  tableView?.reloadData()
 }
}
1

Another advantage of Require is that it throws exceptions as well as calling preconditionFailure() method 1, enabling the exception escalation tools to capture metadata when the exception occurs.

Require is now open source on Github if you want to use it in your own code

conclusion

So, to summarize, when dealing with optional values that are not optional in Swift, I have a few nice tips of my own:

The lazy attribute is better than an optional value that is not optional Proper dependency management is better than optional values that are not optional Elegant crash when you use optional values that are not optional

Related articles: