클로저

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

형태

  • 전역함수 : 이름 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는 인스턴스가 아닌 타입 자신을 의미한다

 

구조체와 클래스 (struct, class)

 

  • 구조체(struct)
    • 값 타입(value type) - 값이 복사됨
    • 상속 불가능
    • deinit 없음
    • 참조카운팅 없음
    • Swift의 대부분의 데이터 타입은 구조체로 작성되어 있다
  • 클래스(class)
    • 참조 타입(reference type) - 인스턴스를 참조하여 공유함
    • 상속 가능
    • deinit 있음
    • 참조 카운팅 있음

가장 큰 차이점은 값/참조 타입이다. -> 이에 대한 자세한 내용은 후에 나온다.

B.U.T. 구글링해서 좀 알아보고 읽으면 이해가 더! 잘될 것이라 생각한다.

 

클래스 vs 구조체

struct PersonStruct{
	var name: String
}

class PersonClass{
	var name: String
    
    deinit{
    	print("소멸됐어염")
    }
    
}

let structPerson = PersonStruct(name: "tree")
let classPerson = PersonClass(name: "tree")
var classPersonVar = PersonClass(name: "tree")

structPerson.name = "randy" // 변경 불가, 값 타입을 상수로 선언 시, 내부 프로퍼티 또한 변경할 수 없다
classPerson.name = "randy" // 변경 가능, 참조 타입을 상수로 선언 시, 참조하는 포인터(?)를 가진 classPerson만 변경이 불가

classPersonVar.name = "randy" // 가능
classPersonVar = nil // 소멸됐어염

 

간단하게만 차이를 알아보자

  구조체 클래스
메모리 영역 Stack Heap
속도 빠름 느림
상속 불가능 가능

 

선택하기

  • 애플은 가이드 라인에서 다음 조건 중 하나 이상에 해당한다면 구조체를 사용하라고 권장한다
    • 연관된 간단한 값의 집합을 캡슐화하는 것만이 목적일 때
    • 캡슐화한 값을 참조하는 것보다 복사하는 것이 합당할 때
    • 구조체에 저장된 프로퍼티가 값 타입이며, 참조하는 것보다 복사하는 것이 합당할 때
    • 상속받거나 상속할 필요가 없을 때

스위프트의 데이터 타입들이 대부분 구조체라서 속도가 빠른 건 알겠어!

B.U.T. 쓸데없이 메모리를 많이 잡아 먹는 것 아냐? 매개변수로든 치환이든 다 복사해서 메모리에 올라가잖아?

-> 스위프트는 꼭 필요한 경우에만 "진짜 복사"를 한다고 한다.

-> 진짜 복사 : 메모리에 실제로 데이터를 복사하여 올리는 것

-> 스위프트가 적절히 효율적으로 처리한다고 하는데 그에 대한 기준은 모르겠다... 나중에 시간나면 찾아보는 걸로...

-> 혹여나 이걸 보는 사람은 없겠지만, 혹시라도 보다가 궁금해서 찾게되면 저도 알려주세요..bb

 

 

 

+ Recent posts