Swift 3. 심화 문법 part 2

박건희·2022년 3월 9일
0

Swift

목록 보기
3/10
post-thumbnail

swift 문법을 기록하고 복습하기 위해 쓰는 글이며, 모든 내용은 boost course의 야곰님의 강의를 기반으로 합니다. 후에 문제가 될 시 모든 내용을 삭제하겠습니다

Reference:

https://www.boostcourse.org/mo122

📌 목차

  • 상속 (Inheritance)
  • 인스턴스 생성자 (init)
  • 인스턴스 파괴자 (deinit)
  • Optional Chaining
  • nil-coalescing operator (??)
  • Type casting (is/as!/as?)
  • assert & guard
  • Protocol
  • Extension
  • exception handling

+) 이전까지 정리한 내용을 보니 난잡해서 나도 읽기 싫어졌다.. 따라서 더욱 글을 줄이고 깔끔하게 쓸 예정이다


📌 문법 설명

상속 (Inheritance)

Syntax

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

Note

  • 타 OOL과 상속의 개념은 동일하다.

  • 부모 클래스의 메소드를 override하고 싶은 경우:
    - override func parentFunction <- 이렇게 override를 앞에 붙여준다
    - super.parentFunction을 통해서 부모 메소드를 호출할수 있다

  • 상속받은 클래스의 메소드를 override 못하는 경우는:
    1. static func로 선언한 경우 (심화 문법 part 1의 class 참조)
    2. final class func으로 선언한 경우 (자바와 같음)


인스턴스 생성자 (init)

Context

class를 정의할 때 Stored Property(저장 프로퍼티)에 default 값을 넣어주지 않으면 컴파일 에러가 난다. 클래스를 생성할 때 제대로 값을 설정하지 않으면 런타임 에러가 발생하기 때문이다.

따라서 init이라는 constructor를 만들어주지 않으면 무조건 default 프로퍼티 값을 가진 기본 클래스를 생성해서 일일히 프로퍼티를 내가 원하는 값으로 수정해줘야 한다.

결국 swift의 init => java의 constructor

Syntax

class Person {
    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
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

Note

우리가 어떤 클래스를 이용해서 instance를 만들때 무조건 모든 프로퍼티를 입력하진 않는다. 그런 경우엔 자바처럼 여러 종류의 constructor를 만들어 놓아야 한다. (살짝 아쉬운 점... Dart는 굉장히 편했음)

유의 사항
정의가 안될 수도 있는 stored property를 Obtional로 선언하자 (값이 할당이 안된 경우 nil이 돼야 해서)

⭐️ convenience init

class Person {
    var name: String
    var age: Int
    var nickName: String?
    
	convenience init(name: String, age: Int, nickName: String) {
         init(name: name, age: age)
         self.nickName = nickName
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

하지만 init을 수십개 만들 때 코드가 계속 반복될 것이다.. 이런 boilerplate를 예방하기 위해 이미 만들어 놓은 다른 init을 새로운 init안에 선언해서 재활용할 수 있다.
이 때 다른 init이 안에 들어 있는 init은 반드시 앞에 convenience라는 키워드를 붙여야 한다.

⭐️ lazy init

실제 코딩을 하다보면 lazy init이 필요한 경우가 정말 많다.

class Person {
    var name: String
    var age: Int!

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

그럴 땐 lazy init할 프로퍼티를 implicit unwrapped Optional(!)로 선언하면 된다.

failable initializer

init을 호출 할 때 instance를 무조건 생성하지 않고 조건을 안맞는 경우 nil을 return하게 할 수 있다.
init 뒤에 Optional(?)을 붙이고 여러 condition을 추가하면 된다.

이 경우 initialize가 안될 수도 있기 때문에 만들어지는 instance 역시 Optional Type으로 정의해야 한다.

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
    }

인스턴스 파괴자 (deinit)

Syntax

 deinit {
        if let petName = pet?.name {
            print("\(name)\(child.name)에게 \(petName)를 인도합니다")
            self.pet?.owner = child
        }
    }

Note

메모리에서 instance를 해제되는 시점에 호출되며, 이 때 특정 작업을 해주고 싶으면 deinit을 사용한다. deinit은 파라미터를 가질 수 없다

메모리에서 해제되는 시점이 언젠데?

자바의 Garbage Collector처럼 Swift도 리소스 관리자가 있고, 이를 Automatic Reference Counting(ARC)라고 부른다. ARC에서 리소스를 해제하는 시점과 작동 방식을 알려면 스위프트 공식 문서를 보면 된다.

Optional Chaining

Context

복잡한 형식의 json 파일을 클라이언트 쪽으로 가져올 때 클래스 안의 클래스 안의 클래스.. 의 형태가 자주 나타난다. 이 외에도 클래스 안에 또 다른 클래스가 마트료시카처럼 존재하는 경우는 종종 마주하게 되는데, 만약 Optional property가 섞여 있는 클래스가 있다면 nil check를 하기 위해 엄청 드러운 코드를 짜게 된다.

이 상황을 해결하기 위해서 Optional Chaining을 사용한다.

Syntax

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
    }
}
...

//여기서 아파트 인스턴스의 nil 값을 체크할 때:
if let guardJob = owner?.home?.guard?.job {
        print("우리집 경비원의 직업은 \(guardJob)입니다")
} else {
        print("우리집 경비원은 직업이 없어요")
}

Note

다른 null satety OOL과 비슷한 코드다. 다만 nil을 반환할 때 chain의 어떤 위치에서 정확히 nil이 발생했는지 알려주지 못하기에 (i.e. 중간에 Optional 중 누가 nil이던 무조건 nil을 반환하기에) debugging 할때 불편할 것 같다.

nil-coalescing operator

Syntax

guardJob = yagom?.home?.guard?.job ?? "슈퍼맨"

Note

Dart랑 똑같은 문법이다.
if (valA != nil) valB = valA, else valB = 1 을 valB = valA ?? 1 로 줄여주는 편리한 연산자

Type Casting

Context

가벼운 형변환 (int <-> float)에도 주로 사용되지만,
json File을 받아오기 위해서 <String, Any> 타입으로 받아온 변수를 사용할때나, AnyObject의 타입을 명시할 때 주로 사용된다.

이번에 알았는데 타입을 변형하는 것 뿐 아니라 타입을 확인하는 것도 Type casting 개념의 일부란다

Type 확인자 'is'

Syntax

let person = Person()
let student = Student() //Student inherits person

let isPerson:Bool = person is Person // true
let isPerson2:Bool = student is Person // true
let isStudent:Bool = person is Student // false

Note

  • is를 활용해서 switch-case문을 만들 수도 있다

java의 is와 유사하다. 자신이 계승한 class까지도 포함해서 boolean 값을 내놓는다.

Up casting 'as'

Syntax

let student = Student() //Student inherits person

var John: Person = Student() as Person

Note

  • 보통 자신과 다르거나 자신보다 더 구체화된 type으로 casting하기 위해 형변환을 하는데, 자신을 조상 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 타입 정보를 알려주는 것이다.

  • Any나 AnyObject 타입으로 변환할 수도 있다.

Down casting 'as?' & 'as!'

as? 는 조건부as!는 강제 다운 캐스팅

Syntax

var John: Person = Student() as Person
var Sydney: Person = Person()

var optionalCast: Student?

optionalCast = John as? Student //Person type이어도 실제론 Student 인스턴스를 할당 받았기에 casting 가능
optionalCast = Sydney as? Student //Person instance 이기 때문에 nil

optionalCast = John as! Student //Person type이어도 실제론 Student 인스턴스를 할당 받았기에 casting 가능
optionalCast = Sydney as! Student //바로 런타임 에러 BAAM

Note

  • as?의 경우 조건을 만족할 때는 casting 된 타입으로 인스턴스를 반환하고, 불만족시 nil을 반환

  • as!의 경우 그딴거 없이 강제로 type casting 시키고, 안될 경우 런타임 에러 발생

switch-case문에서 is로 타입을 확인하고, 그 다음 as!를 사용해서 검증된 타입으로 casting 하는 방법을 많이 쓴다.

혹은 if-let문으로 as?를 사용해서 nil인지 확인하는 방법도 있다.

assert & guard

Context

앱을 디버깅 하는 과정에서 값을 테스트하려고 쓰는 게 assert다. 배포하는 앱에선 제외된다.

guard는 배포하는 앱에서도 사용가능하며, 문제가 생겼을 때 early exit을 처리해주기 위해 사용한다.

Syntax

var age:Int = 0

assert(age > 0, "age can't be < 0!")   

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

Note

  • 만약 assert문의 첫번째 parameter에 넣은 값이 false라면 동작이 멈추고 두번째 parameter의 error msg를 내보낸다.

  • guard문은 try catch 구문이나 if else 문처럼 사용할 수 있으며, 동작대로 처리 되지 않을 경우를 핸들링 하기 위해 else 후에 return이나 break를 꼭 포함해줘야 한다.

  • guard let 의 경우 if-let과는 다르게 let 뒤에 선언한 변수를 guard let statement 이후에도 쓸 수 있다

Protocol

Syntax

// 프로토콜 정의부 
protocol Talkable {
    var topic: String { get set }
    var language: String { get }
    
    // 메서드 요구
    func talk()
    
    // 이니셜라이저 요구
    init(topic: String, language: String)
}

//프로토콜 채택(adopt) 및 준수(conform) 부
struct Person: Talkable {
    var topic: String
    let language: String
    
    
    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다")
    }
   
    init(topic: String, language: String) {
        self.topic = topic
        self.language = language
    }
}

Note

  • class, struct, enum 모두 protocol adopt 가 가능하다

  • 프로토콜에서 프로퍼티를 정의할 경우 read&write가 다 가능한 경우 get set을 둘 다 써줘야 하고, read only 인 경우 get만 써줘야 한다.

  • 프로토콜을 준수할 때 Computed Property로 대체해도 된다

  • 프로토콜은 다중 상속이 가능하다

  • 프로토콜과 클래스를 동시에 상속 받을 때, 클래스를 무조건 먼저 앞에 써줘야 한다

  • 프로토콜로 is를 통해 true or false를 알 수 있다

Extension

Context

Syntax

extension 확장할 타입 이름: 프로토콜1, 프로토콜2, 프로토콜3... {
    /* 프로토콜 요구사항 구현 */
}

// 프로토콜 adopt 부분은 작성하지 않아도 상관 없다.

Note

  • 이미 완성된 class, struct, enum에 새로운 기능을 추가하게 해주는 기능이다.
  • Property, Computed property, method, init 등을 추가할 수 있다.
  • 특정 프로토콜을 추가할 수도 있다.

Exception Handling

Syntax

enum VendingMachineError: Error {
    case invalidInput
    case insufficientFunds(moneyNeeded: Int)
    case outOfStock
}

//에러만 핸들하는 예시용 클래스
class VendingMachine {
    let itemPrice: Int = 100
    var itemCount: Int = 5
    var deposited: Int = 0
    
    func receiveMoney(_ money: Int) throws {
        
        guard money > 0 else {
            throw VendingMachineError.invalidInput
        }
    }
    
    func vend(numberOfItems numberOfItemsToVend: Int) throws -> String {
    
        guard numberOfItemsToVend > 0 else {
            throw VendingMachineError.invalidInput
        }
        
    
        guard numberOfItemsToVend * itemPrice <= deposited else {
            let moneyNeeded: Int
            moneyNeeded = numberOfItemsToVend * itemPrice - deposited
            
            throw VendingMachineError.insufficientFunds(moneyNeeded: moneyNeeded)
        }
        
        guard itemCount >= numberOfItemsToVend else {
            throw VendingMachineError.outOfStock
        }
    }
}

Note

  • 주로 Error protocol을 채택한 enum을 사용해서 에러를 정의한다.
  • guard문이나 if else문으로 에러가 날 환경을 걸러준 뒤 throw를 통해 에러를 던져준다.
  • throw를 사용하는 함수의 경우 (parameters,...)-> returnType 사이에 throws라고 적어준다.

⭐️ try catch 문

Syntax의 예시에서 error를 throw할 수 있는 함수를 작성했으니, 이를 호출할 때는 try catch를 통해 throw된 에러를 잡아야 한다. 근데 문법이 java나 Dart와는 살짝 다르니 유의하자

do {
    try machine.receiveMoney(0)
} catch VendingMachineError.invalidInput {
    print("입력이 잘못되었습니다")
} catch VendingMachineError.insufficientFunds(let moneyNeeded) {
    print("\(moneyNeeded)원이 부족합니다")
} catch VendingMachineError.outOfStock {
    print("수량이 부족합니다")
} 

// switch문을 사용한 handling
do {
    try machine.receiveMoney(300)
} catch  {
    switch error {
    case VendingMachineError.invalidInput:
        print("입력이 잘못되었습니다")
    case VendingMachineError.insufficientFunds(let moneyNeeded):
        print("\(moneyNeeded)원이 부족합니다")
    case VendingMachineError.outOfStock:
        print("수량이 부족합니다")
    default:
        print("알수없는 오류 \(error)")
    }
}

이처럼 do {try functionName} catch error.A { } catch error.B {} ... 방식으로 사용하거나 하나의 catch 문 안에 switch-case로 처리한다.

⭐️⭐️ try? 와 try!

result = try? machine.vend(numberOfItems: 2)
result = try! machine.vend(numberOfItems: 1)

위와 같은 방식으로 do-catch문 대신 try 하나만 써서 처리할 수도 있다. try?를 사용한 경우 error throw가 되면 그냥 nil을 return 하고, try!는 런타임 에러를 내버린다.

고차함수 (Higher-order function)

parameter나 return 값에 함수가 들어있는 함수다.
collection을 다룰 때 자주 쓰이는 고차함수 map filter reduce를 알아보자

⭐️⭐️⭐️ map ⭐️⭐️⭐️

Dart랑 개념 동일함
map() 안에 들어가는 parameter에 closure를 넣어준다는 부분만 다름.

자주 쓸거라는 것을 본능적으로 알 수 있음

let numbers: [Int] = [0, 1, 2, 3, 4]
var doubledNumbers: [Int]

doubledNumbers = numbers.map({ 
	(number: Int) -> Int in 
    	return number * 2
    }
) // or numbers.map { $0 * 2 }

//doubledNumbers == [0, 2, 4, 6, 8]

map function은 parameter에 함수가 들어가는 고차함수의 일종이다.

filtered

Bool type을 return하는 closure를 parameter로 한다.
true를 return 하는 element만 골라서 collection을 만들어준다.

let numbers: [Int] = [0, 1, 2, 3, 4]

let filtered: [Int] = numbers.filter
	{ (number: Int) -> Bool in 
    	number % 2 == 0 
    }
 // or numbers.filter { $0 % 2 == 0 }

//filtered == [0, 2, 4]

reduce

collection 내부의 값을 하나로 통일 시켜준다.
통일 시키는 규칙은 parameter에 들어가는 closure에 따라 달라진다.

let numbers: [Int] = [0, 1, 2, 3, 4]

let addAll: [Int] = numbers.reduce(0, 
	{ (first: Int, second: Int) -> Bool in 
    	return first + second
    }
) // or numbers.filter { $0 + $1 }

//filtered == 10

reduce(initial value, closure) 의 형태를 가지고 있으며, initial value는 말 그대로 초기 값이다.

부스트 코스 수료증

별거 아니지만 받아버렸다~~ 이런 소소한 성취감 나쁘지 않아

부스트 코스 야곰님 강의 후기

  1. 일단 목소리가 좋다
  2. 쓸데 없는 얘기를 많이 안하시고, 너무 초심자 기준으로 쉽게 설명하려고 하시지 않으신다. 덕분에 기본적인 프로그래밍 지식이나 타 언어를 공부해본 사람들이 딱 좋아할 정도의 입문 강의다.
  3. 2022년 2월에 최신화된 강의도 있다. 즉 계속해서 강의를 업데이트 하고 있단 뜻이고, 프론트엔드쪽 언어 강의에선 더없는 혜택이다

결론: Swift 관심이 있었다면 한번 해봐라. 이런 공짜 강의 쉽지 않다.

profile
CJ ENM iOS 주니어 개발자

0개의 댓글