Learning Swift Again
I have used Swift for 6 years, from the first version to the version 5.5 now. I had learned Objective-C just for four months when I first learned Swift. At that time, I had written an application by Objective-C and then I decide to rewrite the application by Swift. It’s a very painful experience for me, especially the Optional Value, I had to unwrap the optional values again and again before using them. But I love its new concise grammar compared with Objective-C.
I didn’t know whether or not I can use it when I work in a company to write an iOS project, because almost all of the development teams were using Objective-C as the major development language. Maybe I can use Swift in company projects after two or three years I thought at the time. Fortunately, the company I first joined is a entrepreneurial team, we had only two iOS developers. I decided to import Swift to our projects one year later because I thought I had learned Swift for a whole year and written several example applications.
In that after I began using Swift in company’s projects, I have never backed to Objective-C. The Swift has updated several significant versions in these years, some versions need us to make many syntax modifications in our projects. And I have to learn these new features and changes over and over again. I don’t know how many times learning Swift this is, but now I decide to read the official Swift document seriously to search the knowledge I missed.
This is a checklist to record the syntax problems I’m interested in and some magical usages about some types.
The Basics
Implicitly Unwrapped Optional
When should we use implicitly unwrapped optionals?
First, we should know that the implicitly unwrapped optionals are also of the normal Optional
type.
1 | let value: Int! |
When you assign an implicitly unwrapped optional to an optional variable, the compiler do nothing, because they are the same type. But when you assign an implicitly unwrapped optional to an un-optional variable, the compile will force-unwraps the value. If the implicitly unwrapped optional is nil and you try to access its wrapped value, you’ll trigger a runtime error.
We all know that we shouldn’t use implicitly unwrapped optionals, because they are dangerous to crash applications. But we still need it in some special situations, such as below.
1 | class Owner { |
An owner must has a pet, to initialize a pet needing an owner. If we modify the var pet: Pet!
to var pet: Pet
, the compiler will require us to assign a value to the property pet
before using the self
, but we have to get the self
to create a pet, they conflict each other. So we need to use implicitly unwrapped optional to declare that the pet
must has a value and to tell the compiler not checking the property during initialization because we will assign it a value soon after.
Assertions, Preconditions and Fatal Errors
What’s the Precondition?
They are all used to check code conditions. We usually use Assertions, such as assert(id != nil, "The goods id cannot be nil.")
. Sometimes, we would use assert(false, "error message")
under a checked condition situation, here is a better syntax assertFailure("error message")
.
I have never used Preconditions. To be honest, I have never heard about it. They are all same in usages, but the compiler evaluates Assertions only in development mode. The Preconditions can be still checked in release mode if the compiler doesn’t open the unchecked mode(-Ounchecked).
The Fatal Errors are always checked in any compiler mode.
MeatType
A metatype type refers to the type of any type, including class types, structure types, enumeration types, and protocol types.
- The metatype of a class, structure, enumeration type is the name of that type followed by
.Type
(let metatype: SomeClass.Type) - The metatype of a protocol type is the name of the protocol followed by
.Protocol
(let metatype: SomeProtocol.Protocol). If you want to depict the metatype of the concrete type satisfying the protocol conformance, you can use the type followed by.Type
to do this.(let concreteMetatype = SomeProtocol.Type) - You can postfix the types with
.self
to get their metatypes. (let metatype: SomeClass.Type = SomeClass.self)
We can use a type’s metatype to construct its instances. But if the type is class type, we must declare the class type with final
or mark the initializers that will be used as required
.
Self Type
Swift provides the Self
type that we can conveniently use it to represent the type itself without repeating its name. The Self
refers to the same type as the type(of:)
function in the Swift standard library.
Basic Operators
Remainder Operator
A knowledge point about
%
.
We use the %
to calculate a number’s remainder. We assume an expression let reminder = a % b
, the sign of the number b is ignored when calculating. This means that a % b
and a % -b
always give the same answer.
Comparison Operators
Compare two tuples.
We can use comparison operators directly to compare two tuples.
1 | (10, "a") > (9, "b") // true |
The compiler will compare their elements one by one from left to right.
Range Operators
How many kinds of range operators?
1 | print(type(of: 1...2)) // ClosedRange<Int> |
The ...
can only be used in sequences to declare slicing all elements of it.
There are some alias types of range operators.
1 | public typealias CountableRange<Bound> = Range<Bound> where Bound : Strideable, Bound.Stride : SignedInteger |
Strings and Characters
String Literals
Set up a single line string with the multiline string literal format via the symbol
\
.
1 | let softWrappedQuotation = """ |
A multiline string can be indented to match the surrounding code.
Extended String Delimiters
Do not render string interpolations.
1 | let name = "CSL" |
We can surround the string with number signs #
to disable string interpolation. If we want to use string interpolation in a string that uses extended delimiters, we can add a sign #
behind the backslash of string interpolations.
Unicode Scalars and Extended Grapheme Clusters
What is a Swift’s string composed by?
A string contains a sequence of characters of Swift’s Character
type.
Every instance of Swift’s Character
type represents a single extended grapheme cluster
.
An extended grapheme cluster contains a sequence of unicode scalars
.
1 | let eAcute: Character = "\u{E9}" // é |
The two characters above are rendered as the same format but they are absolutely different in underlying values.
Counting Characters
Do you know how to calculate the length of a string?
1 | let eAcute: String = "\u{E9}" |
As you can see, the count of a string of Swift’s String type means the count of extended grapheme clusters. The length of a string of NSString is based on the number of 16-bit code units within the string’s UTF-16 representation.
String Indices
Why does Swift not provide the Int indices for String?
As mentioned above, a string is composed by a sequence of grapheme clusters and a grapheme cluster might contain several unicode scalars(One scalar occupies 21 bits memory). So the compiler can not use an int index to offset the memory address to find the specific character, because the memory size one character occupied is uncertain.
Comparing Strings
"\u{E9}"
=="\u{65}\u{301}"
, true or false?
The result of the comparison expression above is true. The "\u{E9}"
is é
and the "\u{65}\u{301}"
is e
followed by ́
. Theirs unicode scalars are not equal, but the representations of their grapheme clusters are all the character é
. So Swift thinks they are the same strings.
Control Flow
Where
Using
Where
in switch cases to perform dynamic filtering.
1 | let yetAnotherPoint = (1, -1) |
You can write any boolean result expressions following the statement where
.
Fallthrough
Fallthrough won’t check the next case’s conditions, directly executes its block.
1 | let age = 18 |
The variable age is 18 of type Int, it’s outside of the range 19...100
. But the compiler would ignore the next case’s condition checking to straightly execute its block when using the keyword fallthrough
.
Labeled Statements
Exit the specified one in nested control flows.
1 | for x in 0...2 { |
In the switch statement, we want to exit the outer loop when checking the x is equal to 2. But the break statement in the switch’s scope means exiting the switch control flow rather than the outer loop.
The resolution is to label the outer loop and to use the break followed by the loop’s label to definitely exit the outer loop.
1 | outerLoop: for x in 0...2 { |
Every control statements can prefix labels.
Functions
Variadic Parameters
We can define multiple variadic parameters for a function.
1 | func zip(leftValues: String..., rightValues: String...) -> [(String, String)] { |
We defined a zip method of variadic parameter style above. The count of variadic parameters is unlimited, but every parameter following a variadic parameter must have the argument label.
Closures
Escaping Closures
Escaping closures can’t capture a mutable reference to self for structures.
1 | struct SomeStruct { |
Structures and enumerations don’t allow shared mutability.
Enumerations
Recursive Enumerations
Use
indirect
to indicate that an enumeration or enumeration cases are recursive.
1 | enum MatryoshkaDoll { |
The inner of a matryoshka doll has either a smaller doll or nothing. We declare an enumeration above named MatryoshkaDoll
to represent a matryoshka doll. The case doll
of the enumeration has an associated value of type MatryoshkaDoll
means it has another one in its inner. The case doll’s associated values a value of itself enumeration type means it’s a recursive case that we need to add the indirect
attribute before the keyword case. Alternatively, you can only add the indirect
attribute before the keyword enum
of the enumeration to indicate all cases of the enumeration are indirect.
Properties
Lazy Stored Properties
Access a lazy stored property in multiple threads simultaneously isn’t thread-safe.
If we access a lazy stored property of an instance in multiple threads simultaneously, the initialization of the property would be executed repeatedly.
Property Observers
What’s the observers calling order when we assign a new value to an override stored property of a subclass instance?
1 | class Person { |
As described in the example above, if we set a new value to the age
property of an Student
instance, the willSet
property observer of the age
property in Student
(subclass) is invoked before it’s invoked in Person
(superclass), but the didSet
property observer is invoked in Student
(subclass) after it’s invoked in Person
(superclass).
The calling order is like this: willSet(subclass)
-> willSet(superclass)
-> didSet(superclass)
-> didSet(subclass)
.
If we pass a value property as an
inout
parameter to a function, the property observers for that property will be invoked even though the function doesn’t change the property’s value in its body.
1 | let me = Student() |
This is because the implementation principle of the inout
parameter. The value of the inout parameter will be written back to the property at the end of the function. The writing operation would trigger the property observers for the property.
Property Wrapper
You can use a structure, class, or enumeration to define a property wrapper.
Usually, we would use a structure or class to implement a property wrapper, because they can have stored properties to as the storage of the wrappedValue
.
If we don’t need to store the wrappedValue
’s value or the underlying storage system is global static or is a singleton like UserDefaults or DataBase, using enumerations maybe is an alternative choice.
When you apply a wrapper to a property, the compiler synthesizes code that provides storage for the wrapper and code that provides access to the property through the wrapper.
1 | @propertyWrapper |
The example above is from official documents. The TwelveOrLess
property wrapper would limit the value of the Int property to max 12.
We can use it like below.
1 | struct SmallRectangle { |
We use the TwelveOrLess
wrapper to modify the two properties height
and width
of the SmallRectangle
struct. When we build code, the compiler will automatically synthesize code for wrapped properties to provide access methods and private storage properties like below.
1 | struct SmallRectangle { |
As described in the example above, the wrapped properties will be converted to computed properties, and the value storing is actually supported by the stored properties of type TwelveOrLess
(propertyWrapper). The underlying private stored properties are named with the wrapped properties names prefixed an underscore.
1 | struct SmallRectangle { |
As you can see, we can directly access the _height
and the _width
properties. They are of type TwelveOrLess
.
How to set up initial values for wrapped properties?
Int the example above, we can not customize an initial value for the wrapped value. Before learning set up an initial value, let’s take a look at how the propertyWrapper
is instanced, because the initial value you passed is actually stored to the underlying property wrappers.
1 | @propertyWrapper |
If we just write a pure property wrapper type name for a property like below, the code synthesized by the compiler will call the wrapper’s init()
initializer.
1 | struct SmallRectangle { |
Of course, you can add other initializers for SmallNumber to pass some initial arguments.
1 | @propertyWrapper |
Alternatively, if you implements an init(wrappedValue:)
initializer for the propertyWrapper
, you can assign a initial value to the wrapped property as part of declaring the property.
1 | @propertyWrapper |
There is a complex usage that you can config the SmallNumber wrapper with custom arguments and assign an initial value to the property in one single declaring code.
1 | @propertyWrapper |
If the initializer has an parameter named wrappedValue
, the initial value to the wrapped property will be pass to the initializer as the wrappedValue
parameter’s argument.
Using projected values to expand property wrappers functionality.
Projected values are similar as wrapped values. You can declare a property named projectedValue
of any type you need and then use the dollar sign $
followed the property’s name to access its projected value.
1 | @propertyWrapper |
We add a property named projectedValue
in SmallNumber to record how many times the wrapped property has been changed.
1 | var rectangle = SmallRectangle() |
- From Swift 5.4 onwards the property wrapper can be used in parameters of functions. Although the parameters of functions are
let
constants wil limit the power of properties wrappers, but we can also utilize the all features like using in properties or variables.
Global and Local Variables
All the computing, observing and property wrappers can be used in global or local variables.
1 | struct ValueBox { |
As you can see, we separate variables to four kinds, globalStoredVariable
, globalComputedVariable
, localStoredVariable
and localComputedVariable
. Property wrappers can only be used in localStoredVariable
, the global variables and computed variables are not supported.
Global constants and variables are always computed lazily, in a similar manner to lazy stored properties.
Type Properties
Stored type properties are always computed lazily
Stored type properties are always computed lazily, they are initialized on their fist access. Unlike lazy instance properties, Swift guarantee that they are initialized only once, even when accessed by multiple threads simultaneously.
Type Property Syntax
The differences between
static
andclass
when defining a type property.
We can use static
to define a type property, stored or computed. If you want to override a computed type property in a subclass, you need to replace static
to class
.
Methods
The differences of the implicit
self
property between classes and structures.
The implicit property self
in classes is a constant that you can’t assign a new value to it. But the self
in structures or enumerations is a variable that you can assign a new value to it in a mutating method.
The differences between
class
andstatic
when defining a type method.
Using the class
keyword to replace of the static
keyword to define a type method to make it can be overridden by subclasses.
Inheritance
Accessing superclasses Methods, Properties, and Subscripts
Where can we use the
super
keyword?
- An overridden method, calling
super.someMethod()
. - An overridden property, calling
super.someProperty
in getter or setter. - An overridden subscript, calling
super[someIndex]
.
Overrides getter and setter for stored or computed properties inherited from superclasses.
The subclass doesn’t know the property’s nature is computed or stored when inheriting it from the superclass. So we can override properties of any type and don’t care whether its type is computed or stored.
However, we can only override a read-only property to be a read-write property, but we can not override a read-write property to be a read-only property.
Overrides property observers for variable stored properties or read-write computed properties.
The constant properties and read-only properties value can’t change, so they don’t need the willSet
and didSet
observers.
Preventing Overrides
Using the
final
keyword to prevent any override.
We can write the final
modifier before the method, property, or subscript’s introducer keyword to prevent them from being overridden. The method, property, or subscript defined in an extension can also use the final
modifier. Adding the final
modifier before the class
introducer can mark the class type that can’t be inherited.
Initialization
Setting initial values for Stored Properties
When you assign a default value to a stored property, or set its initial value within an initializer. The value of the property is set directly, without calling any property observers.
For class instances, a constant property can be modified during initialization only by the class that introduces it. It can’t be modified by a subclass.
Initializer Delegation for Value Types
Customizes an initializer for a structure and keeps its automatically generated memberwise initializer.
When we provide a custom initializer for structures or classes, the automatically generated initializer(memberwise initializer for structures) will be disabled. If you still want to access the default initializer, you can place your custom initializers in an extension for the type rather than declaring them in the type’s original implementation.
Initializer Delegation for Class Types
Initializer delegation rules
- A designated initializer must call a designated initializer from its immediate superclass.
- A convenience initializer must call an initializer from the same class.
- A convenience initializer must ultimately call a designated initializer.
Two-Phase Initialization
How to write a correct initializer?
To completely initialize an instance of a class needs through two phases. The first phase is allocating memory for the new instance and initializing all stored properties with a default value. The second phase is customizing properties further and then the instance is ready for use.
The detail checking rules in the official document.
Next, let me show you a standard example:
1 | class Superclass { |
- A subclass designated initializer must initialize all stored properties introduced by itself before calling the superclass initializer.
- Before calling the superclass initializer, the subclass can’t access any inherited properties or instance methods.
- After then, you can begins customizing any property or calling instance methods you want.
- Within a subclass convenience initializer, you can’t access any property before calling another initializer.
- Then, you can customize whatever you want.
If an initializer doesn’t do any customization in phase 2, and the superclass has a zero-argument designated initializer, you can omit a call to
super.init()
after initializing all stored properties.
1 | class Person { |
Initializer Inheritance and Overriding
By default, subclasses in Swift don’t inherit any initializer from its superclasses. If you want to use initializers that’s the same as the superclass, you can customize them again.
Constant properties can only be modified in initializers for the classes introduced them. So subclasses can only modify inherited variable properties during initialization, but can’t modify inherited constant properties.
Just like overriding properties, methods, or descriptions. If a subclass override a superclass initializer, it needs to add an override
modifier before the initializer.
When you inherit a superclass designated initializer, you always need to write the override
modifier before your overriding initializer, whether your implementation of it is designated or convenience.
Conversely, if you override a superclass convenience initializer, subclasses can’t directly access superclasses convenience initializers, so strictly speaking that you subclass doesn’t provide an override of the superclass initializer. Therefore, you don’t need writing the override
modifier for your overriding initializers.
Automatic Initializer Inheritance
In some specific scenes, subclasses can inherit their superclass initializers.
Rule 1
If your subclass doesn’t define any designated initializer, it automatically inherits all of its superclass designated initializers.
Rule 2
If your subclass provides an implementation of all its superclass designated initializers, it automatically inherits all of its superclass convenience initializers.(satisfying the rule 1, or overriding all superclass designated initializers. Overriding superclass designated initializers to convenience initializers is also ok)
Overriding a Failable Initializer
You can override a failable initializer with a nonfailable initializer but not the other way around.
You can delegate from init!
to init?
and vise versa, and you can override init?
with init!
and vice versa. And also you can delegate from init
to init!
and visa versa.
1 | class Person { |
Required Initializers
Write a required
modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer.
You don’t have to provide an override
modifier when overriding a required designated initializer, you can think required init
to required override init
.
You don’t have to explicitly implement a required initializer if you can satisfy the requirement with an inherited initializer.
Optional Chaining
Calling Methods Through Optional Chaining
We know that a function without specifying a returned value type returns a Void
(alias to empty tuple ()
) value. Alternatively, the assignment operation also returns a Void
value.
So we can compare a method call’s returned value against nil to see if the method call was successful.
1 | class Person { |
Similarly, we can also compare the returned value of an assignment operation against nil to see if the property was set successfully.
1 | if (me.pet?.name = "Raspberry") != nil { |
Linking Multiple Levels of Chaining
- If the type you are trying to retrieve isn’t optional, it will become an optional because of the optional chaining.
- If the type you are trying to retrieve is already optional, it won’t become more optional because of the chaining.
Concurrency
What’s the
__consuming
keyword?
Unused syntax feature StackOverflow
What’s the
@rethrows
keyword before definitions of protocols?
1 | public protocol AsyncSequence { |
If you want to executes a sequence of asynchronous tasks, you can call the asynchronous methods within a block scope of a loop method. Otherwise the more concise way is using the AsyncSequence
type. Swift provides many extensions for common asynchronous types to conform to the AsyncSequence
protocol. Then you can easily use the for await-in
pattern to iterate a sequence of asynchronous tasks. Of course, you can custom you own types that conform to the AsyncSequence
protocol.
Executing asynchronous tasks in parallel.
Using await
to call synchronous tasks is sequential. But sometimes the multiple tasks are executing independently, so they can run at the same time to increase the processing performance. This time, you can select to use the await let
pattern to execute several tasks in parallel.
1 | func generateDiplomaImage() async throws -> UIImage { |
Type Casting
Type Casting for Any and AnyObject
The type
Any
represents values of any type, includingoptional
types.
Swift gives you a warning if you use an optional value where a value of Any type is expected. If you really do need to use an optional value as an Any value, you can explicitly cast the optional to Any.
1 | let optionalNumber: Int? = 3 |
Nested Types
Extensions
Extensions can add new functionality to a type, but they can’t override existing functionality.
Extensions can add new computed properties for a type, but they can’t add stored properties, or add any property observers to existing properties.
Extensions can’t override any properties or methods for a type.
Extensions can only expand new convenience initializers for a type, adding designated initializers and deinitializers aren’t allowed.
Protocols
Swift thinks that all protocols should be used as type constraints rather than type declarations. In fact, a type declared by a protocol will be implicitly cast to a existential
that carries the protocol’s information.
In Swift 5.6, a new syntax any Protocol
will be introduced to explicitly declare variables of existential
types. Existential containers will erase all information about the actual type, you can only access methods or variables required by the protocol.
Erasing type information will prevent the performance improvement by Swift compiler, so we should always use generic types
or opaque types
to support Swift compiler to optimize code performance base on static type system.
Good news, in Swift 5.7, all protocols (having associated values or using Self) can be used as existential types, but remember do not abuse it.
Property Requirements
- A protocol can require a property to be gettable or gettable and settable.
- If a protocol require a property to be gettable and settable, the requirement can’t be satisfied by a constant stored property or a read-only computed property.
- If a protocol require a property to be gettable, the requirement can be satisfied by any kind of property. And it’s valid for the property to be also settable if this is useful for your own code.
Always using the
static
keyword to require a type property in protocols, you can implement it by usingstatic
orclass
freely in classes.
Mutating Method Requirements
A protocol instance method requirement marked with
mutating
.
- The adopting classes can ignore the mutating keyword.
- And the adopting value types(structures or enumerations) can also ignore the mutating keyword if their implementation to this method don’t mutate themselves instances when executing.
- If the protocol instance methods requirement doesn’t prefix with
mutating
, the implementation to this method in value types prefixed withmutating
isn’t fulfilled to the requirement.
Initializer Requirements
A protocol initializer requirement requires that the adopting classes must mark their implementing initializer as
required
.
1 | protocol InitWithName { |
This rule can make sure all subclasses of the Person
class perform to the InitWithName
protocol.
If the class conforming to the protocol was marked as final
(final classes can’t be inherited), you can omit the required
modifier.
Note that a final subclass inherited from a superclass defined a required initializer has to override the required initializer with the required
modifier
If a subclass overrides a designated initializer from its superclass, and also implements a matching initializer requirement from a protocol, mark the initializer implementation with both the required
and override
modifiers.
1 | protocol SomeProtocol { |
Protocol Extensions
Protocol extensions can add implementation to conforming types but can’t make a protocol extend or inherit from another protocol.
Generics
Type Constraints
When defining a generic type function, we can require type constraints to limit the selection scope of the generic types. As described in above, we know that we can also add type constraints when expanding an existing type.
Normally, we constrain a generic type by appending a colon(😃 following a type name or protocol composition that represents the generic type has to inherits from the class type or conforms to the protocol or protocol composition.
1 | func someFunction<T: SomeClass & SomeProtocol>(a: T) {} |
In extensions with type constraints, we can also write type constraints with the where
clause. Similar to generic type constraints above, we can write a extension example like this:
1 | extension Array where Element: SomeClass & SomeProtocol {} |
This means that the type of elements of an array has to inherit from SomeClass
and conform to SomeProtocol
to use the additional functionality expanded by the extension.
In addition to the colon symbol, we can replace it with the equal to(==
) operator in extension constraints. That means the Element type has to be SomeClass
, not its subclasses.
Opaque Types
We know the Opaque Type
’s syntax is some SomeProtocol
. It’s similar to Protocol but having completely different meaning. You use a protocol as a method’s return value means that this method can return values of various types conforming to the protocol. A function or method has an opaque return value means that they return a value of a certain type conforming to the protocol, but they hide the return value’s type information.
The biggest difference is that a protocol return value doesn’t have any certain type information-the actual return values type can be any conforming types, but an opaque return value takes the implicit type information the compiler can access-the method’s body can only return the same type values.
Why do we use an opaque return value to replace of a protocol one? A protocol value is clearly more flexible. Let’s see an example:
1 | protocol Animal: Comparable {} |
The method catchAnAnimal
will return a value conforming to Animal
. We call this method twice to return two values of Animal
, but they can’t be compared by the compare ==
operator. Because the compiler can’t determine whether these two values are of the same type although they are both conforming to Animal
. Swift is a type-safe language that you can’t compare two values of different types.
But if we replace the protocol value of an opaque values, we can satisfy the situation above.
1 | func catchAnAnimal() -> some Animal { /*...*/ } |
The method catchAnAnimal
also returns a value that conforms to Animal
, but the difference from Protocol is that the opaque return values from one method is always of the same type. The compiler make sure that all return branches of the method can only return values of the same type in compiling-time. So that we can compare values returned by the method catchAnAnimal
returning opaque values above.
There is another special situation about that a value of a protocol doesn’t conform to the protocol itself. As shown in the example below:
1 | struct Dog: Animal {} |
In the example above, you build the code will result in a compiling-time error. The error info is that Protocol Animal as a type cannot conform to the protocol itself. Why? It’s all because of the type-safe. The type T for the generic method is certain in compiling-time, so the animal parameter is of the certain type T not the protocol Animal. You can think the T is a type placeholder and it will be replaced to a certain type(Dog, Cat, or others). So if the method takes a Dog property but you pass it a value of Animal, there’s obviously an error of type matching.
If we do a little modifications for the method, giving it an opaque return value, you can build the code successfully. This is due to whether the opaque type or generic type are all type placeholders, they are representing one certain type at compile time.
Not like C++ or Rust, Swift doesn’t always specialize generic functions to certain type functions
In additional to the usages above, opaque values have another functionality. We know that protocols with associated values or using Self can’t be used as types. They can only be used in type constraints. This is because the protocol with different associated values are completely different types. When you pass an argument of protocol Container with an associated Element to a method, how do we use the elements of the container without type information about these elements?
If we use opaque values to replace result values of protocols with associated values of methods, the compiling can pass because of the types are constant and the compiler can get their type information.
1 | protocol Container { |
As you can see above, the type of some Container
is a certain type. In compiling-time, you can argue that the compiler think of some Container
as IntContainer
.
Automatic Reference Counting
Keyword
unowned(unsafe)
The unowned
keyword has two different formats, unowned(safe)
and unowned(unsafe)
. We write the unowned
before a property or variable means that the instance assigned to the property or variable won’t be strong referenced. But we must sure the instance to that property refers isn’t deallocated when accessing.
The other format unowned(unsafe)
is equivalent of Objective-C’s @property(assign)
or __unsafe_unretained
. It shouldn’t be used in Swift code, because it’s purpose is to bridge to code written in Objective-C.
An __unsafe_unretained
value is a simple pointer, it won’t increase the referenced instance’s reference count just like using the weak
keyword. But the only difference from weak
is the pointer won’t be automatically set to nil when the value it refers to is deallocated. The benefit of using __unsafe_unretained
is having more performance, because the runtime doesn’t need to record the pointer and set the pointer to nil in due time.
Unowned Optional References
We can declare a property of Optional as unowned. We know the unowned modifier can only be used for reference types, but Optional is an exception. A property declared with weak
has to be a variable of Optional, because the property may be set to nil in run-time. Not like weak, an unowned optional property won’t be set to nil when the value it refers to is deallocated. It behaves the same as an unowned property with an extra implication that the property may be nil. If the value of an unowned optional property is Optional.none
, you can safely access it whenever. But if its value is of Optional.some
, you can think of it as a normal unowned property. Trying to unwrap an unowned optional property that’s referenced value has been deallocated will trigger a runtime error.
Defining a Capture List
Let’s firstly see an example:
1 | class Person { |
AS you can see above, if we directly use self
in the body of the block property of the instance, this will cause a reference cycle. Using a capture list can break the reference cycle, we usually write the capture list like this:
1 | private func doTest() { |
Using [weak self]
, the block will only weakly reference to self for preventing a strong reference cycle. But the weak self
makes self in the block’s be an optional value, so we need to add a question mark after self before accessing it.
Unfamiliar usages for Capture List.
If we just want to use the name
property in the block’s body, we can only write [name]
in the capture list, and then we can directly use the name
like a local variable in the block’s body. Another writing format is [name = self.name]
.
1 | private func doTest() { |
Memory Safety
Keeping memory access safety in single thread.
Whenever we read the value of a variable or assign a new to the variable will access its location in memory. If a function or method gets a value’s write permission and another operation writes or reads the value at the same time, there is a memory conflict occurred. There are three key points that least one operation gets the write access, accessing the same location in memory, and their accessing durations overlap.
In our development routine, the inout
parameters is the most possible place occurring memory conflicts.
We list the three commonest scenes:
- Simultaneously accessing an inout parameter’s location in memory in functions body.
1 | var stepSize = 1 |
1 | func balance(_ x: inout Int, _ y: inout Int) { |
See the last line of code, these two inout parameters refers to the same location in memory of the global variable playerTwoScore
. The function will get the playerTwoScore
’s accessing permission twice at the same time. This will result in a memory conflict.
- Mutating functions in structures.
A function of structures marked with the mutating
modifier means that the function can modify the structure itself when calling, so the mutating function will holds the structure’s accessing permission.
1 | struct Player { |
As show in the code above, the searchHealth
is a mutating function, it will hold the accessing permission of the player itself. If we pass the searchHealth
function a inout parameter that refers to the same player, there is a memory conflict.
- If you pass a property of structures as a function’s inout parameter, the function gets the whole structure’s accessing permission.
1 | var holly = Player(name: "Holly", health: 10, energy: 10) |
Access Control
Main guiding principle of declaring access levels
- The main guiding principle of declaring access levels is that the access level of your custom entities(methods, types) must be more or as restrictive as the access levels of its constituent types.
- If you state the whole custom type as
private
orfileprivate
, all its properties, methods, or nested types are declared asprivate
orfileprivate
by default. But if you state the custom type aspublic
oropen
, all its constituent’s access levels areinternal
rather than the corresponding access levels. This rule is used to prevent accidently leaking the detail implementation of your code’s functionality when your declare your code aspublic
oropen
for creating an API.
The
open
access level can only be used in class types.
Custom Types
- Defining a custom type as
private
orfile private
, all its properties or methods are marked asprivate
orfile private
. - Defining a custom type as
public
oropen
, all its properties or methods are marked asinternal
rather than the corresponding access levels.
Tuple Types
A tuple type can’t define its own access level. The access level of a tuple type depends on the one of the lowest access level of its elements.
Function Types
A function’s highest access level depends on the most restrictive access level of its constituent types(property types and return value types). If you don’t explicitly state the function’s access level(defaults to internal
) and the inferred access level of the function is lower than internal
, the compiler will report an error that warns you to explicitly add private
or fileprivate
keywords for your function.
Enumeration Types
Similarly, an enumeration with a raw value type or associated values, its highest access level depends on the lowest access level of its constituent types, raw value type and associated values type.
Nested Types
A nested type’s access level is normally equal to its containing type. But if the containing type is declared as public
, the nested type’s access level is internal
by default. You can then explicitly add the public
modifier for the nested type to upgrade its access level to public
.
Subclassing
- A subclass’s access level must be lower than its superclass.
- The access level of any member of the class can have a higher level than the class itself.
- An overriding member of the subclass can have a higher access level than the overridden member of its superclass.
- There is an exception that the subclass can’t override the member as
private
if it’sfileprivate
in the superclass.
Kinds | Superclass | Subclass |
---|---|---|
Class Type | Any access level | Lower |
Members | open/public/internal | Any access level(equal, lower, or higher) |
Members | fileprivate | fileprivate or higher(can’t be private) |
Constants, Variables, Properties, and Subscripts
- A constant, variable, or property can’t be more public than its type.
- Similarly, a subscript’s access level can’t be public than either its index type or return type.
Default Memberwise Initializers for Structure Types
The access level of the default memberwise initializer of a structure depends on the lowest access level of its all stored properties. Having any private
stored property, the memberwise initializer is considered as private
. Likewise, having any fileprivate
stored property, the memberwise initializer is considered as fileprivate
.
Protocols
- The requirements of protocols can’t be separately defined to different access levels.
- If we can access a protocol in an access context, all the protocol’s implementations of the classes conforming to the protocol and visible in the context must be accessible.
- As described above, if a public class conforms to a public protocol, all its implements for the protocol’s requirements must be explicitly marked as public or more higher access levels(open, if can)
Protocol Inheritance
As with class inheritance, a sub protocol’s access level can’t be more public than its super protocol.
Extensions
- If you extend a public or internal type, the extension will be marked as
internal
. - If you extend a private or fileprivate type, the extension will be marked as
private
orfileprivate
- You can explicitly specify the extension’s access level, as a result, all its extended properties, methods, and so on are marked as the specified access level.
- An extension conforming to a protocol can’t explicitly specify an access level, its access level is marked as equal to the protocol by default.
Type Aliases
The type alias access level can be lower or equal to the type it aliases.
An identifier prefixed with one underscore
_
means its access level isinternal
even though explicitly marked aspublic
.
An identifier prefixed with two underscores are reserved for the Swift compiler and standard library.
Advanced Operators
Two ways to define operators for you custom types.
- Global methods
1 | struct Point { |
- Extensions
1 | struct Point { |
We can’t overload the default assignment operator(
=
) and the ternary condition operator(a ? b : c).
You don’t specify a precedence for a prefix or postfix operator. However, if you apply both a prefix and postfix operator to the same operand, the postfix operator is applied first.