baicai

白菜

一个勤奋的代码搬运工!

Comprehensive Analysis of Swift Syntax

Introduction to Swift#

Swift is a new language for developing applications for iOS, macOS, watchOS, and tvOS.
Swift is a safe, fast, and interactive programming language.
Swift supports code previews (playgrounds), a feature that allows programmers to run Swift code and see results in real-time without compiling and running the application.

Swift avoids many common programming errors by adopting modern programming paradigms:

  • Variables are always initialized before use.
  • Checks for array index out-of-bounds errors.
  • Checks for integer overflow.
  • Optional values ensure that nil values are handled explicitly.
  • Memory is managed automatically.
  • Error handling allows recovery from unexpected failures.

Basic Section#

Constants and Variables#

Declare constants and variables; constants and variables must be declared before use, using let to declare constants and var to declare variables.
Example:

let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0

// Type annotation
var welcomeMessage: String

Comments#

Single-line comments use double forward slashes (//), and multi-line comments use (/* multi-line */). Swift's multi-line comments can be nested within other multi-line comments.
Example:

// This is a comment

/* This is also a comment,
but it is multi-line */

/* This is the beginning of the first multi-line comment
/* This is the second nested multi-line comment */
This is the end of the first multi-line comment */

Semicolons#

Swift does not require you to use semicolons (;) at the end of each statement.
Multiple independent statements on the same line must be separated by semicolons.

let cat = "🐱"; print(cat)
// Outputs "🐱"

Identifiers#

Identifiers are names assigned to variables, constants, methods, functions, enums, structs, classes, protocols, etc. The letters that make up an identifier have specific rules, and the naming rules for identifiers in Swift are as follows:

  • Case-sensitive; Myname and myname are two different identifiers;

  • The first character of an identifier can start with an underscore (_) or a letter, but cannot be a digit;

  • Other characters in an identifier can be underscores (_), letters, or digits.

For example: userName, User_Name, _sys_val, height, etc., are valid identifiers, while 2mail, room#, and class are invalid identifiers.

Note: The letters in Swift use Unicode encoding. Unicode is a universal encoding system that includes Asian character encodings, such as Chinese, Japanese, Korean, etc., and even the emojis we use in chat tools.

If you must use a keyword as an identifier, you can add backticks (`) before and after the keyword, for example:

let `class` = "xiaobai"

Keywords#

Keywords are reserved sequences of characters similar to identifiers, and cannot be used as identifiers unless enclosed in backticks (`). Keywords are predefined reserved identifiers that have special meaning to the compiler. Common keywords include the following four types.
Keywords related to declarations

class 	deinit 	enum 	extension
func 	import 	init 	internal
let 	operator 	private 	protocol
public 	static 	struct 	subscript
typealias 	var 

Keywords related to statements

break 	case 	continue 	default
do 	else 	fallthrough 	for
if 	in 	return 	switch
where 	while 		

Expression and type keywords

as 	dynamicType 	false 	is
nil 	self 	Self 	super
true 	_COLUMN_ 	_FILE_ 	_FUNCTION_
_LINE_ 		

Keywords used in specific contexts

associativity 	convenience 	dynamic 	didSet
final 	get 	infix 	inout
lazy 	left 	mutating 	none
nonmutating 	optional 	override 	postfix
precedence 	prefix 	Protocol 	required
right 	set 	Type 	unowned
weak 	willSet 		

Swift Whitespace#

Swift has specific requirements for the use of whitespace.
In Swift, operators cannot be placed directly next to variables or constants. For example, the following code will cause an error:

let a= 1 + 2

The error message is:

error: prefix/postfix '=' is reserved

This means that using the equals sign directly next to the preceding or following value is reserved.

The following code will also cause an error (continue to pay attention to whitespace):

let a = 1+ 2

The error message is:

error: consecutive statements on a line must be separated by ';'

This is because Swift thinks the statement ends at 1+, and 2 is considered the next statement.

Only writing it this way will not cause an error:

let a = 1 + 2;  // Coding standards recommend using this format
let b = 3+4 // This is also OK

Integers and Floating-Point Numbers#

Using Int uniformly can improve code reusability, avoid conversions between different types of numbers, and match the type inference of numbers.
Example:

let minValue = UInt8.min  // minValue is 0, of type UInt8
let maxValue = UInt8.max  // maxValue is 255, of type UInt8

Type Safety and Type Inference#

Swift is a type-safe language, which means Swift allows you to know the type of a value clearly.
If you do not explicitly specify a type, Swift will use type inference to select the appropriate type (int, double).
Example:

let meaningOfLife = 42
// meaningOfLife will be inferred as Int type

let pi = 3.14159
// pi will be inferred as Double type

Numeric Literals and Numeric Type Conversion#

Example:

let decimalInteger = 17
let binaryInteger = 0b10001       // Binary 17
let octalInteger = 0o21           // Octal 17
let hexadecimalInteger = 0x11     // Hexadecimal 17

Type Aliases#

Type aliases are another name defined for existing types. You can use the typealias keyword to define a type alias.
Example:

typealias AudioSample = UInt16
var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound is now 0

Boolean Values#

Example:

let orangesAreOrange = true
let turnipsAreDelicious = false

Tuples#

Tuples combine multiple values into a single compound value. The values in a tuple can be of any type and do not need to be of the same type.
Example:

let http404Error = (404, "Not Found")
// http404Error's type is (Int, String), value is (404, "Not Found")

Optional Types#

Use optional types to handle situations where a value may be missing. An optional type represents two possibilities: either there is a value, and you can unwrap the optional type to access that value, or there is no value at all.
Example:

var serverResponseCode: Int? = 404
// serverResponseCode contains an optional Int value 404
serverResponseCode = nil
// serverResponseCode now contains no value

Error Handling#

Error handling addresses potential error conditions that may occur during program execution.
Example:

func makeASandwich() throws {
    // ...
}

do {
    try makeASandwich()
    eatASandwich()
} catch SandwichError.outOfCleanDishes {
    washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
    buyGroceries(ingredients)
}

Assertions and Preconditions#

Assertions and preconditions are checks made at runtime.

let age = -3
assert(age >= 0, "A person's age cannot be less than zero")
// Since age < 0, the assertion will trigger

Basic Operators#

Swift supports most standard C language operators and also provides range operators not found in C, such as a..<b or a...b.
Assignment operators, arithmetic operators, compound assignment operators, comparison operators, ternary operators, nil-coalescing operators, range operators, logical operators

Operators are classified as unary, binary, and ternary operators.
The closed range operator (a...b) defines a range that includes all values from a to b (including a and b).
The half-open range operator (a..<b) defines a range from a to b but does not include b.
The closed range operator has another expression form that can express ranges extending infinitely in one direction, (a..., ...b).
Example:

let names = ["Anna", "Alex", "Brian", "Jack"]
let count = names.count
for i in 0..<count {
    print("Person \(i + 1) is named \(names[i])")
}
// Person 1 is named Anna
// Person 2 is named Alex
// Person 3 is named Brian
// Person 4 is named Jack

Strings and Characters#

String literals, string interpolation, counting characters, accessing and modifying strings, substrings, comparing strings

Initialize an empty string, string mutability, strings are value types, concatenating strings and characters (+, +=).
Using characters, you can iterate through a string using a for-in loop to get the value of each character in the string.
String interpolation is a way to construct a new string that can include constants, variables, literals, and expressions. You can insert constants, variables, literals, and expressions into an existing string to form a longer string.
Swift provides three ways to compare text values: string character equality, prefix equality, and suffix equality.
Example:

// Multi-line string literal
let quotation = """
The White Rabbit put on his spectacles.  "Where shall I begin,
please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""

// The following two strings are actually the same
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""

// String interpolation
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"

// Counting characters
var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// Prints "the number of characters in cafe is 4"

var emptyString = ""               // Empty string literal
var anotherEmptyString = String()  // Initialization method
// Both strings are empty and equivalent.

let catCharacters: [Character] = ["C", "a", "t", "!"]
let catString = String(catCharacters)
print(catString)
// Prints "Cat!"

Collection Types#

The Swift language provides three basic collection types: arrays (Array), sets (Set), and dictionaries (Dictionary) for storing collection data. An array is a collection of ordered data. A set is a collection of unordered, unique data. A dictionary is a collection of unordered key-value pairs.
Mutability of collections, arrays (Arrays), sets (Sets), collection operations, dictionaries

Arrays use an ordered list to store multiple values of the same type. The same value can appear multiple times at different positions in an array.
Sets are used to store values of the same type without a defined order. When the order of elements in a set is not important or when you want to ensure that each element appears only once, you can use a set instead of an array.
Collection operations can efficiently perform some basic operations on collections, such as combining two collections, determining common elements between two collections, or checking whether two collections are entirely contained, partially contained, or not intersecting.
Dictionaries are an unordered collection that stores relationships between key-value pairs, where all keys must be of the same type and all values must also be of the same type. Each value (value) is associated with a unique key (key), which serves as the identifier for that value's data in the dictionary.
Example:

// Collection
var someInts = [Int]()
print("someInts is of type [Int] with \(someInts.count) items.")
// Prints "someInts is of type [Int] with 0 items."

var threeDoubles = Array(repeating: 0.0, count: 3)
// threeDoubles is a [Double] array, equivalent to [0.0, 0.0, 0.0]

var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles is inferred as [Double], equivalent to [2.5, 2.5, 2.5]
var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles is inferred as [Double], equivalent to [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]

// The enumerated() method iterates through the array
var shoppingList: [String] = ["Eggs", "Milk"]
for (index, value) in shoppingList.enumerated() {
    print("Item \(String(index + 1)): \(value)")
}

Control Flow#

For-In loops, While loops (Repeat-While), Conditional statements, Control transfer statements, Early exit (guard), Checking API availability

Like if statements, the execution of guard depends on the boolean value of an expression. We can use guard statements to require that conditions must be true to execute the code following the guard statement. Unlike if statements, a guard statement always has an else clause, which executes the code in the else clause if the condition is not true.
Swift has built-in support for checking API availability, and the compiler uses available information in the SDK to verify that all APIs used in our code are available on the project's specified deployment target. If we attempt to use an unavailable API, Swift will throw an error at compile time.
Example:

let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
    print("Hello, \(name)!")
}

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
    print("\(animalName)s have \(legCount) legs")
}

// General format of repeat-while loop
repeat {
    statements
} while condition


// Early exit
func greet(person: [String: String]) {
    guard let name = person["name"] else {
        return
    }

    print("Hello \(name)!")

    guard let location = person["location"] else {
        print("I hope the weather is nice near you.")
        return
    }

    print("I hope the weather is nice in \(location).")
}
greet(person: ["name": "John"])
// Prints "Hello John!"
// Prints "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// Prints "Hello Jane!"
// Prints "I hope the weather is nice in Cupertino."

Functions#

Function definition and calling, function parameters and return values, function parameter labels and parameter names, function types, nested functions

Optional tuple return types.
When defining an input-output parameter, add the inout keyword before the parameter definition.
Example:

// Function
func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// Prints "Hello Bill!  Glad you could visit from Cupertino."

// Optional tuple return type
func minMax(array: [Int]) -> (min: Int, max: Int)? {
    if array.isEmpty { return nil }
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)
}

// Implicitly returned function
func greeting(for person: String) -> String {
    "Hello, " + person + "!"
}
print(greeting(for: "Dave"))
// Prints "Hello, Dave!"

// Parameter labels
func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// Prints "Hello Bill!  Glad you could visit from Cupertino."

Closures#

Closures are self-contained blocks of function code that can be passed around and used in your code. They are similar to anonymous functions (Lambdas) in some programming languages.
Closure expressions, trailing closures, value capturing, closures as reference types, escaping closures (@escaping), auto closures

If you need to pass a long closure expression as the last parameter to a function, it is useful to replace this closure with a trailing closure form.
A closure can capture constants or variables from its defining context. Even if the original scope that defined these constants and variables no longer exists, the closure can still reference and modify these values within its body.
Example:

// Closure expression syntax
{ (parameters) -> return type in
    statements
}

// Trailing closure
let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
let strings = numbers.map {
    (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings is inferred as an array of strings, i.e., [String]
// Its value is ["OneSix", "FiveEight", "FiveOneZero"]

// Value capturing
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

// Auto closure, lazy evaluation
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

Enums#

Use the enum keyword to create an enumeration and place the entire definition within a pair of braces.
Enum syntax, using switch statements to match enum values, iterating enum members, associated values, raw values (default values), recursive enums (indirect)

You can define Swift enums to store associated values of any type, and each enum member can have a different associated value type.
Example:

// Enum syntax
enum SomeEnumeration {
    // Enum definitions go here
}

enum CompassPoint {
    case north
    case south
    case east
    case west
}

enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

let somePlanet = Planet.earth
switch somePlanet {
case .earth:
    print("Mostly harmless")
default:
    print("Not a safe place for humans")
}
// Prints "Mostly harmless"

// Associated values
enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

// Recursive enums
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
// (5 + 4) * 2

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case let .number(value):
        return value
    case let .addition(left, right):
        return evaluate(left) + evaluate(right)
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product))
// Prints "18"

Structs and Classes#

Comparison of structs and classes, structs and enums are value types, classes are reference types

Structs and classes serve as a general and flexible structure, forming the basis for building code. You can define properties and add methods to your structs and classes using the syntax for defining constants, variables, and functions.
Example:

// Classes and structs
struct SomeStructure {
    // Define the struct here
}
class SomeClass {
    // Define the class here
}

struct Resolution {
    var width = 0
    var height = 0
}
class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

Properties#

Stored properties, computed properties, property observers, property wrappers, global variables and local variables, type properties (static)

Properties associate values with specific classes, structs, or enums. Stored properties store constants and variables as part of an instance, while computed properties directly compute (rather than store) values. Computed properties can be used with classes, structs, and enums, while stored properties can only be used with classes and structs.
Property observers monitor and respond to changes in property values, and property observers are called every time a property is set a value, even if the new value is the same as the current value.

  • willSet is called before the new value is set
  • didSet is called after the new value is set

Property wrappers add a layer of separation between managing how properties are stored and defining the code for the properties.
Type properties are also accessed using the dot operator. However, type properties are accessed through the type itself rather than through an instance.
Example:

// Properties
struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var width = 0.0, height = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()       // Stored property
    var center: Point {     // Computed property
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
    size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)"

// Property wrappers
@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

Methods#

Instance methods, type methods (static)

Methods are functions associated with specific types.
Classes, structs, and enums can define instance methods; instance methods encapsulate specific tasks and functionalities for a given type.
Classes, structs, and enums can also define type methods; type methods are associated with the type itself.
Example:

// Methods
class Counter {
    var count = 0
    func increment() {
        count += 1
    }
    func increment(by amount: Int) {
        count += amount
    }
    func reset() {
        count = 0
    }
}

Subscripts#

Subscripts can be defined in classes, structs, and enums, providing a shortcut for accessing elements in a collection, list, or sequence.
Subscript syntax (subscript), subscript usage, subscript options, type subscripts (static)

subscript(index: Int) -> Int {
    get {
      // Return an appropriate Int type value
    }
    set(newValue) {
      // Perform appropriate assignment operation
    }
}

// Example
struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// Prints "six times three is 18"

var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2

// Type subscripts
enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
    static subscript(n: Int) -> Planet {
        return Planet(rawValue: n)!
    }
}
let mars = Planet[4]
print(mars)

Inheritance#

Defining a base class, subclass generation, overriding (override), preventing overriding (final)

A class that does not inherit from other classes is called a base class.
Example:

// Inheritance
class SomeClass: SomeSuperclass {
    // Here is the subclass definition
}

class Vehicle {
    var currentSpeed = 0.0
    var description: String {
        return "traveling at \(currentSpeed) miles per hour"
    }
    func makeNoise() {
        // Do nothing—because vehicles do not necessarily make noise
    }
}

class Car: Vehicle {
    var gear = 1
    override var description: String {
        return super.description + " in gear \(gear)"
    }
}

class AutomaticCar: Car {
    override var currentSpeed: Double {
        didSet {
            gear = Int(currentSpeed / 10.0) + 1
        }
    }
}

Initialization Process#

The initialization process is the preparation process before using an instance of a class, struct, or enum.
Initial assignment of stored properties, custom initialization process, default initializer, initializer delegation for value types, class inheritance and initialization process, failable initializers, required initializers

Initializers can complete part of the initialization process of an instance by calling other initializers. This process is called initializer delegation, which avoids code duplication between multiple initializers.
Swift provides two initializers for class types to ensure that all stored properties in an instance receive initial values, called designated initializers and convenience initializers.
You can add one or more failable initializers to the definition of a class, struct, or enum. The syntax is to add a question mark after the init keyword (init?).
A required initializer indicates that all subclasses of that class must implement that initializer by adding the required modifier before the class's initializer.
Example:

// Initialization process
init() {
    // Perform initialization here
}

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

struct Color {
    let red, green, blue: Double
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}

Deinitialization Process#

Deinitializers are only applicable to class types, and a deinitializer is called immediately before an instance of a class is released. Deinitializers are marked with the keyword deinit, similar to how initializers are marked with init.
Swift automatically releases instances that are no longer needed to free up resources.
Example:

// Deinitialization process
deinit {
    // Perform deinitialization here
}

class Bank {
    static var coinsInBank = 10_000
    static func distribute(coins numberOfCoinsRequested: Int) -> Int {
        let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
        coinsInBank -= numberOfCoinsToVend
        return numberOfCoinsToVend
    }
    static func receive(coins: Int) {
        coinsInBank += coins
    }
}

class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = Bank.distribute(coins: coins)
    }
    func win(coins: Int) {
        coinsInPurse += Bank.distribute(coins: coins)
    }
    deinit {
        Bank.receive(coins: coinsInPurse)
    }
}

Optional Chaining#

Optional chaining is a method that allows you to request and call properties, methods, and subscripts on an optional value that might currently be nil.
You can define an optional chain by placing a question mark (?) after the optional value of the property, method, or subscript you want to call, similar to placing an exclamation mark (!) after an optional value to force unwrap its value. The main difference is that when an optional value is nil, optional chaining will simply fail the call, while force unwrapping will trigger a runtime error.
Example:

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}
let john = Person()
let roomCount = john.residence!.numberOfRooms
// This will trigger a runtime error

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

john.residence = Residence()

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."

Error Handling#

Error handling is the process of responding to errors and recovering from them. Swift provides first-class support for throwing, catching, propagating, and manipulating recoverable errors at runtime.
Representing and throwing errors, handling errors, specifying cleanup actions

In Swift, errors are represented by values of types that conform to the Error protocol.
There are four ways to handle errors in Swift. You can propagate errors thrown by functions to the code that calls that function (throws), handle errors with do-catch statements, handle errors as optional types (try?), or assert that an error will not occur (try!).
The defer statement delays the execution of code until just before the current scope exits.
Example:

// Error handling
enum VendingMachineError: Error {
    case invalidSelection                     // Invalid selection
    case insufficientFunds(coinsNeeded: Int) // Insufficient funds
    case outOfStock                             // Out of stock
}
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
    print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
    print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
    print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
    print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."

// Specifying cleanup actions
func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // Process the file.
        }
        // close(file) will be called here, i.e., at the end of the scope.
    }
}

Type Casting#

Type casting in Swift is implemented using the is and as operators. These two operators provide a clear way to check the type of a value or convert its type.
Defining class hierarchies for type casting, checking types (is), downcasting (as? or as!), type casting for Any and AnyObject

Type casting can be used on class and subclass hierarchies to check the type of a specific class instance and convert that class instance's type to other types within that hierarchy.
Swift provides two special type aliases for uncertain types:

  • Any can represent any type, including function types.

  • AnyObject can represent an instance of any class type.

Example:

// Type casting
// A base class MediaItem
class MediaItem {
 var name: String
 init(name: String) {
  self.name = name
 }
}

class Movie: MediaItem {
 var director: String
 init(name: String, director: String) {
  self.director = director
  super.init(name: name)
 }
}

class Song: MediaItem {
 var artist: String
 init(name: String, artist: String) {
  self.artist = artist
  super.init(name: name)
 }
}

let library = [
 Movie(name: "Casablanca", director: "Micheal Curtiz"),
 Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
 Movie(name: "Citizen Kane", director: "Orson Wells"),
 Song(name: "The One And Only", artist: "Chesney Hawkes"),
 Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
var movieCount = 0
var songCount = 0

for item in library {
 if item is Movie {
  movieCount += 1
 } else if item is Song {
  songCount += 1
 }
}

print("Media library contains \(movieCount) movies and \(songCount)")
// Prints "Media library contains 2 movies and 3 songs"

for item in library {
 if let movie = item as? Movie {
  print("Movie: \(movie.name), dir. \(movie.director)")
 } else if let song = item as? Song {
  print("Song: \(song.name), by \(song.artist)")
 }
}
// Movie: Casablanca, dir. Michael Curtiz
// Song: Blue Suede Shoes, by Elvis Presley
// Movie: Citizen Kane, dir. Orson Welles
// Song: The One And Only, by Chesney Hawkes
// Song: Never Gonna Give You Up, by Rick Astley

Nested Types#

Swift allows you to define nested types, which can include nested enums, classes, and structs within supported types.
Practicing nested types, referencing nested types

To nest one type within another, write the definition of the nested type inside the {} of its outer type, and you can define multiple levels of nesting as needed.
Example:

// Nested types
struct BlackjackCard {
 // Nested Suit enum
 enum Suit: Character {
  case spades = "1", hearts = "2", diamonds = "3", clubs = "4"
 }
 
 // Nested Rank enum
 enum Rank: Int {
  case two = 2, three, four, five, six, seven, eight, nine, ten
  case jack, queen, king, ace
  struct Values {
   let first: Int, second: Int?
  }
  var values: Values {
   switch self {
   case .ace:
    return Values(first: 1, second: 11)
   case .jack, .queen, .king:
    return Values(first: 10, second: nil)
   default:
    return Values(first: self.rawValue, second: nil)
   }
  }
 }
 
 // BlackjackCard's properties and methods
 let rank: Rank, suit: Suit
 var description: String {
  var output = "suit is \(suit.rawValue),"
  output += " value is \(rank.values.first)"
  if let second = rank.values.second {
   output += " or \(second)"
  }
  return output
 }
}

let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print("theAceOfSpades: \(theAceOfSpades.description)")
// Prints "theAceOfSpades: suit is 1, value is 1 or 11"

let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// 2

Extensions#

Extensions can add new functionality to an existing class, struct, enum, or protocol.
Extension syntax, computed properties, initializers, methods, subscripts, nested types

Extensions in Swift can:

  • Add computed instance properties and computed type properties

  • Define instance methods and type methods

  • Provide new initializers

  • Define subscripts

  • Define and use new nested types

  • Make existing types conform to a protocol

Extension syntax:

extension SomeType {
  // Add new functionality to SomeType here
}

Extensions can add computed instance properties and computed type properties to existing types.
Extensions can add new initializers to existing types.
Extensions can add new instance methods and type methods to existing types.
Extensions can add new subscripts to existing types.
Extensions can add new nested types to existing classes, structs, and enums.
Example:

// Extension syntax
extension SomeType {
  // Add new functionality to SomeType here
}
// Adding one or more protocols
extension SomeType: SomeProtocol, AnotherProtocol {
  // Implementations required by the protocols go here
}

struct Size {
 var width = 0.0, height = 0.0
}
struct Point {
 var x = 0.0, y = 0.0
}
struct Rect {
 var origin = Point()
 var size = Size()
}

extension Rect {
 init(center: Point, size: Size) {
  let originX = center.x - (size.width / 2)
  let originY = center.y - (size.height / 3)
  self.init(origin: Point(x: originX, y: originY), size: size)
 }
}
let centerRect = Rect(center: Point(x: 4.0, y: 4.0), 
 size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

extension Int {
 func repetitions(task: () -> Void) {
  for _ in 0..<self {
   task()
  }
 }
}
3.repetitions {
 print("Hello!")
}
// Hello!
// Hello!
// Hello!

extension Int {
 mutating func square() {
  self = self * self
 }
}
var someInt = 3
someInt.square()
// someInt is now 9

Protocols#

Protocols define a blueprint that specifies methods, properties, and other requirements for implementing a specific task or functionality.
Classes, structs, or enums can conform to protocols and provide concrete implementations for the requirements defined by the protocol.
Protocol syntax, property requirements, method requirements, mutating method requirements, initializer requirements, protocols as types, delegation, protocol type collections, protocol inheritance, class-only protocols, protocol composition, checking protocol conformance, optional protocol requirements, protocol extensions,

Protocol syntax:

protocol SomeProtocol {
    // This is the definition part of the protocol
}

Protocols can require conforming types to provide specific instance properties or type properties with specific names and types.
Protocols can require conforming types to implement certain specified instance methods or class methods.
In instance methods of value types (i.e., structs and enums), the mutating keyword is used as a prefix before the func keyword to indicate that the method can modify the instance it belongs to and any properties of that instance.
Delegation is a design pattern that allows classes or structs to delegate some functionality they are responsible for to instances of other types.
Example:

// Protocol syntax
protocol SomeProtocol {
    // This is the definition part of the protocol
}

struct SomeStructure: FirstProtocol, AnotherProtocol {
 // This is the definition part of the struct
}

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
 // This is the definition part of the class
}

protocol SomeProtocol {
 var mustBeSettable: Int { get set }
 var doesNotNeedToBeSettable: Int { get }
}

protocol AnotherProtocol {
 static var someTypeProperty: Int { get set }
}

protocol FullyNamed {
 var fullName: String { get }
}

struct Person: FullyNamed {
 var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

class Starship: FullyNamed {
 var prefix: String?
 var name: String
 init(name: String, prefix: String? = nil) {
  self.name = name
  self.prefix = prefix
 }
 
 var fullName: String {
  return (prefix != nil ? prefix! + " " : "") + name
 }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

Generics#

Generic code allows you to write flexible and reusable functions and types that can work with any type based on your custom requirements.
You can avoid writing duplicate code and express the intent of your code in a clear and abstract way.
Generic functions, type parameters, named type parameters, generic types, generic extensions, type constraints, associated types

Example:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", anotherString is now "hello"

Opaque Types#

Functions or methods with opaque return types hide the type information of the return value.
Functions no longer provide a specific type as the return type but describe the return value based on the protocol it supports.
Problems solved by opaque types, returning opaque types, differences between opaque types and protocol types

Hiding type information is very useful when dealing with the relationship between modules and calling code, as the underlying data type returned can remain private.
Opaque types are the opposite of generics. Opaque types allow a function to choose a return type that is independent of the calling code.
If there are multiple places in a function that return an opaque type, all possible return values must be of the same type. The main difference between returning an opaque type and returning a protocol type is whether type consistency is guaranteed.
An opaque type can correspond to only one specific type, even though the function caller does not know which type it is; a protocol type can correspond to multiple types as long as they conform to the same protocol.
Example:

protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result = [String]()
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}
let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
// *
// **
// ***

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
// ***
// **
// *

Automatic Reference Counting#

Swift uses Automatic Reference Counting (ARC) to track and manage memory in your application.
This situation occurs when two class instances hold strong references to each other, causing each instance to keep the other alive. This is known as a strong reference cycle.
Swift provides two ways to resolve strong reference cycle issues you encounter when using class properties: weak references (weak reference) and unowned references (unowned reference).

  • When declaring a property or variable, prefix it with the weak keyword to indicate that it is a weak reference.

  • When declaring a property or variable, prefix it with the unowned keyword to indicate that it is an unowned reference.

Example:

// Automatic Reference Counting practice
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
reference1 = nil
reference2 = nil
reference3 = nil
// Prints "John Appleseed is being deinitialized"

// Strong reference cycle
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john = nil
unit4A = nil

// Weak reference
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
// Prints "John Appleseed is being deinitialized"

Memory Safety#

By default, Swift prevents unsafe behavior in your code.
Understanding memory access conflicts, access conflicts with in-out parameters, access conflicts with self in methods, access conflicts with properties.

Example:

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // Normal
balance(&playerOneScore, &playerOneScore)
// Error: playerOneScore access conflict

Access Control#

Access control can restrict access to your code from other source files or modules.

  • The open and public levels allow entities to be accessed by all entities in the same module source file and also from outside the module by importing that module. Typically, you would use open or public levels to specify the external interface of a framework.

  • The internal level allows entities to be accessed by any entity in the same module source file but not by entities outside the module. Typically, if an interface is only used internally within an application or framework, it can be set to internal level.

  • The fileprivate level restricts access to entities only within the file they are defined in. If part of a feature's implementation details only needs to be used within a file, you can use fileprivate to hide it.

  • The private level restricts access to entities only within their defining scope and extensions within the same file. If part of a feature's details only needs to be used within the current scope, you can use private to hide it.

Open is the highest access level (least restrictive), and private is the lowest access level (most restrictive).
Open can only apply to classes and class members, and the main difference between open and public is that classes and members defined as open can be inherited and overridden outside the module.
Example:

public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}

class SomeInternalClass {}   // Implicitly internal
var someInternalConstant = 0 // Implicitly internal

public class SomePublicClass {                  // Explicit public class
    public var somePublicProperty = 0            // Explicit public class member
    var someInternalProperty = 0                 // Implicitly internal class member
    fileprivate func someFilePrivateMethod() {}  // Explicit fileprivate class member
    private func somePrivateMethod() {}          // Explicit private class member
}

class SomeInternalClass {                       // Implicitly internal class
    var someInternalProperty = 0                 // Implicitly internal class member
    fileprivate func someFilePrivateMethod() {}  // Explicit fileprivate class member
    private func somePrivateMethod() {}          // Explicit private class member
}

fileprivate class SomeFilePrivateClass {        // Explicit fileprivate class
    func someFilePrivateMethod() {}              // Implicitly fileprivate class member
    private func somePrivateMethod() {}          // Explicit private class member
}

private class SomePrivateClass {                // Explicit private class
    func somePrivateMethod() {}                  // Implicitly private class member
}

Advanced Operators#

Swift also provides several advanced operators that can perform complex operations on values. These include bitwise operators and shift operators.
Bitwise operators, overflow operators, precedence and associativity, operator functions, custom operators

Example:

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // Equals 0b11110000

var potentialOverflow = Int16.max
// potentialOverflow's value is 32767, which is the maximum integer Int16 can hold
potentialOverflow += 1
// This will cause an error

struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    static func + (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y + right.y)
    }
}

let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector is a new Vector2D instance with values (5.0, 5.0)
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.