본문 바로가기

프로그래밍

[Swift] boostcourse로 시작하는 swift 문법 - 2

 

< 2단원. 다양한 표현 및 확장 >

1. 구조체

- Swift 대부분의 타입은 구조체로 이루어져 있다.

- 구조체는 값(value) 타입이다.

- 타입명은 대문자 Camel Case를 사용하여 정의한다.

struct 이름 {
	/* 구현부 */
}

e.g.)
struct Sample {
	// 가변 프로퍼티(값 변경 가능)
    var mutableProperty: Int = 100
    
    // 불변 프로퍼티(값 변경 불가능)
    let immutableProperty: Int = 100
    
    // 타입 프로퍼티(static 키워드 사용: 타입 자체가 사용하는 프로퍼티)
    static var typeProperty: Int = 100
    
    // 인스턴스 메서드(인스턴스가 사용하는 메서드)
    func instanceMethod() {
    	print("instance method")
    }
    
    // 타입 메서드(static 키워드 사용: 타입 자체가 사용하는 메서드)
    static func typeMethod() {
    	print("type method")
    }
}

// 가변 인스턴스 생성
var mutable: Sample = Sample()

mutable.mutableProperty = 200
mutable.immutableProperty = 200 // 불변 프로퍼티는 인스턴스 생성 후 수정 불가능. 컴파일 오류발생

// 불변 인스턴스 생성
let immutable: Sample = Sample()

// 불변 인스턴스는 가변 프로퍼티라도 인스턴스 생성 후에 수정 불가능
immutable.mutableProperty = 200 // 컴파일 오류발생
immutable.immutableProperty = 200 // 컴파일 오류발생

// 타입 프로퍼티 및 메서드
Sample.typeProperty = 300
Sample.typeMethod()

mutable.typeProperty = 400 // 인스턴스에서는 타입 프로퍼티 사용 불가능. 컴파일 오류발생
mutable.typeMethod() // 인스턴스에서는 타입 메서드 사용 불가능. 컴파일 오류발생

 

2. 클래스

- 클래스는 참조(reference) 타입이다.

- 타입명은 대문자 Camel Case를 사용하여 정의한다.

- Swift의 클래스는 다중 상속이 불가능하다.

class 이름 {
	/* 구현부 */
}

e.g.)
class Sample {
	// 가변 프로퍼티
    var mutableProperty: Int = 100

    // 불변 프로퍼티
    let immutableProperty: Int = 100

    // 타입 프로퍼티
    static var typeProperty: Int = 100

    // 인스턴스 메서드
    func instanceMethod() {
        print("instance method")
    }

    // 타입 메서드
    // 상속 시 재정의 불가 타입 메서드 - static
    static func typeMethod() {
        print("type method - static")
    }

    // 상속 시 재정의 가능 타입 메서드 - class
    class func classMethod() {
        print("type method - class")
    }
}

// 인스턴스 생성 - 참조정보 수정 가능
var mutableReference: Sample = Sample()

mutableReference.mutableProperty = 200

// 불변 프로퍼티는 인스턴스 생성 후 수정할 수 없습니다.
mutableReference.immutableProperty = 200 // 컴파일 오류발생

// 인스턴스 생성 - 참조정보 수정 불가
let immutableReference: Sample = Sample()

// ** 클래스의 인스턴스는 참조 타입이므로 let으로 선언되었더라도 인스턴스 프로퍼티의 값 변경이 가능합니다.
immutableReference.mutableProperty = 200

// 다만 참조정보를 변경할 수는 없습니다.
immutableReference = mutableReference // 컴파일 오류발생

// 참조 타입이라도 불변 인스턴스는 인스턴스 생성 후에 수정할 수 없습니다.
immutableReference.immutableProperty = 200 // 컴파일 오류발생

// 타입 프로퍼티 및 메서드
Sample.typeProperty = 300
Sample.typeMethod()

// 인스턴스에서는 타입 프로퍼티나 타입 메서드를 사용할 수 없습니다.
mutableReference.typeProperty = 400 // 컴파일 오류발생
mutableReference.typeMethod() // 컴파일 오류발생

 

3. 열거형

* Swift의 열거형은 다른 언어와는 다르게 강력한 기능을 지니고 있다.

- 유사한 종류의 여러 값을 한 곳에 모아서 정의한 것이다.

- enum 자체가 하나의 데이터 타입으로 대문자 Camel Case를 사용하여 이름을 정의한다.

- 각 case는 소문자 Camel Case로 정의한다.

- 각 case는 그 자체가 고유의 값이다. (각 case에 자동으로 정수값이 할당되지 않는다.)

- 각 case는 한줄에 개별로도, 한줄에 여러개도 정의할 수 있다.

enum 이름 {
	case 이름1
    case 이름2
    case 이름3, 이름4, 이름5
}

 

- 타입이 명확할 경우 열거형 이름은 생략 가능하다.

enum Weekday {
	case mon
    case tue
    case wed
    case thu, fri, sat, sun
}

// 열거형 타입과 케이스 모두 사용 가능
var day: Weekday = Weekday.mon

// 타입이 명확할 경우 케이스처럼 표현 가능
day = .tue
print(day) // tue

// switch의 비교값에 열거형 타입이 위치할 때 모든 열거형 케이스를 포함한다면 default 생략 가능
switch day {
case .mon, .tue, .wed, .thu:
	print("평일입니다")
case Weekday.fri:
	print("불금입니다")
case .sat, .sun:
	print("신나는 주말입니다")
}

 

- rawValue(원시값) : C언어의 enum처럼 정수값을 가질 수 있다. case별로 각각 다른 값을 가져야 하며, 지정되지 않은 경우 자동으로 1이 증가된 값이 할당된다.

enum Fruit: Int {
	case apple = 0
    case grape = 1
    case peach
    
    case mango = 0 // mango와 apple의 원시값이 같으므로 컴파일 오류발생
}
print("Fruit.peach.rawValue == \(Fruit.peach.rawValue)") // Fruit.peach.rawValue == 2

// Hashable 프로토콜을 따르는 모든 타입을 원시값의 타입으로 지정할 수 있다.
enum School: String {
	case elementary = "초등"
    case middle = "중등"
    case high = "고등"
    case university
}
// 열거형의 원시값 타입이 String일 때, 원시값이 지정되지 않았다면 case의 이름을 원시값으로 사용
print("School.university.rawValue == \(School.university.rawValue)") // School.university.rawValue == university

 

- rawValue를 통해 초기화 할 수 있으며, rawValue가 case에 해당하지 않을 수 있으므로 rawValue를 통해 초기화 한 인스턴스는 Optional 타입이다.

let apple: Fruit = Fruit(rawValue: 0) // rawValue를 통해 초기화 한 열거형 값은 optional type 컴파일 오류발생
let apple: Fruit? = Fruit(rawValue: 0)

// if let 구문을 사용하면 rawValue에 해당하는 케이스를 바로 사용할 수 있다.
if let orange: Fruit = Fruit(rawValue: 5) {
	print("rawValue 5에 해당하는 케이스는 \(orange)입니다")
} else {
	print("rawValue 5에 해당하는 케이스가 없습니다")
} // rawValue 5에 해당하는 케이스가 없습니다

 

- Swift의 열거형에는 메서드도 추가할 수 있다.

enum Month {
    case dec, jan, feb
    case mar, apr, may
    case jun, jul, aug
    case sep, oct, nov

    func printMessage() {
        switch self {
        case .mar, .apr, .may:
            print("따스한 봄")
        case .jun, .jul, aug:
            print("무더운 여름")
        case .sep, .oct, .nov:
            print("선선한 가을")
        case .dec, .jan, .feb:
            print("추운 겨울")
        }
    }
}
Month.mar.printMessage() // 따스한 봄

 

4. 클래스 vs 구조체/열거형

- 클래스는 참조 타입, 열거형과 구조체는 값 타입

- 클래스는 상속(subclassing)이 가능하지만 열거형과 구조체는 불가능

* 값 타입(Value Type) : 데이터를 전달할 때 값을 복사하여 전달

* 참조 타입(Reference Type) : 데이터를 전달할 때 값의 메모리 위치를 전달

struct ValueType {
    var property = 1
}

class ReferenceType {
    var property = 1
}

// 첫번째 구조체 인스턴스
let firstStructInstance = ValueType()

// 두번째 구조체 인스턴스에 첫번째 인스턴스 값 복사
var secondStructInstance = firstStructInstance

// 두번째 구조체 인스턴스 프로퍼티 값 수정
secondStructInstance.property = 2

// 두번째 구조체 인스턴스는 첫번째 구조체를 똑같이 복사한 별도의 인스턴스이기 때문에 두번째 구조체 인스턴스의 프로퍼티 값을 변경해도 첫번째 구조체 인스턴스의 프로퍼티 값에는 영향이 없음
print("first struct instance property : \(firstStructInstance.property)") // 1
print("second struct instance property : \(secondStructInstance.property)") // 2

// 클래스 인스턴스 생성 후 첫번째 참조 생성
let firstClassReference = ReferenceType()

// 두번째 참조 변수에 첫번째 참조 할당
let secondClassReference = firstClassReference
secondClassReference.property = 2

// 두번째 클래스 참조는 첫번째 클래스 인스턴스를 참조하기 때문에 두번째 참조를 통해 인스턴스의 프로퍼티 값을 변경하면, 첫번째 클래스 인스턴스의 프로퍼티 값을 변경하게 됨
print("first class reference property : \(firstClassReference.property)") // 2
print("second class reference property : \(secondClassReference.property)") // 2

 

* 값 타입(Value Type)을 사용하는 경우

- 연관된 몇몇의 값들을 모아서 하나의 데이터 타입으로 표현하고 싶은 경우

- 다른 객체 또는 함수 등으로 전달될 때 참조가 아니라 복사할 경우

- 자신을 상속할 필요가 없거나, 다른 타입을 상속 받을 필요가 없는 경우

* Swift에서의 사용

- Swift의 기본 데이터 타입은 모두 구조체로 구현되어 있다. (Swift는 구조체와 열거형 사용을 선호함)

- Apple 프레임 워크의 대부분의 큰 뼈대는 모두 클래스로 구성되어 있다.

 

5. 클로저 기본

- 클로저(Closure) : 실행 가능한 코드 블럭. 이름 정의가 필요하지 않다는 것 외에 함수와 동일하다. (함수는 클로저의 일종으로 이름이 있는 클로저라고 한다. 일급 객체)

{ (매개변수 목록) -> 반환 타입 in
	실행 코드
}

e.g.)
let sum: (Int, Int) -> Int = { (a: Int, b: Int) in
	return a + b
}

let sumResult: Int = sum(1, 2)
print(sumResult) // 3

 

- 클로저는 함수 내부에서 원하는 코드 블럭을 실행할 수 있기 때문에 주로 함수의 전달인자로 많이 사용된다.

let add: (Int, Int) -> Int
add = { (a: Int, b: Int) in
    return a + b
}

let substract: (Int, Int) -> Int
substract = { (a: Int, b: Int) in
    return a - b
}

let divide: (Int, Int) -> Int
divide = { (a: Int, b: Int) in
    return a / b
}

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}

var calculated: Int

calculated = calculate(a: 50, b: 10, method: add)

print(calculated) // 60

calculated = calculate(a: 50, b: 10, method: substract)

print(calculated) // 40

calculated = calculate(a: 50, b: 10, method: divide)

print(calculated) // 5

// 따로 클로저를 상수/변수에 넣어 전달하지 않고, 함수를 호출할 때 클로저를 작성하여 전달할 수도 있습니다.
calculated = calculate(a: 50, b: 10, method: { (left: Int, right: Int) -> Int in
    return left * right
})

print(calculated) // 500

 

6. 클로저 심화

- 클로저는 아래 규칙을 통해 다양한 모습으로 표현 가능하다.

1) 후행 클로저(trailing closure) : 클로저가 함수의 마지막 전달인자일 때, 마지막 매개변수 이름을 생략한 후 함수 소괄호 외부에 클로저를 구현할 수 있다.

result = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int In
    return left + right
}

print(result) // 20

 

2) 반환 타입 생략 : 컴파일러가 클로저의 타입을 유추할 수 있는 경우 매개변수, 반환 타입은 생략이 가능하다. (단, in 키워드는 생략 불가능)

result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) in
    return left + right
})

print(result) // 20

// 후행 클로저와 함께 사용할 수 있다.
result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
    return left + right
}

print(result) // 20

 

3) 단축 인자 이름 : 전달인자의 이름이 굳이 필요없고, 컴파일러가 타입을 유추할 수 있는 경우 축약된 전달인자 이름($0, $1, $2...)을 사용할 수 있다.

result = calculate(a: 10, b: 10, method: {
    return $0 + $1
})

print(result) // 20

// 당연히 후행 클로저와 함께 사용할 수 있다.
result = calculate(a: 10, b: 10) {
    return $0 + $1
}

print(result) // 20

 

4) 암시적 반환 표현 : 반환값이 있는 경우, 암시적으로 클로저의 맨 마지막 줄은 return 키워드를 생략하더라도 반환값으로 취급한다.

result = calculate(a: 10, b: 10) {
    $0 + $1
}

print(result) // 20

// 간결하게 한 줄로 표현해 줄 수도 있다.
result = calculate(a: 10, b: 10) { $0 + $1 }

print(result) // 20

 

7. 프로퍼티

- 종류 : 인스턴스 저장 프로퍼티, 타입 저장 프로퍼티, 인스턴스 연산 프로퍼티, 타입 연산 프로퍼티, 지연 저장 프로퍼티

- 구조체, 클래스, 열거형 내부에 구현 가능하다. (다만, 열거형 내부에는 연산 프로퍼티만 구현 가능)

- 연산 프로퍼티는 var로만 선언 가능하며, 읽기 전용으로는 구현할 수 있지만, 쓰기 전용은 구현할 수 없다.

- 읽기 전용으로 구현하려면 get 블럭만 작성해주면 된다. (생략 가능)

- 읽기, 쓰기 모두 가능하게 하려면 get, set 블럭을 모두 구현하면 된다.

struct Student {

    // 인스턴스 저장 프로퍼티
    var name: String = ""
    var 'class': String = "Swift"
    var koreanAge: Int = 0

    // 인스턴스 연산 프로퍼티
    var westernAge: Int {
        get {
            return koreanAge - 1
        }

        set(inputValue) {
            koreanAge = inputValue + 1
        }
    }

    // 타입 저장 프로퍼티
    static var typeDescription: String = "학생"

    /*
    // 인스턴스 메서드
    func selfIntroduce() {
        print("저는 \(self.class)반 \(name)입니다")
    }
    */

    // 읽기 전용 인스턴스 연산 프로퍼티
    // 간단히 위의 selfIntroduce() 메서드를 대체할 수 있다.
    var selfIntroduction: String {
        get {
            return "저는 \(self.class)반 \(name)입니다"
        }
    }

    /*
    // 타입 메서드
    static func selfIntroduce() {
        print("학생타입입니다")
    }
    */

    // 읽기 전용 타입 연산 프로퍼티
    // 읽기 전용에서는 get을 생략할 수 있다.
    static var selfIntroduction: String {
        return "학생타입입니다"
    }
}

// 타입 연산 프로퍼티 사용
print(Student.selfIntroduction) // 학생타입입니다

// 인스턴스 생성
var noey: Student = Student()
noey.koreanAge = 10

// 인스턴스 저장 프로퍼티 사용
noey.name = "noey"
print(noey.name) // noey

// 인스턴스 연산 프로퍼티 사용
print(noey.selfIntroduction) // 저는 Swift반 noey입니다

print("제 한국나이는 \(noey.koreanAge)살이고, 미국나이는 \(noey.westernAge)살입니다.") // 제 한국나이는 10살이고, 미국나이는 9살입니다.
e.g.)
struct Money {
    var currencyRate: Double = 1100
    var dollar: Double = 0
    var won: Double {
        get {
            return dollar * currencyRate
        }
        set {
            dollar = newValue / currencyRate
        }
    }
}

var moneyInMyPocket = Money()

moneyInMyPocket.won = 11000

print(moneyInMyPocket.won) // 11000

moneyInMyPocket.dollar = 10

print(moneyInMyPocket.won) // 11000

 

- 저장 프로퍼티와 연산 프로퍼티의 기능은 함수, 메서드, 클로저, 타입 등의 외부에 위치한 지역/전역 변수에도 모두 사용이 가능하다.

var a: Int = 100
var b: Int = 200
var sum: Int {
    return a + b
}

print(sum) // 300

 

8. 프로퍼티 감시자(Property Observer)

- 프로퍼티의 값이 변경될 때 원하는 동작을 수행할 수 있다.

- 값이 변경되기 직전에 willSet 블럭이, 값이 변경된 직후에 didSet 블럭이 호출된다. (둘 중 하나만 구현해도 무방)

- 변경되려는 값이 기존값과 동일해도 프로퍼티 감시자는 항상 동작한다.

- willSet 블럭에서는 암시적 매개변수 newValue, didSet 블럭에서는 oldValue를 사용할 수 있다.

- 프로퍼티 감시자는 연산 프로퍼티에는 사용할 수 없다. (저장된 값 변경 시 사용되기 때문에)

- 함수, 메서드, 클로저, 타입 등의 지역/전역 변수에 모두 사용 가능하다.

struct Money {
    // 프로퍼티 감시자 사용
    var currencyRate: Double = 1100 {
        willSet(newRate) {
            print("환율이 \(currencyRate)에서 \(newRate)으로 변경될 예정입니다")
        }

        didSet(oldRate) {
            print("환율이 \(oldRate)에서 \(currencyRate)으로 변경되었습니다")
        }
    }

    // 프로퍼티 감시자 사용
    var dollar: Double = 0 {
        // willSet의 암시적 매개변수 이름 newValue
        willSet {
            print("\(dollar)달러에서 \(newValue)달러로 변경될 예정입니다")
        }

        // didSet의 암시적 매개변수 이름 oldValue
        didSet {
            print("\(oldValue)달러에서 \(dollar)달러로 변경되었습니다")
        }
    }

    // 연산 프로퍼티
    var won: Double {
        get {
            return dollar * currencyRate
        }

        set {
            dollar = newValue / currencyRate
        }

        /* 프로퍼티 감시자와 연산 프로퍼티 기능은 동시에 사용할 수 없다.
        willSet {

        }
        */
    }
}

var moneyInMyPocket: Money = Money()

// 환율이 1100.0에서 1150.0으로 변경될 예정입니다
moneyInMyPocket.currencyRate = 1150
// 환율이 1100.0에서 1150.0으로 변경되었습니다

// 0.0달러에서 10.0달러로 변경될 예정입니다
moneyInMyPocket.dollar = 10
// 0.0달러에서 10.0달러로 변경되었습니다

print(moneyInMyPocket.won) // 11500.0

 

9. 상속(Inheritance)

- 클래스, 프로토콜 등에서 가능하며, 열거형, 구조체는 상속이 불가능하다.

- Swift의 클래스는 단일 상속으로 다중 상속을 지원하지 않는다.

class 이름: 상속받을 클래스 이름 {
	/* 구현부 */
}

- final 키워드를 사용하면 재정의(override)를 방지할 수 있다.

- static 키워드를 사용해 타입 메서드를 만들면 재정의가 불가능하다.

- class 키워드를 사용해 타입 메서드를 만들면 재정의가 가능하다.

- final class는 static 키워드를 사용한 것과 동일하게 동작한다. (재정의가 불가능하다)

- override 키워드를 사용해 부모 클래스의 메서드를 재정의 할 수 있다.

// 기반 클래스 Person
class Person {
    var name: String = ""

    func selfIntroduce() {
        print("저는 \(name)입니다")
    }

    // final 키워드를 사용하여 재정의를 방지할 수 있다.
    final func sayHello() {
        print("hello")
    }

    // 타입 메서드
    // 재정의 불가 타입 메서드 - static
    static func typeMethod() {
        print("type method - static")
    }

    // 재정의 가능 타입 메서드 - class
    class func classMethod() {
        print("type method - class")
    }

    // 재정의 가능한 class 메서드라도 final 키워드를 사용하면 재정의 할 수 없다.
    // 메서드 앞의 'static'과 'final class'는 똑같은 역할을 한다.
    final class func finalClassMethod() {
        print("type method - final class")
    }
}

// Person을 상속받는 Student
class Student: Person {
    var major: String = ""

    override func selfIntroduce() {
        print("저는 \(name)이고, 전공은 \(major)입니다")
    }

    override class func classMethod() {
        print("overriden type method - class")
    }

    // static을 사용한 타입 메서드는 재정의 할 수 없다.
    override static func typeMethod() { } // 컴파일 오류발생

    // final 키워드를 사용한 메서드, 프로퍼티는 재정의 할 수 없다.
    override func sayHello() { } // 컴파일 오류발생
    override class func finalClassMethod() { } // 컴파일 오류발생
}

let noey: Person = Person()
let hana: Student = Student()

noey.name = "noey"
hana.name = "hana"
hana.major = "Swift"

noey.selfIntroduce() // 저는 noey입니다

hana. selfIntroduce() // 저는 hana이고, 전공은 Swift입니다

Person.classMethod() // type method - class

Person.typeMethod() // type method - static

Person.finalClassMethod() // type method - final class

Student.classMethod() // overriden type method - class

Student.typeMethod() // type method - static

Student.finalClassMethod() // type method - final class

 

10. 인스턴스 생성(init) / 소멸(deinit)

- Swift의 모든 인스턴스는 초기화와 동시에 모든 프로퍼티에 유효한 값이 할당되어 있어야 한다.

- 프로퍼티에 미리 기본값을 할당해두면 인스턴스가 생성됨과 동시에 초기값을 지니게 된다.

class PersonA {
    // 모든 저장 프로퍼티에 기본값 할당
    var name: String = "unknown"
    var age: Int = 0
    var nickName: String = "nick"
}

// 인스턴스 생성
let jason: PersonA = PersonA()

// 기본값이 인스턴스가 지녀야 할 값과 맞지 않다면 생성된 인스턴스의 프로퍼티에 각각 값 할당
jason.name = "jason"
jason.age = 30
jason.nickName = "j"

 

- 이니셜라이저(Initializer) : 프로퍼티 초기값을 지정하기 어려운 경우, init을 통해 인스턴스가 가져야 할 초기값을 전달할 수 있다.

class PersonB {
    var name: String
    var age: Int
    var nickName: String

    // 이니셜라이저
    init(name: String, age:Int, nickName: String) {
        self.name = name
        self.age = age
        self.nickName = nickName
    }
}

let hana: PersonB = PersonB(name: "hana", age: 20, nickName: "하나")

// 프로퍼티의 초기값이 꼭 필요 없을 때
// - 옵셔널을 사용
// - class 내부의 init을 사용할 때는 convenience 키워드 사용
class PersonC {
    var name: String
    var age: Int
    var nickName: String?

    init(name: String, age: Int, nickName: String) {
        self.name = name
        self.age = age
        self.nickName = nickName
    }

    // 위와 동일한 기능 수행
    convenience init(name: String, age:Int, nickName: String) {
        self.init(name: name, age: age)
        self.nickName = nickName
    }

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let jenny: PersonC = PersonC(name: "jenny", age: 10)
let mike: PersonC = PersonC(name: "mike", age: 15, nickName: "m")

// 암시적 추출 옵셔널은 인스턴스 사용에 꼭 필요하지만 초기값을 할당하지 않고자 할 때 사용
class Puppy {
    var name: String
    var owner: PersonC!

    init(name: String) {
        self.name = name
    }

    func goOut() {
        print("\(name)가 주인 \(owner.name)와 산책을 합니다")
    }
}

let happy: Puppy = Puppy(name: "happy")

// happy.goOut() // owner가 없는 상태라 컴파일 오류발생

happy.owner = jenny
happy.goOut() // happy가 주인 jenny와 산책을 합니다

 

- 이니셜라이저 매개변수로 전달되는 초기값이 잘못된 경우 인스턴스 생성에 실패할 수 있으며, nil을 반환한다.

- 실패 가능한 이니셜라이저의 반환 타입은 옵셔널 타입이다. (init?을 사용)

class PersonD {
    var name: String
    var age: Int
    var nickName: String?

    init?(name: String, age: Int) {
        if (0...120).contains(age) == false {
            return nil
        }

        if name.count == 0 {
            return nil
        }

        self.name = name
        self.age = age
    }
}

// let john: PersonD = PersonD(name: "john", age: 23) // 컴파일 오류발생
let john: PersonD? = PersonD(name: "john", age: 23)
let john: PersonD? = PersonD(name: "joker", age: 123)
let batman: PersonD? = PersonD(name: "", age: 10)

print(joker) // nil
print(batman) // nil

 

- 디이니셜라이저(deinitializer) : deinit은 클래스의 인스턴스가 메모리에서 해제되는 시점에 자동으로 호출된다. 매개변수를 지닐 수 없으며, 클래스 타입에만 구현 가능하다.

- 인스턴스가 메모리에서 해제되는 시점은 ARC(Automatic Reference Counting)의 규칙에 따라 결정.

(참고 : https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/)

class PersonE {
    var name: String
    var pet: Puppy?
    var child: PersonC

    init(name: String, child: PersonC) {
        self.name = name
        self.child = child
    }

    // 인스턴스가 메모리에서 해제되는 시점에 자동 호출
    deinit {
        if let petName = pet?.name {
            print("\(name)가 \(child.name)에게 \(petName)을 인도합니다")
            self.pet?.owner = child
        }
    }
}

var donald: PersonE? = PersonE(name: "donald", child: jenny)
donald?.pet = happy
donald = nil // donald 인스턴스가 더이상 필요없으므로 메모리에서 해제된다.
// donald가 jenny에게 happy를 인도합니다.
 

11. 옵셔널 체이닝(Optional Chaining) & nil 병합 연산자

- 옵셔널 체이닝(Optional Chaining) : 옵셔널 내부의 내부의 내부로 옵셔널이 연결되어 있을 때 매번 nil 확인을 하지 않고 최종적으로 원하는 값이 있는지 없는지 확인할 수 있다.

class Person {
    var name: String
    var job: String?
    var home: Apartment?

    init(name: String) {
        self.name = name
    }
}

class Apartment {
    var buildingNumber: String
    var roomNumber: String
    var 'guard': Person?
    var owner: Person?

    init(dong: String, ho: String) {
        buildingNumber = dong
        roomNumber = ho
    }
}

// 옵셔널 체이닝 사용
let noey: Person? = Person(name: "noey")
let apart: Apartment? = Apartment(dong: "101", ho: "202")
let superman: Person? = Person(name: "superman")

// 옵셔널 체이닝이 실행 후 결과값이 nil일 수 있으므로 결과 타입도 옵셔널입니다.

// 옵셔널 체이닝을 사용하지 않는 경우
func guardJob(owner: Person?) {
    if let owner = owner {
        if let home = owner.home {
            if let 'guard' = home.guard {
                if let guardJob = 'guard'job {
                    print("우리집 경비원의 직업은 \(guardJob)입니다")
                } else {
                    print("우리집 경비원은 직업이 없어요")
                }
            }
        }
    }
}

guardJob(owner.noey)

// 옵셔널 체이닝을 사용할 경우
func guardJobWithOptionalChaining(owner: Person?) {
    if let guardJob = owner?.home?.guard?.job {
        print("우리집 경비원의 직업은 \(guardJob)입니다")
    } else {
        print("우리집 경비원은 직업이 없어요")
    }
}

guardJobWithOptionalChaining(owner: noey) // 우리집 경비원은 직업이 없어요

noey?.home?.guard?.job // nil

noey?.home = apart

noey?.home // Optional(Apartment)
noey?.home?.guard // nil

// 경비원 할당
noey?.home?.guard = superman

noey?.home?.guard // Optional(Person)

noey?.home?.guard?.name // superman
noey?.home?.guard?.job // nil

noey?.home?.guard?.job = "경비원"

 

- nil 병합 연산자 : 중위 연산자. Optional ?? Value 옵셔널 값이 nil일 경우, 우측의 값을 반환한다. (띄어쓰기 주의)

var guardJob: String

guardJob = noey?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 경비원

noey?.home?.guard?.job = nil

guardJob = noey?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 슈퍼맨

 

12. 타입 캐스팅(Type Casting)

- 클래스의 인스턴스를 부모 혹은 자식 클래스의 타입으로 사용할 수 있는지 확인하는 용도이다. is, as를 사용한다.

* 형변환은 타입 캐스팅이 아니라 새로운 값을 생성하는 것. *

class Person {
    var name: String = ""
    func breath() {
        print("숨을 쉽니다")
    }
}

class Student: Person {
    var school: String = ""
    func goToSchool() {
        print("등교를 합니다")
    }
}

class UniversityStudent: Student {
    var major: String = ""
    func goToMT() {
        print("멤버쉽 트레이닝을 갑니다")
    }
}

// 인스턴스 생성
var noey: Person = Person()
var hana: Student = Student()
var jason: UniversityStudent = UniversityStudent()

 

- is를 사용하여 타입을 확인한다.

var result: Bool

result = noey is Person // true
result = noey is Student // false
result = noey is UniversityStudent // false

result = hana is Person // true
result = hana is Student // true
result = hana is UniversityStudent // false

result = jason is Person // true
result = jason is Student // true
result = jason is UniversityStudent // true

if noey is UniversityStudent {
    print("noey는 대학생입니다")
} else if noey is Student {
    print("noey는 학생입니다")
} else if noey is Person {
    print("noey는 사람입니다")
} // noey는 사람입니다

switch jason {
case is Person:
    print("jason은 사람입니다")
case is Student:
    print("jason은 학생입니다")
case is UniversityStudent:
    print("jason은 대학생입니다")
default:
    print("jason은 사람도, 학생도, 대학생도 아닙니다")
} // jason은 사람입니다

switch jason {
case is UniversityStudent:
    print("jason은 대학생입니다")
case is Student:
    print("jason은 학생입니다")
case is Person:
    print("jason은 사람입니다")
default:
    print("jason은 사람도, 학생도, 대학생도 아닙니다")
} // jason은 대학생입니다

 

- 업 캐스팅(Up Casting) : as를 사용하여 부모 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 타입 정보를 전환해준다. Any 혹은 AnyObject로도 타입 정보를 변환할 수 있다. (암시적으로 처리되므로 생략 가능)

// UniversityStudent 인스턴스를 생성하여 Person 행세를 할 수 있도록 업 캐스팅
var mike: Person = UniversityStudent() as Person

var jenny: Student = Student()
// var jina: UniversityStudent = Person() as UniversityStudent // 컴파일 오류발생

// UniversityStudent 인스턴스를 생성하여 Any 행세를 할 수 있도록 업 캐스팅
var jina: Any = Person() // as Any 생략가능

 

- 다운 캐스팅(Down Casting) : as? 또는 as!를 사용하여 자식 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 인스턴스의 타입 정보를 전환해준다.

// A. 조건부 다운 캐스팅
// as?를 사용한다.
// 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 nil을 반환하기 때문에 결과의 타입은 옵셔널 타입이다.
var optionalCasted: Student?

optionalCasted = mike as? UniversityStudent
optionalCasted = jenny as? UniversityStudent // nil
optionalCasted = jina as? UniversityStudent // nil
optionalCasted = jina as? Student // nil

// B. 강제 다운 캐스팅
// as!를 사용한다.
// 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 런타임 오류가 발생한다.
// 캐스팅에 성공하면, 옵셔널이 아닌 일반 타입을 반환한다.
var forcedCasted: Student

forcedCasted = mike as! UniversityStudent
// forcedCasted = jenny as! UniversityStudent // 런타임 오류발생
// forcedCasted = jina as! UniversityStudent // 런타임 오류발생
// forcedCasted = jina as! Student // 런타임 오류발생
e.g.)
func doSomethingWithSwitch(someone: Person) {
    switch someone {
    case is UniversityStudent:
        (someone as! UniversityStudent).goToMT()
    case is Student:
        (someone as! Student).goToSchool()
    case is Person:
        (someone as! Person).breath()
    }
}

doSomethingWithSwitch(someone: mike as Person) // 멤버쉽 트레이닝을 갑니다
doSomethingWithSwitch(someone: mike) // 멤버쉽 트레이닝을 갑니다
doSomethingWithSwitch(someone: jenny) // 등교를 합니다
doSomethingWithSwitch(someone: noey) // 숨을 쉽니다

func doSomething(someone: Person) {
    if let universityStudent = someone as? UniversityStudent {
        universityStudent.goToMT()
    } else if let student = someone as? Student {
        student.goToSchool()
    } else if let person = someone as? Person {
        person.breath()
    }
}

doSomething(someone: mike as Person) // 멤버쉽 트레이닝을 갑니다
doSomething(someone: mike) // 멤버쉽 트레이닝을 갑니다
doSomething(someone: jenny) // 등교를 합니다
doSomething(someone: noey) // 숨을 쉽니다

 

13. assert & guard

- 애플리케이션이 동작 도중에 생성하는 다양한 연산 결과값을 동적으로 확인하고 안전하게 처리할 수 있도록 확인하고 빠르게 처리할 수 있다.

- Assertion : assert(_:_file:line:) 함수를 사용한다. 디버깅 모드에서만 동작하여 예상했던 조건의 검증을 위해 사용한다.

var someInt: Int = 0

// 검증 조건과 실패시 나타날 문구를 작성해준다.
// 검증 조건에 부합하므로 지나간다.
assert(someInt == 0, "someInt != 0")

someInt = 1
// assert(someInt == 0) // 동작 중지, 검증 실패
// assert(someInt == 0, "someInt != 0") // 동작 중지, 검증 실패

func functionWithAssert(age: Int?) {

    assert(age != nil, "age == nil")

    assert((age! >= 0) && (age! <= 130), "나이값 입력이 잘못되었습니다")
    print("당신의 나이는 \(age!)세입니다")

}

functionWithAssert(age: 50)
// functionWithAssert(age: -1) // 동작 중지, 검증 실패
// functionWithAssert(age: nil) // 동작 중지, 검증 실패

* assert(_:_:file:line:)와 같은 역할을 하지만 실제 배포 환경에서도 동작하는 precondition(_:_:file:line:) 함수도 있다.

 

- guard(Early Exit) : 잘못된 값의 전달 시 특정 실행구문을 빠르게 종료한다. 디버깅 모드뿐만 아니라 어떤 조건에서도 동작한다. else 블럭 내부에는 특정 코드블럭을 종료하는 지시어(return, break 등)가 꼭 있어야 한다. 타입 캐스팅, 옵셔널과도 자주 사용된다. 그 외에도 단순 조건 판단 후 빠르게 종료할 때도 용이하다.

func functionWithGuard(age: Int?) {

    guard let unwrappedAge = age,
        unwrappedAge < 130,
        unwrappedAge >= 0 else {
        print("나이값 입력이 잘못되었습니다")
        return
    }

    print("당신의 나이는 \(unwrappedAge)세입니다")

}

var count = 1

while true {
    guard count < 3 else {
        break
    }
    print(count)
    count += 1
}
// 1
// 2

func someFunction(info: [String: Any]) {
    guard let name = info["name"] as? String else {
        return
    }

    guard let age = info["age"] as? Int, age >= 0 else {
        return
    }

    print("\(name): \(age)")
}

someFunction(info: ["name": "jenny", "age": "10"])
someFunction(info: ["name": "mike"])
someFunction(info: ["name": "noey", "age": 10]) // noey: 10
// ** if let / guard를 이용한 옵셔널 바인딩 비교 **

// 1. if let 옵셔널 바인딩
if let unwrapped: Int = someValue {
    // do something
    unwrapped = 3
}
// if 구문 외부에서는 unwrapped 사용이 불가능하다.
// unwrapped = 5

// 2. guard 옵셔널 바인딩
// guard 구문 이후에도 unwrapped 사용 가능하다.
guard let unwrapped: Int = someValue else {
    return
}
unwrapped = 3

 

14. 프로토콜(Protocol)

- 특정 역할을 수행하기 위한 메서드, 프로퍼티, 이니셜라이저 등의 청사진을 정의한다.

- 구조체, 클래스, 열거형은 프로토콜을 채택(Adopted)해서 특정 기능을 수행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있다.

- 어떤 프로토콜의 요구사항을 모두 따르는 타입은 그 프로토콜을 준수한다(Conform)고 표현한다.

- 타입에서 프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 청사진의 기능을 모두 구현해야 한다. (즉, 프로토콜은 기능을 정의하고 제시할 뿐이지 스스로 기능을 구현하지는 않는다.)

protocol 프로토콜 이름 {
	/* 정의부 */
}
// << 프로퍼티 요구 >>
// 프로퍼티 요구는 항상 var 키워드를 사용한다.
// get은 읽기만 가능해도 상관 없다는 뜻이며, get과 set을 모두 명시하면 읽기 쓰기 모두 가능한 프로퍼티여야 한다.
protocol Talkable {

    // 프로퍼티 요구
    var topic: String { get set }
    var language: String { get }

    // 메서드 요구
    func talk()

    // 이니셜라이저 요구
    init(topic: String, language: String)

}
// << 프로토콜 채택 >>
// 타입명: 프로토콜 이름

// Person 구조체는 Talkable 프로토콜을 채택한다.
struct Person: Talkable {
    // 프로퍼티 요구 준수
    var topic: String
    let language: String

    // 읽기 전용 프로퍼티 요구는 연산 프로퍼티로 대체가 가능하다.
    var language: String { return "한국어" }

    // 물론 읽기, 쓰기 프로퍼티도 연산 프로퍼티로 대체할 수 있다.
    // var subject: String = ""
    // var topic: String {
    //     set {
    //         self.subject = newValue
    //     }
    //     get {
    //         return self.subject
    //     }
    // }

    // 메서드 요구 준수
    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다")
    }

    // 이니셜라이저 요구 준수
    init(topic: String, language: String) {
        self.topic = topic
        self.language = language
    }
}

// 프로퍼티 요구는 다양한 방법으로 해석, 구현할 수 있다.
struct Person: Talkable {
    var subject: String = ""

    // 프로퍼티 요구는 연산 프로퍼티로 대체가 가능하다.
    var topic: String {
        set {
            self.subject = newValue
        }
        get {
            return self.subject
        }
    }

    var language: String { return "한국어" }

    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다")
    }

    init(topic: String, language: String) {
        self.topic = topic
    }
}

 

- 프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항을 추가할 수 있다.

- 프로토콜 상속 문법은 클래스의 상속 문법과 유사하지만, 프로토콜은 클래스와 다르게 다중상속이 가능하다.

protocol 프로토콜 이름: 부모 프로토콜 이름 목록 {
    /* 정의부 */
}
e.g.)
protocol Readable {
    func read()
}
protocol Writeable {
    runc write()
}
protocol ReadSpeakable: Readable {
    func speak()
}
protocol ReadWriteSpeakable: Readable, Writeable {
    func speak()
}

struct SomeType: ReadWriteSpeakable {
    func read() {
        print("Read")
    }
    func write() {
        print("Write")
    }
    func speak() {
        print("Speak")
    }
}
// << 클래스 상속과 프로토콜 >>
// 클래스에서 상속과 프로토콜 채택을 동시에 하려면 상속받으려는 클래스를 먼저 명시하고 그 뒤에 채택할 프로토콜 목록을 작성한다.
class SuperClass: Readable {
    func read() { }
}

class SubClass: SuperClass, Writeable, ReadSpeakable {
    func write() { }
    func speak() { }
}

 

- is, as 연산자를 사용해서 인스턴스가 특정 프로토콜을 준수하는지 확인 가능하다.

let sup: SuperClass = SuperClass()
let sub: SubClass = SubClass()

var someAny: Any = sup
someAny is Readable // true
someAny is ReadSpeakable // false

someAny = sub
someAny is Readable // true
someAny is ReadSpeakable // true

someAny = sup

if let someReadable: Readable = someAny as? Readable {
    someReadable.read()
} // read

if let someReadSpeakable: ReadSpeakable = someAny as? ReadSpeakable {
    someReadSpeakable.speak()
} // 동작하지 않음

someAny = sub

if let someReadable: Readable = someAny as? Readable {
    someReadable.read()
} // read

 

15. 익스텐션(Extension)

- 구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가할 수 있는 기능이다.

- 기능을 추가하려는 타입의 구현된 소스 코드를 알지 못하거나 볼 수 없다 해도, 타입만 알고 있다면 그 타입의 기능을 확장할 수 있다.

- 외부 라이브러리나 프레임워크 등 외부에서 가져온 타입에 원하는 기능을 추가하고자 할 때 유용하게 사용 가능하다.

<< Swift의 익스텐션이 타입에 추가할 수 있는 기능 >>

1) 연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티

2) 타입 메서드 / 인스턴스 메서드

3) 이니셜라이저

4) 서브스크립트

5) 중첩 타입

6) 특정 프로토콜을 준수할 수 있도록 기능 추가

** 익스텐션은 타입에 새로운 기능을 추가할 수는 있지만, 기존에 존재하는 기능을 재정의할 수는 없다. **

<< 클래스의 상속과 익스텐션 비교 >>

// extension 키워드를 사용하여 정의
extension 확장할 타입 이름 {
    /* 타입에 추가될 새로운 기능 구현 */
}

// 익스텐션은 기존에 존재하는 타입이 추가적으로 다른 프로토콜을 채택할 수 있도록 확장할 수 있다. 
// 이런 경우에는 클래스나 구조체에서 사용하던 것과 똑같은 방법으로 프로토콜 이름을 나열해준다.
extension 확장할 타입 이름: 프로토콜1, 프로토콜2, 프로토콜3... {
    /* 프로토콜 요구사항 구현 */
}
e.g.)
// << 연산 프로퍼티 추가 >>
// 아래 익스텐션은 Int 타입에 두 개의 연산 프로퍼티를 추가한 것이다.
// Int 타입의 인스턴스가 홀수인지 짝수인지 판별하여 Bool 타입으로 알려주는 연산 프로퍼티
// 익스텐션으로 Int 타입에 추가해준 연산 프로퍼티는 Int 타입의 어떤 인스턴스에도 사용이 가능하다.
// 인스턴스 연산 프로퍼티를 추가할 수도 있으며, static 키워드를 사용하여 타입 연산 프로퍼티도 추가할 수 있다.
extension Int {
    var isEven: Bool {
        return self % 2 == 0
    }
    var isOdd: Bool {
        return self % 2 == 1
    }
}
print(1.isEven) // false
print(2.isEven) // true
print(1.isOdd) // true
print(2.isOdd) // false

var number: Int = 3
print(number.isEven) // false
print(number.isOdd) // true

number = 2
print(number.isEven) // true
print(number.isOdd) // false

// << 메서드 추가 >>
// 메서드 익스텐션을 통해 Int 타입에 인스턴스 메서드인 multiply(by:) 메서드를 추가
// 여러 기능을 여러 익스텐션 블록으로 나눠서 구현해도 전혀 문제가 없다.
// 관련된 기능별로 하나의 익스텐션 블록에 묶어주는 것도 좋다.
extension Int {
    func multiply(by n: Int) -> Int {
        return self * n
    }
}
print(3.multiply(by: 2)) // 6
print(4.multiply(by: 5)) // 20

number = 3
print(number.multiply(by: 2)) // 6
print(number.multiply(by: 3)) // 9

// << 이니셜라이저 추가 >>
// 인스턴스를 초기화(이니셜라이즈)할 때 인스턴스 초기화에 필요한 다양한 데이터를 전달받을 수 있도록 여러 종류의 이니셜라이저를 만들 수 있다. 
// 타입의 정의부에 이니셜라이저를 추가하지 않더라도 익스텐션을 통해 추가할 수 있다.
// 익스텐션으로 클래스 타입에 편의 이니셜라이저는 추가할 수 있지만, 지정 이니셜라이저는 추가할 수 없다. 
// 지정 이니셜라이저와 디이니셜라이저는 반드시 클래스 타입의 구현부에 위치해야 한다. (값 타입은 상관없음)
extension String {
    init(int: Int) {
        self = "\(int)"
    }

    init(double: Double) {
        self = "\(double)"
    }
}

let stringFromInt: String = String(int: 100) // "100"

let stringFromDouble: String = String(double: 100.0) // "100.0"