Swift is accomplishing an amazing feat, it is changing the way we program on Apple devices, introducing many modern paradigms such as functional programming and richer type checking than pure object-oriented languages like Objective-C. Swift aims to help developers avoid bugs by adopting safe programming patterns. However, this will inevitably lead to some artificial traps, which will introduce bugs without the compiler reporting an error. Some of these traps are already mentioned in the Swift book, and some are not. Here are seven traps I encountered in the past year, involving Swift protocol extensions, optional chaining, and functional programming. Protocol extensions: powerful but need to be used with caution The ability for a Swift class to inherit from another class is powerful. Inheritance makes specific relationships between classes clearer and supports fine-grained code sharing. However, in Swift, if it is not a reference type (such as a structure or enumeration), there can be no inheritance relationship. However, a value type can inherit a protocol, and a protocol can inherit another protocol. Although protocols cannot contain other code besides type information, protocol extensions can contain code. In this way, we can use an inheritance tree to achieve code sharing, where the leaves of the tree are value types (structures or enumeration classes), and the interior and root of the tree are protocols and their corresponding extensions. But the implementation of Swift protocol extensions is still new and unexplored territory, and there are still some problems. The code does not always perform as we expect. Because these problems occur when value types (structures and enumerations) are used in combination with protocols, we will use the example of combining classes with protocols to show that there are no traps in this scenario. Surprising things will happen when we switch back to using value types and protocols. Let’s start with our example: classy pizza Suppose there are three pizzas made with two different grains:
We can get the raw materials corresponding to the pizza through the crustGrain attribute
Since most pizzas are made from wheat, this common code can be put into a superclass as the default code to be executed.
These default codes can be overridden to handle other situations (made with corn)
Oops! This code is wrong, and fortunately the compiler caught these errors. Can you spot the error? We forgot to write r in the second crustGain. Swift avoids this error by explicitly annotating override. For example, in this example, we use override, but the misspelled "crustGain" does not actually override any property. Here is the modified code:
Now it compiles and runs successfully:
At the same time, the Pizza superclass allows our code to operate on pizzas without knowing the specific type of Pizza. We can declare a variable of type Pizza.
But the generic type Pizza can still get information about specific types.
Swift's reference type works well in this demo. But if this program involves concurrency and race conditions, we can use value types to avoid them. Let's try Pizza of value type! This is as simple as above, just change the class to struct:
implement
When we use reference types, we achieve this through a superclass Pizza, but for value types this will require a protocol and a protocol extension to work together.
This code can be compiled, let's test it:
As for the execution result, we want to say that cornmeal pizza is not made with Wheat, but the returned result is wrong! Oops! I put
In the code above, crustGrain is written as crustGain, and we forget the r again. However, there is no override keyword for value types to help the compiler find our mistake. Without the help of the compiler, we have to be more careful when writing code. Be careful when overriding protocol properties in protocol extensions Ok, let's correct this spelling mistake:
Re-execution
In order to talk about pizza without worrying about whether it is New York, Chicago, or cornmeal, we can use the Pizza protocol as the type of the variable.
This variable can be used in different types of Pizza
Why does this program show that cornmeal pizza contains wheat? Swift ignores the current actual value of variables when compiling code. The code can only use information known at compile time, and has no knowledge of specific information at run time. The information available to the program at compile time is that pie is of type pizza, and the pizza protocol extension returns wheat, so the override in the CornmealPizza struct has no effect. Although the compiler can warn about potential errors when replacing dynamic dispatch with static dispatch, it actually does not do so. Carelessness here will lead to huge pitfalls. In this case, Swift provides a solution. In addition to defining the crustGrain property in a protocol extension, you can also declare it in the protocol.
Declaring the variable inside the protocol and defining it in the protocol extension tells the compiler to pay attention to the runtime value of the variable pie. A property declaration in a protocol has two different meanings, static or dynamic dispatch, depending on whether the property is defined in a protocol extension. After adding the declaration of the variables in the protocol, the code can run normally:
Each property defined in a protocol extension needs to be declared in the protocol. However, this approach of trying to avoid traps does not always work. Imported protocols are not fully extensible. A framework (library) allows a program to import an interface to use without having to include the relevant implementation. For example, Apple provides us with the necessary frameworks to implement user experience, system facilities and other functions. Swift extensions allow programs to add their own properties to imported classes, structures, enumerations and protocols (the properties here are not storage properties). Properties added through protocol extensions are as if they were originally in the protocol. But in fact, properties defined in protocol extensions are not first-class citizens, because property declarations cannot be added through protocol extensions. We first implement a framework that defines the Pizza protocol and specific types
Import the framework and extend Pizza
As before, static dispatch produces a wrong answer
This is because (same as explained above) the crustGrain property is not declared in the protocol, but only defined in the extension. However, we have no way to modify the framework code, so we cannot solve this problem. Therefore, it is unsafe to add protocol properties of other frameworks through extensions. Do not extend imported protocols to add new properties that may require dynamic dispatch. As just described, the interaction between frameworks and protocol extensions limits the usefulness of protocol extensions, but frameworks are not the only limiting factor. Similarly, type constraints are also not conducive to protocol extensions. Attributes in restricted protocol extensions: declaration is no longer enough Let’s review the Pizza example from earlier:
Let's make a meal out of Pizza. Unfortunately, not every meal is pizza, so we use a generic Meal structure to accommodate various situations. We only need to pass in a parameter to determine the specific type of meal.
The Meal structure inherits from the MealProtocol protocol, which can test whether a meal contains gluten.
To avoid poisoning, the code uses the default value (gluten-free)
Where in Swift provides a way to express constraint protocol extensions. When the main dish is pizza, we know that pizza has a scrustGrain property, so we can access this property. Without the restriction of where, it is unsafe to access scrustGrain when it is not a pizza.
An extension with a Where clause is called a constrained extension. Let's make a delicious cornmeal Pizza
result:
As we demonstrated in the previous section, when dynamic dispatch occurs, we should declare it in the protocol and define it in the protocol extension. But the definition of constraint extensions is always statically dispatched. To prevent bugs caused by accidental static dispatch: If a new property requires dynamic dispatch, avoid using a constrained protocol extension. Using optional chaining with side effects Swift can avoid errors by statically checking whether a variable is nil, and using a convenient shorthand expression, optional chaining, to ignore the possibility of nil. This is also the default behavior of Objective-C. Unfortunately, if the reference being assigned in optional chaining could be null, this could lead to errors. Consider the following code, where a Holder stores an integer:
In the first line of this code, we assign n++ to the attribute of h. In addition to the assignment, the variable n is incremented, which we call a side effect. The final value of variable n depends on whether h is nil. If h is not nil, then the assignment statement is executed, and n++ is also executed. But if h is nil, not only the assignment statement will not be executed, but n++ will also not be executed. In order to avoid surprising results caused by no side effects, we should: Avoid assigning the result of an expression with side effects to the variable on the left side of the equal sign through optional chaining. Functional Programming Pitfalls Thanks to Swift, the benefits of functional programming have been brought to Apple's ecosystem. Functions and closures in Swift are first-class citizens, which are not only convenient and easy to use but also powerful. Unfortunately, there are also some pitfalls that we need to be careful to avoid. For example, inout parameters will silently become invalid in closures. Swift's inout parameter allows a function to accept a parameter and assign it directly, and Swift's closure supports referencing captured functions during execution. These features help us write elegant and readable code, so you may use them together, but this combination may cause problems. We override the crustGrain property to illustrate the use of inout parameters, starting with no closure for simplicity:
To test this function, we pass it a variable as a parameter. After the function returns, the value of this variable should change from Wheat to Corn:
Now let's try returning the closure from the function and then setting the value of the parameter in the closure:
Using this closure only requires one more call:
So far so good, but what if we pass the arguments directly into the getCrustGrainSetter function instead of a closure?
Then try again:
inout parameters become invalid when passed outside the scope of the closure, so: Avoid using in-out parameters in closures This issue is mentioned in the Swift documentation, but there is another related issue worth noting, which is related to the equivalent method of creating closures: currying. When using currying, inout parameters appear inconsistent. In a function that creates and returns a closure, Swift provides a concise syntax for the function’s type and body. Although this currying may seem like a shorthand expression, it can be a bit surprising when used in conjunction with inout parameters. To illustrate this, let’s implement the example above using currying syntax. Instead of declaring the function to return a closure, the function adds a second parameter list after the first, and then omits the explicit closure creation in the function body:
Just like when creating a closure explicitly, we call this function and return a closure:
In the example above, the closure was explicitly created but failed to assign a value to the inout parameter, but this time it succeeds:
This shows that inout parameters work fine in curried functions, but not when explicitly creating closures. Avoid using inout parameters in curried functions, as this code will fail if you later change the currying to explicitly create closures. Summary: Seven things to avoid
|
<<: Value test: Are domain names still linked to brands today?
>>: Best Android Hacking Tools of 2016
Due to the impact of the new coronavirus, many co...
Before the analysis, let me briefly explain my de...
According to data from the World Health Organizat...
This article reviews and compares two test games ...
What is the happiest thing about short video oper...
"If a video website is free, it will lose mo...
Recently, market research firm Gartner released a...
In previous years, on this day, i.e., Double 12, ...
According to Reuters, on July 10, Dutch prosecuto...
[[265442]] The US blockade of China's 5G tech...
In the early morning of January 6, LG released it...
Cough cough cough, heh~TUI! Wait, don’t be so dis...
In the past few months, we have participated in t...
In mystery novels, there is a classic "Snows...
Following the launch of Android 11 Beta1 version ...