옵셔널 체이닝과 빠른 종료

- 옵셔널 바인딩을 하는 것인데 여러 값이 중첩된 형태를 띄우는 형태를 말한다

- 옵셔널을 이해하지 못하고 Swift를 사용한다면 절반도 이해하지 못한다고 할 정도로 중요한 개념이 옵셔널이다

 

옵셔널 체이닝

- 옵셔널에 속해 있는 nil일지도 모르는 프로퍼티, 메서드, 서브스크립트 등을 가져오거나 호출할 때 사용할 수 있는 일련의 과정

  - 옵셔널이 nil이면 모두 nil을 반환한다.

  - 당연히 nil이 아니면 제 값을 반환/호출한다

- 옵셔널을 반복 사용하여 옵셔널이 체인처럼 꼬리를 물고 있는 모양을 말한다

 

느낌표(!)의 사용

  • 값을 강제로 추출하기 때문에 옵셔널에 값이 없다면 런타임 오류가 발생한다
  • 또한 반환값은 옵셔널이 아니라는 점이다
  • 따라서 옵셔널이 아닌 타입에 nil 이 들어오는? 오류가 발생한다
  • 무조건 100% nil이 아니라고 확신하더라도 사용을 지양하는 것이 좋다

 

let optionalChaining: Int? = person.address?.building?.room?.number // nil

let oprionalUnwrapping: Int = person.address?.building?.room?.number // 런타임 오류

 

빠른 종료

- 옵셔널을 추출할 때 사용되는 guard가 핵심이다

- if 문과 유사하게 Bool 타입으로 동작한다

- B.U.T. guard 뒤의 구문이 false 라면 else 블록 내부 코드를 실행하게 되는데, 여기서 블록 내부에는 자신보다 상위의 코드 블록을 종료하는 코드가 들어가야합니다

- guard 구문을 사용해서 함수나 블록에서 쓸데없이 모두 진행할 필요없이, 앞에서 옵셔널을 추출한 후에 nil이라면 바로 블록을 나올 수 있다 -> 이게 바로 빠른 종료를 이용하는 것이다

 

guard Bool타입 else{
	예외상황 실행문
    상위 코드 블록을 종료하는 코드
}

 

func enterClub(name: String?, age: Int?){
	guard let name: String = name, let age: Int = age, age > 19, name.isEmpty == false else{
    	print("넌 너무 어려서 못 들어간다!")
        return // 반환 타입이 Void기 때문에 어떠한 값도 반환하지 않는다
    }
    
    print("어서오세요 \(name)")
}

//대충 예가 기억이 안나서 이 사람이 클럽에 들어갈 수 있는가?라는 함수를 작성해보자

func canEnterClub(name: String?, age: Int?) -> Bool{
	guard let name: String = name, let age: Int = age, age > 19, name.isEmpty == false else{
    	print("넌 너무 어려서 못 들어간다!")
        return false // 반환 타입이 Bool이기 때문에 Bool 타입의 값을 반환하는데, 이 사람은 들어갈 수 없으므로 false를 반환
    }
    
    print("어서오세요 \(name)")
}

 

클로저

  • 제네릭, 프로토콕, 모나드 등과 결합이 가능하다
  • 람다와 비슷하다
  • 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것
  • 함수도 클로저의 일종

형태

  • 전역함수 : 이름 o, 값 획득 x
  • 중첩된 함수 : 이름 o, 다른 함수의 내부의 값 획득 o
  • 축약 문법으로 작성 : 이름 x, 주변 문맥에 따라 값을 획득 o

 

특징

  • 문맥을 통해 매개변수와 반환 값을 추론 가능하기 때문에, 생략이 가능하다
  • 단 한줄만 있다면 암시적으로 그것을 반환 값으로 취급
  • 축약된 전달인자 이름을 사용할 수 있다
  • 후행 클로저 문법을 사용할 수 있다

 

기본 클로저

정렬을 위한 함수 전달

func backWords(first: String, second: String) -> Bool{
	return first > second
}

var names: [String] = [.....]
let reversed: [String] = names.sorted(by: backWords) // 알파벳의 역순으로 정렬된 배열이 저장

정렬을 위한 클로저 전달

let reversed: [String] = names.sorted(by: { (first:String, second: String) -> Bool in
	return first > second
}) // 위의 코드보다 훨씬 간결해졌다.

후행 클로저

- 함수나 메서드의 마지막 전달인자가 클로저일 때, 소괄호를 닫고 작성할 수 있다.

let reversed: [String] = names.sorted(){ (first: String, second: String) -> Bool in 
	return first > second
}

//OR

let reversed: [String] = names.sorted{ (first: String, second: String) -> Bool in 
	return first > second
} // 소괄호 내에 어떠한 매개변수도 없기 때문에, 생략 가능

 

클로저의 간소화

- 클로저를 간소화 함으로써 가독성이 좋아지고, 코드가 간결해진다.

- 하지만 너무 많은 간소화를 통하면 소통이 되지 않을 수도 있으니 조심

 

문맥을 이용한 타입 유추

let reversed = names.sorted{ (first, second) in
	return first > second
} // 매개변수와 반환 타입을 생략 가능

단축인자 이름 사용

let reversed: [String] = names.sorted{
	return $0 > $1
} // $0 = first , $1 = second 라고 생각하면 된다

암시적 반환 표현

let reversed: [String] = names.sorted{ $0 > $1 }

아래로 내려오면서 코드가 훨씬 간결해지는 것을 볼 수 있다.

지금 예로 든 클로저는 간단해서 "어? 무조건 간소화하는 게 좋은 거 아냐?" 라고 생각할 수 있지만, 클로저 내부에서 많은 동작을 요구하고 매개변수 타입또한 복잡해지면 무턱대고 간소화하는 것은 오히려 역효과를 부를 수 있다

 

연산자 함수

- 연산자도 일종의 함수이다

 

// > 연산자 정의
public func ><T : Comparable>(lhs: T, rhs: T) -> Bool

T는 제네릭으로 이후에 자세히 포스팅할 것이다

여기서만큼은 T는 String이라고 생각하면 된다

 

우리는 위에서 reversed를 구하기 위해 (String, String) -> Bool 타입인 클로저를 사용했다

......

....?

> 연산자도 (String, String) -> Bool 타입이네?

 

그렇다, 클로저에도 타입이 존재하기 때문에, 매개변수의 클로저와 같은 타입의 클로저/함수는 매개변수로 전달할 수 있다

-> 쉽게 생각하면 된다.

var a: Int = 0 , var b: Int = 10  // 여기서 a, b 모두 Int지 않은가?

func printInt(num: Int){

    print(num)

} // 이 함수에서 필요한 매개변수인 num과 타입이 같은 a, b는 매개변수의 인자가 될 수 있는 것과 같다

 

다시 연산자 함수로 돌아가서

let reversed: [String] = names.sorted(by: >)

이렇게 간단하게 연산자 함수를 통해 작성도 가능하다

 

값 획득

- 클로저에서 개인적으로 가장 어려웠던 개념이다

- 이 자체가 어렵다기 보다는 다른 개념들까지 생각하며 염두하게 되면 헷갈리는 개념이기도 하다

- 값 획득을 통해 정의한 상수/변수가 더이상 존재하지 않아도 내부에서 그 값을 참조/수정이 가능

  1. 값 획득을 통해 정의한 상수/변수

  2. 존재하지 않아도

  3. 내부에서 그 값을 참조/수정이 가능

3가지로 나누어서 보자

 

//makeIncrementer 함수는 하나의 Int 매개변수를 받아서
// ()->Int 타입의 클로저를 반환한다

//incrementer 함수는 매개변수로 아무것도 받지 않고
// Int 타입을 반환한다

func makeIncrementer(amount: Int) -> (()->Int){
    var runningTotal = 0
    func incrementer() -> Int{//중첩된 함수
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTwo = makeIncrementer(amount: 2)

let first = incrementByTwo() // 2
let second = incrementByTwo() // 4
let third = incrementByTwo() // 6

 

무언가 이상하다

incrementByTwo 라는 상수에 ()->Int 타입의 클로저를  반환 한 것은 알겠다.

근데 함수안에 있는 변수들은 함수가 종료될 때, 메모리에서 해제된다고 알고 있는데, 왜 값이 계속 증가해?

 

천천히 살펴보자

우선 incrementByTwo = makeIncrementer(amount: 2) 는 ()->Int 타입의 클로저라는 것을 잊지말라

1. first에서 incrementByTwo()를 호출했다

2. incrementByTwo() 내에 있는 runningTotal 변수가 생성된다.

3. incrementer 라는 함수가 생성된다.

4. incrementer 라는 함수가 반환된다.

5. incrementer() 함수가 실행된다.

6. runningTotal 값이 0에서 2로 증가한다

7. 2가 된 runningTotal이 반환된다.

 

이 순서인데.... 아니 4번에 incrementer라는 함수가 반환된 시점에서 runningTotal 변수는 죽은거아냐?

makeIncrementer하는 함수가 끝났잖아?

 

-> 맞는 말이다, 하지만 값 획득을 통해 외부의 값을 사용하는 것이다.

-> 위에서 3가지 단계로 나눠놓은 것을 빗대어 설명하자면

-> 1. 값 획득을 통해 정의한 상수/변수 : amount, runningTotal

-> 2. 존재하지 않아도 : makeIncrementer 함수가 종료되어서 메모리에서 사라져도

-> 3. 내부에서 그 값을 참조/수정이 가능 : incrementer하는 함수 내부에서 해당 값을 참조/수정이 가능

 

이러한 과정을 통해 runningTotal, amount 값을 사용할 수 있다.

 

 

클로저는 참조타입

- 이전 포스팅들에서 말했듯, 값 타입과 참조 타입이 존재한다

- 클로저는 참조타입이다.

- 만약 클로저가 값타입이라고 생각해보면 위의 incrementByTwo는 동작하면 안된다

  -> incrementByTwo 는 let 즉, 상수로 선언되었다.

  -> 상수로 선언된 값 타입 ?? struct!! 상수로 선언된 구조체 인스턴스의 내부 값을 변경할 수 없지 않았던가

  -> 근데 incrementByTwo는 상수인데 내부 값이 증가되는 것을 보면 클로저는 참조 타입이라는 것을 역으로 보일 수 있는 것이다

 

 

탈출 클로저

- 함수의 전달인자로 클로저가 전달되고, 해당 함수가 종료되고 클로저가 호출될 때, 클로저가 함수를 탈출한다고 표현한다

- 예를 들어 비동기 작업을 실행하는 함수들은 클로저를 ComplementHandler 전달인자로 받아온다.

  -> 비동기 작업으로 함수가 종료되고 난 후, 호출할 필요가 있는 클로저를 상요해야할 때 탈출 클로저가 필요하다

 

하지만 지금까지 알아본 함수들은 모두 @escaping 키워드를 찾을 수 없었다

 -> 모두 비탈출 클로저 였기 때문이다

 

typealias VoidVoidClosure: () -> Void

func withNoescapeClosure(closure: VoidVoidClosure){
	closure()
}

func withEscapingClosure(completionHandler: @escaping 
						VoidVoidClosure) -> VoidVoidClosure{
	return completionHandler
}

class SomeClass{
	var x = 10
    
    func runNoescapeClosure(){
    // 비탈출 클로저에서 self 키워드 사용은 선택
    	withNoescapeClosure{ x = 200 }
    }
    
    func runEscapingClosure -> VoidVoidClosure{
    // 탈출 클로저에서 self 키워드 사용은 필수!!
    	return withEscapingClosure{ self.x = 100 }
    }
}

let instance: SomeCLass = SomeClass()
instance.runNoescapeClosure()
print(instance.x) // 200

let returnedClosure: VoidVoidClosure = instance.runEscapingClosure()
returnedClosure()
print(instance.x) // 100


 

 

 

인스턴스 생성 및 소멸

- 클래스 인스턴스의 소멸 시점 : ARC에 의해 결정된다

 

  • 이니셜라이저
    • 저장 프로퍼티의 초깃값 설정
    • 옵셔널 프로퍼티는 제외
  • 프로퍼티의 기본값
    • var num = 100
  • 상수 프로퍼티의 초기화
    • lazy var 얘기할 때 한번 언급한 것 같은데
    • let(상수) 프로퍼티는 초기화 과정에서만 값을 할당할 수 있다
  • 실패 가능한 이니셜라이저
    • init?(...)
    • 대표적으로 이니셜라이저의 전달인자로 잘못된 값이 전달되었을 때, 초기화 실패할 수 있다
    • 실패를 염두한 이니셜라이저
  • 함수를 사용한 프로퍼티 기본값 설정
    • iOS에서 UI 컴포넌트를 클래스의 프로퍼티로 구현하고, UI 컴포넌트의 생성과 동시에 Constraint등의 설정할 때 용이함
  • 인스턴스 소멸
    • deinit은 클래스에서만 구현이 가능함

이번달 내로 2020년 회고 쓸건데 이거 언제 다하고 쓰지...

 

프로퍼티와 메서드

  • 프로퍼티
    • 저장 프로퍼티
      • 변수 or 상수
      • 클래스와 구조체에 포함
    • 연산 프로퍼티
      • 연산을 실행한 결괏값
      • 실제로 데이터 값을 갖지 않음
      • 클래스, 구조체, 열거형에 포함
      • getter/setter가 존재
      • 왜 사용하는가?
        • 메서드로 구현시, get/set Method 2개를 구현해야 하는데, 보다 훨씬 간편함
    • 타입 프로퍼티
      • 특정 타입에 사용
      • static 키워드 사용
    • 지연 저장 프로퍼티
      • 처음으로 호출될 때, 초기화를 진행한다 
      • lazy var 키워드 사용
      • lazy let은 사용할 수 없다 -> (let은 생성될 때, 초기화된 후로 변경될 수 없기 때문이 아닐까?)
      • 여러 쓰레드가 동시에 접근 시, 여러번 초기화 가능성이 있다. -> Non-Thread-Safe
//연산 프로퍼티
struct Point{
	var x: Float
    var y: Float
	var oppositePoint: Self{
    	get{
        	return Point(x: -x, y: -y)
        }
        //set은 생략가능 -> 읽기전용 프로퍼티로 선언/사용 가능
        set(opposite){
        	x = -opposite.x
            y = -opposite.y
        }
    }
}

 

프로퍼티 감시자

- 값이 새로 할당될 때마다 호출

- 저장/연산 프로퍼티에 사용 가능

- willset/didset 구현

- 현재 값과 같더라도 실행된다

- 오버라이드가 가능하다

class Account{
	var credit: Int = 0{
    	willSet{
        	print("잔액이 \(credit)에서 \(newValue)로 변경될 것입니다")
        }
        didSet{
        	print("잔액이 \(oldValue)에서 \(credit)으로 변경되었습니다")
        }
    }
}

 

타입 프로퍼티

- 인스턴스가 아닌 각각의 타입 다체에 속하는 프로퍼티

- static let, static var 사용 가능하지만, 연산 프로퍼티는 var로만 선언 가능

- 반드시 초깃값을 설정해야 하며, 지연 연산된다

- 지연 저장 프로퍼티(lazy var)와는 다르게 다중 쓰레드 환경에서도 단 한번만 초기화 된다


메서드

  • 인스턴스 메서드
    • 인스턴스가 존재할 때만 사용 가능
    • 구조체/열거형에서 인스턴스 내부 값(프로퍼티)을 변경하기 위해서는 mutating func로 선언
  • 타입 메서드
    • static func / class func 가 존재한다
      • 둘다 같은 타입 메서드지만, static은 상속 불가, class는 상속 가능 타입 메서드이다
    • 여기서의 Self는 인스턴스가 아닌 타입 자신을 의미한다

 

+ Recent posts