程序代写案例-CMSC 436
时间:2022-03-26
Introduction to Swift
CMSC 436
What is Swift?
Apple’s language for iOS development
Descended from Objective-C
Has a lot of modern PL features
Compiles to native bytecode
Comes with a REPL (Read-Eval-Print-Loop)
I Easy to test basic language features
I You can use it as a calculator
Some Basics
All your favorite C operators (with some twists)
The usual comments: /* */ and //
Blocks: {}
Semicolons are optional!!!
Control flow: if, while, switch, for — but some differences
repeat-while, guard
break, continue
Optionals
Variables and constants
Variables and Constants
var x=1 — This defines a variable
let y=3 — This defines a constant
Where possible, types are inferred, but can be declared explicitly:
var z:Int
Variables are mutable, while constants are not:
x=2 // just fine
y=2 // error!
Integers can be specified with base:
I 11==11 (decimal)
I 0b11==3 (binary)
I 0o11==9 (octal)
I 0x11==17 (hexadecimal)
Variable and constant names can be unicode, so ☕=3 will work
⇒ Use this power wisely!
Optionals
No NULL, but nil
If variable must be nilable ⇒ Optional
var w:Int?
w=3
w=nil
Dealing with Optionals:
if let v = w { ... } //conditional unwrapping
let v = w ?? 8 //nil-coalescing op ??
let v = (w != nil) ? w! : 8 //forced unwrapping op !
Operator Differences
Arithmetic ops will not overflow
⇒ runtime errors
Put an & in front if you want overflow
⇒ maybe you’re creating a trip odometer?
= does not result in a value
⇒ if x = y { ... } // error!
Control Flow
if, guard
if condition1 { // notice lack of parentheses
statements1
} else if condition2 {
statements2
} else {
statements3
}
guard condition else {
statements1 // if condition is false
// must exit enclosing context
}
Control Flow
while,repeat
while condition {
statements
}
repeat {
statements
} while condition
Control Flow
switch
Like C, but:
I no break needed, fallthrough keyword for C-like behavior
I not limited to integers
I comma-separated values
I ranges
Cases must be exhaustive
⇒ you can use default
Control Flow
for and ranges
for i in collection { // i is declared here
...
}
collection can be anything iterable, like an array or a range
(also called an interval)
1...5 // 1,2,3,4,5 (closed interval)
1..<5 // 1,2,3,4 (half-open interval)
1... // 1,2,3,... (one-sided interval)
...1 // not iterable!
What Use are One-Sided Ranges?
for i in 3... {
// do stuff
if done { break }
}
let a = ["a","b","c"]
for b in a[..<2] {
print(a)
}
let r = ..<5
r.contains(-1) // true
r.contains(3) // true
r.contains(12) // false
Arrays and Tuples
Arrays are expandable:
var a = [Int]()
a.append(3)
a+=[4]
var b = a + [5,6,7]
Tuples are fixed-length:
var t = (3,4,5)
Also: Set — like an array, but with unique elements and set
operations like union and intersection
More Tuples
var t = (a:1, b:2) // named tuple
t.a == t[0]
t.b == t[1]
switch t {
case (3,4):
// something
case let(x,y) where x==y:
// something with x,y defined
case (let(x),0):
// something with x defined
case (let(x),let(y)):
// something with x,y defined
}
Strings
Unicode: let r = "®"
Interpolation: let s = "FooBar\(r)"
Indexing:
I s.startIndex s.index(after:s.startIndex)
I s.endIndex s.index(before:s.endIndex)
I s.index(s.startIndex, offsetBy:3)
⇒ These are not integers!
Substrings: s[..⇒ This is not a string!
s.insert("!", at: s.endIndex)
"""
multi-line
string
"""
String Characters
The usual: \n, \r, \\, \", \', \t
\u{#} — unicode (# is in hex)
Unicode can be entered directly
We don’t use single quotes for individual characters:
let a: Character = "a"
Dictionaries
Key-value stores
var d = [Int:String]()
d[1] = "foo"
d[38] = "bar"
var d2:[Int:String] = [1:"foo", 38:"bar"]
Key types must be Hashable
Type Aliasing
Like typedef in C
typealias Count = UInt16
var c:Count
print(Count.self) // UInt16
Functions
func noArgsVoidRet() { ... }
=> noArgsVoidRet()
func noArgsIntRet() -> Int { ... }
=> let i = noArgsIntRet()
func namedArg(foo:Int) { ... }
=> namedArg(foo:3)
func renamedArg(extFoo foo:Int) { print(foo) }
=> renamedArg(extFoo:3)
func unnamedArg(_ foo:Int) { print(foo) }
=> unnamedArg(3)
func defaultArg(foo:Int=3) { print(foo) }
=> defaultArg()
func tupleRet() -> (a:String,b:Int) {
return ("foo",3)
}
func optionalRet() -> Int? {
if blah { return 3 } else { return nil }
}
Function Returns
Can use return keyword
If the function body is one statement, can omit return:
func add(_ a:Int, _ b:Int) -> Int {
a+b
}
Variadic Parameters
Last argument to a function can be variadic
func manyArgs(a:String, b:Int...) {
// b is of type [Int]
}
manyArgs(a:"foo",b:1,2,3)
func manyArgs2(a:String, _ b:Int...) {
// b is of type [Int]
}
manyArgs(a:"foo",1,2,3)
Mutable Parameters
func changeB(a:String, b: inout String) {
b = "\(a)\(b)"
}
var s="this"
changeB(a:"changing ", b:&s)
print(s)
The & creates a pass-by-reference for s
Functions as Objects
Function are first-class objects
They have a type
func foo(a:Int, b:String) -> Bool {...}
has type (a:Int,b:String)->Bool
func bar(_ a:Int,_ b:String) -> Bool {...}
has type (Int,String)->Bool
func baz(f: (Int,String)->Bool) -> Int {...}
let i:Int = baz(f:bar)
Closures
Functions can return functions
func outer() -> (Int) -> Int {
var incr = 0
func inner(_ a:Int) -> Int {
incr += 1
return a+incr
}
return inner
}
inner function captures state from outer
⇒ closure over incr
Safely provides access to state without exposing it
Some Closure Details
Technically, all functions are closures
I Global function: named, doesn’t capture values
I Nested function: named, captures enclosing function state
I Closure expression: unnamed, captures enclosing function
state
{ (a:Int,b:Int)->Bool in return a>b }
{ (a:Int,b:Int)->Bool in a>b }
{ a,b in a>b } //if types inferrable from context
Closure Example: sort
Array::sorted(by: (T,T)->Bool)
let nums = [7,5,14,12,16]
// Sort with inferred types
let snums = nums.sorted(by:{a,b in a>b})
// Sort with positional parameters
let snums2 = nums.sorted(by:{$0>$1})
// Sort with function name
let snums3 = nums.sorted(by:>)
// Sort with a trailing closure
let snums4 = nums.sorted() {$0>$1}
// Sort with parentheses omitted
let snums5 = nums.sorted {$0>$1}
Data Structures
We have several kinds of data structures:
I struct
I class
I enum
I protocol
Structures
Between a C/C++ struct and a C++/Java class:
I Properties (a.k.a. fields)
I Methods
I self
I Default initializers (these are automatic!)
I Memberwise initializers (these are automatic!)
struct Vector {
var x: Double
var y: Double
func length() -> Double {
(x*x + y*y).squareRoot()
}
}
let v = Vector(x:3,y:4)
print( v.x )
print( v.y )
print( v.length() )
Structures are passed by value, which means they are copied
Classes
These look a lot like structures, but
I There are no automatic memberwise initializers
I Class instances are passed by reference
I Classes support deinitializers
There are other differences, which we will cover when they arise
Methods
class MyClass {
func instanceMethod(a:Int) -> String { ... }
static func typeMethod() { ... }
class func overridableTypeMethod() { ... }
init() { ... }
init(x:Int) { ... }
}
let c = MyClass()
c.instanceMethod(a:3)
MyClass.typeMethod()
MyClass.overridableTypeMethod()
let c2 = MyClass(5)
Instance methods reference the instance as self
Structures (and enumerations) can declare a method mutating
to update the original copy
Enumerations
enum MyBoolean {
case True
case False
case FileNotFound
}
enum MyBoolean2 {
case True, False, FileNotFound
}
var b = MyBoolean.True
b = .FileNotFound
Enumerations are passed by value, like structures
Iterable Enumerations
enum Earthsea: CaseIterable {
case AWizardOfEarthsea
case TheTombsOfAtuan
case TheFarthestShore
case Tehanu
}
for book in Earthsea.allCases {
print(book)
}
Enumerations with Raw Values
enum EarthseaByOrder: Int {
case AWizardOfEarthsea=1
case TheTombsOfAtuan
case TheFarthestShore
case Tehanu
}
print(EarthseaByOrder.Tehanu)
print(EarthseaByOrder.Tehanu.rawValue)
let t = EarthseaByOrder(rawValue: 3)
print(t ?? "Not found")
Combining Stuff
enum Earthsea: Int,CaseIterable {
case AWizardOfEarthsea = 1
case TheTombsOfAtuan
case TheFarthestShore
case Tehanu
}
for book in Earthsea.allCases {
print("\(book) \(book.rawValue)")
}
Enumerations with Associated Values
enum Shape {
case circle(center:Point, radius:Double)
case rect(lowerLeft:Point, upperRight:Point)
}
let s = Shape.circle(center:p, radius:1.5)
switch s {
case .circle(let c, let r): ...
case let .rect(ll, ur): ...
}
If you want the enum itself as an associated value, use indirect
before relevant cases, or the entire enum
Protocols
Similar to Java Interface
protocol MyProtocol { ... }
struct MyStruct: MyProtocol { ... }
protocol MyOtherProtocol {
var prop1: Int { get }
var prop2: String { get set }
static var typeProp: Int { get set }
func f() -> Double
static func g() -> String
mutating func h()
}
Stored, Lazy, and Computed Properties
So far, only looked at stored properties
Lazy stored properties aren’t computed until first accessed:
lazy var x = foo()
Computed properties are generated each time:
struct Length {
var len: Int = 0
var half: Double {
get { return Double(len) / 2.0 }
set(newHalf) { len = Int(newHalf * 2) }
}
}
var l = Length(len:10)
print(l.half)
l.half = 12
print(l.len)
Inheritance
Inheritance looks a lot like Java:
I Type name followed by a colon, then comma-separated list of
base, protocols
I Single inheritance, but as many protocols as you need
I Override properties and methods with override
I super to access superclass’s version
I Prevent overriding with final
I Classes can inherit classes, protocols can inherit protocols
There is no common base class, but:
I Any is a type that is satisfied by any data type
I AnyObject is a protocol that is satisfied by any class
Casting
Test an object bar as an instance of type Foo:
bar is Foo (result is a Boolean)
Downcast bar to type Foo:
bar as? Foo (result is an Optional)
bar as! Foo (result is a Foo, or generates runtime error)
In a switch, this can get complex:
switch thing {
case 1 as Int: ...
case let a as String: ...
case let b as Int where b < 10: ...
case is Foo: ...
case let bar as Bar: ...
default: ...
}
Extensions
Allows us to add functionality (including protocols) to an existing
type or object:
extension Int {
func square() -> Int { self*self }
}
print(3.square())
You can add properties, methods, protocols, nested types, ...
Error Handling
Similar to Java’s exceptions:
func foo() throws -> Int {
// ... some stuff
throw MyErrorType()
}
do {
let x = try foo()
} catch is MyErrorType { ... }
do {
let x = try foo()
} catch let e: MyErrorType { ... }
Error Handling
Optionals, of course:
// x is of type Int?
let x = try? foo()
// y is of type Int
let y = try! foo() // runtime error if throws
// z is of type Int in the block
if let z = try? foo() { ... }
Error Handling
The defer keyword defines a block executed at the end of scope
Similar to finally in Java
The block can be anywhere in the appropriate scope, and is useful
for ensuring cleanup
Reference Counting
Swift uses Automatic Reference Counting :
I allocate correctly, and it does everything for you
I works for classes and closures (reference data)
I does not work for structures and enumerations (value data)
Object has no strong references ⇒ reclaimed
Strong references via
I properties
I constants
I variables
How Reference Counting Works
Create class instance ⇒ memory allocated
Instance is one reference
Instance/property passed to another class ⇒ reference count
increased
Methods/objects holding references go away ⇒ reference count
decreased
Reference count reaches 0 ⇒ memory freed
How Reference Counting Works
Create class instance ⇒ memory allocated
Instance is one reference
Instance/property passed to another class ⇒ reference count
increased
Methods/objects holding references go away ⇒ reference count
decreased
Reference count reaches 0 ⇒ memory freed
How Reference Counting Works
Create class instance ⇒ memory allocated
Instance is one reference
Instance/property passed to another class ⇒ reference count
increased
Methods/objects holding references go away ⇒ reference count
decreased
Reference count reaches 0 ⇒ memory freed
How Reference Counting Works
Create class instance ⇒ memory allocated
Instance is one reference
Instance/property passed to another class ⇒ reference count
increased
Methods/objects holding references go away ⇒ reference count
decreased
Reference count reaches 0 ⇒ memory freed
How Reference Counting Works
Create class instance ⇒ memory allocated
Instance is one reference
Instance/property passed to another class ⇒ reference count
increased
Methods/objects holding references go away ⇒ reference count
decreased
Reference count reaches 0 ⇒ memory freed
Object Lifecycle
1. Allocation (from stack or heap)
2. Initialization (init() method)
3. Usage
4. Deinitialization (deinit() method)
5. Deallocation (memory returned)
What Happens Here?
class Person { // references: 0
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 0
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
What Happens Here?
class Person { // references: 1
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 0
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
Allocate a new Person
Strong reference from john
What Happens Here?
class Person { // references: 1
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 1
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
Allocate a new Apartment
Strong reference from unit4A
What Happens Here?
class Person { // references: 1
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 2
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
Pass a unit4A reference to john
Strong reference
What Happens Here?
class Person { // references: 2
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 2
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
Pass a john reference to unit4a
Strong reference
What Happens Here?
class Person { // references: 1
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 2
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
Remove the strong reference from john
What Happens Here?
class Person { // references: 1
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 1
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
Remove the strong reference from unit4A
What Happens Here?
class Person { // references: 1
let name:String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment { // references: 1
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john:Person? = Person(name: "John Doe")
var unit4A:Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
We still have strong references!
Reference Loops
We have a Strong Cycle
Reference count never hits 0 ⇒ objects never deinitialized!
We can resolve this with Weak References or Unowned References
Weak Unowned
nil-able? yes no
Optional? required no
referenced thing’s lifetime shorter longer
Only strong references contribute to reference count
Unowned references can lead to bad dereferences!
Declaring Weak/Unowned References
class Person {
let name: String
var apartment: Apartment?
var card: CreditCard?
init(name: String) { self.name = name }
}
class Apartment {
let unit: String
weak var tenant: Person?
init(unit: String) { self.unit = unit }
}
class CreditCard {
let number: UInt64
unowned let customer: Person
init(number: UInt64, customer: Person) {
self.number = number; self.customer = customer
}
}
var john: Person? = Person(name: "John")
var unit4A = Apartment(unit: "4A")
let card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john!.apartment = unit4A; unit4A.tenant = john
john!.card = card
If John is erased from existence by a shadowy cabal (john=nil):
unit4A.tenant will be nil
card.customer will generate a runtime error!
A Third Case
Both weak and unowned references require an Optional on one side
What if this isn’t possible?
class Country {
let name: String
var capitalCity: City
init(nameIn: String, capitalName: String) {
name = nameIn
capitalCity = City(nameIn: capitalName, countryIn: self)
}
}
class City {
let name: String
let country: Country
init(nameIn: String, countryIn: Country) {
name = nameIn
country = countryIn
}
}
var country = Country(nameIn: "Bangladesh", capitalName: "Dhaka")
This is a problem; let’s see why
How Initialization Works
1. Phase 1
1.1 Initializer ensures all properties have values
1.2 Superclass initializer does the same (all the way up the chain)
1.3 Initialization is now “complete”
2. Phase 2
2.1 Superclass initializer may do more work
2.2 Initializer may do more work
I self may be accessed
I properties may be modified
I instance methods may be called
Why Our Previous Code Doesn’t Work
class Country {
let name: String
var capitalCity: City
init(nameIn: String, capitalName: String) {
name = nameIn
capitalCity = City(nameIn: capitalName, countryIn: self)
}
}
class City {
let name: String
let country: Country
init(nameIn: String, countryIn: Country) {
name = nameIn
country = countryIn
}
}
var country = Country(nameIn: "Bangladesh", capitalName: "Dhaka")
We’re trying to use self before Phase 1 completes!
Fixing With Unowned and Implicitly Unwrapped Optionals
class Country {
let name: String
var capitalCity: City!
init(nameIn: String, capitalName: String) {
name = nameIn
capitalCity = City(nameIn: capitalName, countryIn: self)
}
}
class City {
let name: String
unowned var country: Country
init(nameIn: String, countryIn: Country) {
name = nameIn
country = countryIn
}
}
var country = Country(nameIn: "Bangladesh", capitalName: "Dhaka")
City holds an unowned reference to Country
Country holds an Implicitly Unwrapped Reference to City
Optional Country.capitalCity can be initialized with nil
Assigned to non-nil instance during Phase 2 (self is available)
Closures and Reference Cycles
class Person {
var firstName: String?
var lastName: String?
lazy var fullName: ()->String = {
[unowned self]
return ("\(self.firstName!) \(self.lastName!)")
}
}
Lazy properties are not initialized until they’re called
Specifying unowned or weak in capture list prevents strong
cycles in captures
Generics
Similar to Java:
struct Stack {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
var tower: Stack
func swapTwoValues(_ a: inout T, _ b: inout T) {
let tempA = a
a = b; b = tempA
}
Generic Extensions
The name of the type parameter must match the definition!
extension Stack {
var topItem: Element? { // Correct!
return items.isEmpty ? nil : items[items.count - 1]
}
var bottomItem: T? { // Wrong!
return items.isEmpty ? nil : items[0]
}
}
Type Associations
Protocols can use associatedtype to make them generic-like:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct Stack: Container {
var items = [Element]()
mutating func append(_ item: Element) {
self.push(item)
}
// ...
}
Item is inferred to be of type Element, which itself will be
bound in a concrete instance
Where Clauses
func allItemsMatch
(_ a: C1, _ b: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
// implementation
// we already know a and b have the same type
}
Constrains when this generic is valid, so the compiler can catch
errors
Equatable protocol requires == and != operators
Can use these with classes, structs, enumerations, and functions
Where Clauses in Protocols and Extensions
Can also use them in protocol associatedtypes!
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where
Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
// ...
}
}