Swift문법 - (22)Closure

Youth·2022년 10월 11일
0

swift문법공부

목록 보기
22/27
post-thumbnail
post-custom-banner

클로저

  • 함수와 클로저는 동일하되, 형태만 다를 뿐이다.
함수의 형태
func myFunction() -> Int {
    return ...
}
// myFunction : 함수의 이름
// () -> Int : 함수의 타입
// { return ... } : 함수의 내용

클로저의 형태
{ () -> Int in
    return ...
}
// () -> Int : 클로저(함수)의 타입
// in 이후 : 클로저(함수)의 내용(in 은 그냥 함수의 내용을 말해주는구나 라고 생각)

**중괄호 "{}"는 함수다**
// 함수의 타입 표기법

let functionA: (String) -> String      // 1)파라미터 문자열, 2)리턴형 문자열

let functionB: (Int) -> ()             // 1)파라미터 정수, 2)리턴형 없음

let functionC: (String) -> Void        // 1)파라미터 문자열, 2)리턴형 없음

함수와 클로저의 함수 형태 비교

// 함수의 정의

func aFunction(str: String) -> String {
    return "Hello, \(str)"
}

// 클로저의 형태

let _ = {(str: String) -> String in
    return "Hello, \(str)"
}

클로저의 선언과 실행

변수 aClosureType에 클로저를 넣는다
() -> () 타입의 print("안녕")을 출력하라는 실행문을 가지고있는 클로저
⭐️**실행문을 가지고 있는 클로저를 넣은것이지 "실행"한것이 아니다**

let aClosureType = { () -> () in    
    print("안녕")
}

let aClosureType = { print("안녕") }     // () -> ()의 경우에는 타입과 in을 생략가능
                                        // 실행문이 한줄이면 return도 생략가능
aClosureType                            // aClosureType는 클로저를 가리키고있다
aClosureType()                          // 함수를 실행하기 위해선 뒤에다가()를 붙여야한다

함수를 일급객체로 취급한다는 의미

  1. 함수를 변수에 할당할 수 있음
  2. 함수를 호출할때, 함수를 파라미터로 전달할 수 있음
  3. 함수에서 함수를 반환할 수 있음

함수의 형태를 클로저의 형태로 변형시키기

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

1.클로저는 "이름 없는 함수"이기 때문에 함수이름을 지워준다
2. "{"를 앞으로 가져온다, 그자리에 in을 넣어준다
3. return타입이 Int인데 result가 Int임이 명확함 -> 생략 가능

{(a: Int, b: Int) in
    let result = a + b
	  return result
}
4.만약 Input의 타입추론이 가능한 경우 타입도 생략이 가능
 ex) let a: (Int, Int) -> Int 라고 적혀있는 경우

{(a, b) in
    let result = a + b
	  return result
}

**보통의 클로저 형태**
{ 파라미터 -> 리턴형 in        // "-> 리턴형"은 생략 가능
    let result = a + b
	  return result
}

왜 클로저를 사용해야하는가?

예제1

예를들어 클로저를 파라미터로 받는 함수를 정의해보면
func closureParamFunction(closure: () -> ()) {
    print("프린트시작")
    closure()          //클로저라를 함수를 실행("()"가 있기 때문에)
}

closureParamFunction(closure: 여기에 함수가 들어가야함(타입 : () -> ()))
그러면 1) print("프린트시작")을 출력 2) input으로 받은 함수를 실행 

이런 경우엔 () -> () 타입인 함수를 선언 해놓고 그 함수를넣는 방식으로 실행
func printSomething() {
    print("프린트끝")
}

이런식으로 사용하는게 일반적인 방식
closureParamFunction(closure: printSomething)

결국, 클로저라는건 파라미터(+타입)in 그리고 실행문 만으로 함수의 역할을 하게 되는거기때문에
closureParamFunction(closure: { () -> () in
    print("프린트끝")
})
으로 사용할수 있음. 이때, 이미 () -> ()인 함수만 넣을 수 있기 때문에 in 과 함께 생략 가능
closureParamFunction(closure: {
    print("프린트끝")
})

**함수를전달할때는 함수의 이름은 필요없고 함수의 실행문이 필요하기때문에** ⭐️**활용도가 늘어남⭐️
함수를 실행할때 파라미터로 전달하는 함수를 "콜백함수"라고 부른다**

예제2

함수(클로저)를 파라미터로 받는 함수를 정의
= a와 b를 더한 값(c)를 파라미터로 받은 함수 closure에 넣어줘
func closureCaseFunction(a: Int, b: Int, closure: (Int) -> Void) {
    let c = a + b
    closure(c)
}

식을 전개해보면
closureCaseFunction(a: Int, b: Int, closure: { (param) in
    print(param)
})
"()" 안에 Int타입의 param이라는 파라미터를 넣어준다(Int는 타입추론이 가능하기 때문에 ": Int"는 생략)
"()" 생략 가능 param in 으로 해도 됌
**실질적으로 클로저는 기존의 함수 내용이 다 실행되고 나서 실행된다고 봐도 무방함

미리 우리는 a와 b를 더하고 "다음에" 함수(클로저)의 인풋파라미터로 던져준다는걸 알고 있다!

⭐️**추가**⭐️**
함수의 마지막 파라미터가 함수라면(트레일링클로저 형식으로 변형 가능)
****closureCaseFunction(a: Int, b: Int) {(param) in
    print(param)
}

클로저의 문법 최적확

  • 클로저는 실제 사용시 읽기 쉽고, 간결한 코드 작성을 위해 축양된 형태의 문법을 제공함

[문법 최적화(간소화)]

  • 문맥상에서 파라미터와 리턴밸류 타입 추론(Type Inference)

    - 리턴타입과 각각의 파라미터 타입을 추론이가능한경우 생략이 가능하다
  • 싱글 익스프레션인 경우(한줄), 리턴을 안 적어도 됨(Implicit Return)

  • 아규먼트 이름을 축약(Shorthand Argements) ===> $0, $1

  • ⭐️트레일링 클로저 문법: 함수의 마지막 전달 인자(아규먼트)로 클로저 전달되는 경우, 소괄호를 생략 가능

후행클로저(Trailing Closure)문법

func closureParamFunction(closure: () -> Void) {
    print("프린트 시작")
    closure()
}

closureParamFunction(closure: {
    print("프린트 종료")
})

closureParamFunction(closure: ) {      // 소괄호를 앞으로 가져오기
    print("프린트 종료")
}

closureParamFunction() {               // 아규먼트 생략가능
    print("프린트 종료")
}

closureParamFunction {                 // 소괄호를 아예 생략할 수 있다.
    print("프린트 종료")
}
**// 클로저를 인풋으로 사용하고 있는 함수의 실행이구나**

응용예제1

func closureCaseFunction(a: Int, b: Int, closure: "(Int) -> Void") {
    let c = a + b
    closure(c)      // 콜백함수 다시 파라미터를 콜백함, a와b를 더한값을 가지고 콜백
}

일단 함수의 마지막 인자가 함수이기 때문에 "후행클로저형태"로 바꿀 수 있다
"(Int) -> Void" 이 자리에 클로저를 넣으면된다 클로저는 {}안에 파라미터와 in 그리고 실행문으로 구성

closureCaseFunction(a: 5, b: 2, closure: { (number) -> () in
    print("출력할까요? \(number)")
})

closureCaseFunction(a: 5, b: 2, closure: ) { number in  // 괄호를 앞으로 뺌
    print("출력할까요? \(number)")                         // output type을 생략
}

아규먼트 ", closure: "를 생략
closureCaseFunction(a: 5, b: 2) { number in  // 괄호를 앞으로 뺌
    print("출력할까요? \(number)")
}

이때 input파라미터와 in을 생략하고 싶다면
⭐️이 경우에는 함수의 형태를 추론할  수 있어야함
1. 트레일링 클로저의 경우엔 그 위치에 정해놓은 타입밖에 없어서 그냥 생략이 가능
2. 변수에 특정 클로저를 넣을경우엔 변수타입에 함수타입(ex) (Int)->Int 를 써놓으면 생략가능)
 
closureCaseFunction(a: 5, b: 2) {
    print("출력할까요? \($0)")                  //클로저의 첫번째 (인풋)인자라는 뜻
                                             //인풋함수는 (Int)->() 타입 = 인자가 한개
}

정리하면 후행클로저의 경우엔 클로저를 제외해버리고 괄호를 닫고
closureCaseFunction(a: 5, b: 2)

함수를 클로저로 선언하면된다(이 경우엔 "(Int) -> ()"형식의 클로저)
{}안에 인풋파라미터(타입생략) in 과 실행문
-> 인풋파라미터(타입생략)in 대신 $0, $을 사용하면 {} 안에 $0, $1을 사용한 실행문만 존재

closureCaseFunction(a: 5, b: 2) { number in
    print("출력할까요? \(number)")
}

응용예제2

func closureCaseFunction1(closure: (Int, Int) -> Int) {
    closure(1, 2)
}

함수를 실행하면 closure에 (Int, Int) -> Int 타입의 함수(클로저)를 넣어줘야함
인자로 받는 함수가 마지막에 있기 때문에 후행클로저 형태로 사용가능

{}안에 파라미터두개(타입생략) in 그리고 실행문이 있어야함

{(a,b) in
    let result = a + b
    return result           // 리턴이 있는 함수이기때문에 리턴도 있어야함
                            // 리턴이 Int인게 명확하기 때문에 리턴타입생략가능
}

closureCaseFunction1(closure: {(a,b) in
    let result = a + b
    return result
})

후행클로저 형태로 변경
closureCaseFunction1()     // 클로저 제외해버리고 괄호를 닫음

closureCaseFunction1() { (a, b) in     // 뒤에 클로저를 붙임
    return a + b
}

+ ()생략, 한줄이기 때문에 return생략, 파라미터와 in을 생략하고 $0, $1사용(인풋이 두개)

closureCaseFunction1 { $0 + $1 }

콜백 함수: 함수를 실행하면서, 파라미터로 전달하는 함수

콜백함수는 주로 특정 task가 끝난이후에 특정 task를 실행하는 함수를 의미한다

→ completion 혹은 completionHandler의 파라미터는 콜백함수를 의미한다

예시1
vc.dismiss(animated: true, completion: ()->Void)
예시1의 경우 콜백함수로 dismiss를 한 후에 특정 task를 진행하는데 input파라미터가 없다
그냥 dismiss를 하고 input에 영향을 받지 않고 task를 내가 선언해줄 수 있다

예시2
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: (Timer)->())
예시2의 경우 withTimeInerval과 repeat을 통해서 Timer형식의 값을만들어서(어떻게만드는지는 모름)
Input으로 받아서 이걸 이용해서 내가 선언한 task를 진행하라는 뜻이다

예시3 
func closureCaseFunction(a: Int, b: Int, closure: (Int) -> Void) {
    let c = a + b
    closure(c)
}
closureCaseFunction(a: 5, b: 2) { number in      // 소괄호가 클로저 앞에서 닫힘
    print("출력할까요? \(number)")
}
예시3의 경우 52를 어찌저찌해서(여기선 더하기) 만든 값을 클로저의 input으로 받아서 이걸 활용해서
내가 정의한 특정 task를 진행하라는 뜻이다 

클로저의 캡처현상

Assumption 변수에 함수(클로저)를 담는 경우

Case.1 클로저 바깥에 내부에서 쓰이는변수가 정의된 경우
var stored = 0 
let closure = { (number: Int) -> Int in 
    stored += number
    return stored
}
main()에 stored라는 변수에 값을 저장하고 변수에 클로저가 할당되는 순간
-> 힙영역에 stored의 주소를 가리키는 클로저 영역이 생긴다
-> stored는 매번 업데이트가 되기때문에 초기화가 되지 않고 누적된다

Case.2 중첩 함수 내부에 있는 "함수" 자체를 return하는경우 내부의 함수가 heap영역에 존재하게됨
-> 이경우에는 특정변수의 주소를 캡쳐하는것이 아니라 값자체를 복사해서 가지고있게된다

func cal() -> ((Int)->Int) {
    var sum = 0
    func square(num: Int) -> Int {
        sum += (num*num)
        return sum
    }
    return square
}
// 변수에 함수를 할당하는 상황임
var squareFunc = cal()
이럴 경우, 함수가 실행될 때마다 sum이 바뀌게 되는데
이미 변수에 함수를 할당하는 순간 힙영역에 클로저영역이 생기기때문에 함수실행이 끝날때마다
sum을 복사해서 힙영역에 저장해둔다

@escaping키워드

@escaping 키워드가 필요한 경우 -> 함수가 종료되어도 클로저가 사라지지 않게 하는 키워드

Case1. 내부에서 사용한 클로저(함수)를 외부 변수에 저장하는 경우

var aSavedFunction: ()->()

func performEscaping(closure: ()->()) {
    aSavedFunction = closure
}

위와 같은 경우에 함수의 실행이 끝나도
aSavedFunction은 closure의 주소를 가지고 있어야하기 때문에 힙영역에 closure가 존재해야한다

Case2. 비동기코드를 사용한 경우
profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료
post-custom-banner

0개의 댓글