클로져(closure)

Ios_Roy·2023년 3월 5일
0

swift 문법

목록 보기
16/29
post-thumbnail

클로져

이름이 없는(익명) 함수

클로져와 함수는 기능은 동일 한테 형태만 다르다고 생각 하면됨

// 함수의 정의

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

// 클로저의 형태

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

함수를 1급객체(First-class Object)로 취급

  • 함수를 1급 객체로 취급
  • 함수를 변수에 할당할 수 있다.
  • 함수를 파라미터로 전달이 가능하다.
  • (함수에서) 함수를 반환할 수 있다. (리턴 가능)

클로저의 형태

  • 파라미터의 타입의 생략도 대부분 가능하다.
// 함수의 형태

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

// 클로저의 형태

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

클로저의 사용하는 이유?

  • (클로저를 파라미터로 받는 함수)정의
  • 함수를 실행할때 클로저 형태로 전달 (클로저를 사용하는 이유)

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

func printSwiftFunction() {          // 함수를 정의
    print("프린트 종료")
}

let printSwift = { () -> () in      // 클로저를 정의
    print("프린트 종료")
}

closureParamFunction(closure: printSwiftFunction)

closureParamFunction(closure: printSwift)

closureParamFunction(closure: { () -> () in
    print("프린트 종료")           // 본래 정의된 함수를 실행시키면서, 클로저를 사후적으로 정의 가능
})                              // (활용도가 늘어남)

closureParamFunction(closure: { () -> () in
    print("프린트 종료 - 1")
    print("프린트 종료 - 2")
})

func closureCaseFunction(a: Int, b: Int, closure: (Int) -> Void) {
    let c = a + b
    closure(c)
}

closureCaseFunction(a: 5, b: 2, closure: { (n) in    // 사후적 정의
    print("이제 출력할께요: \(n)")
})

closureCaseFunction(a: 5, b: 2) {(number) in      // 사후적 정의
    print("출력할까요? \(number)")
}

closureCaseFunction(a: 5, b: 3) { (number) in      // 사후적 정의
    print("출력")
    print("출력")
    print("출력")
    print("값: \(number)")
}

클로저의 문법 최적화

  • 문맥상에서 파라미터와 리턴밸류 타입 추론(Type Inference)
  • 싱글 익스프레션인 경우(한줄), 리턴을 안 적어도 됨(Implicit Return)
  • 아규먼트 이름을 축약(Shorthand Argements) ===> $0, $1
  • 트레일링 클로저 문법: 함수의 마지막 전달 인자(아규먼트)로 클로저 전달되는 경우, 소괄호를 생략 가능

트레일링(Trailing) 클로저

후행 클로저 문법

  • 문법의 간소화
func closureParamFunction(closure: () -> Void) {
    print("프린트 시작")
    closure()
}

// 2) 함수를 실행할때 클로저 형태로 전달
// 함수의 마지막 전달 인자(아규먼트)로 클로저 전달되는 경우, 소괄호를 생략 가능

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

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

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

// 소괄호를 아예 생략할 수 있다.
// ==> 아래 형태가 함수를 실행하고 마지막 아규먼트로 클로저를 전달했다는 형태에 익숙해져야함

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

콜백 함수

  • 함수를 실행하면서, 파라미터로 전달하는 함수
  • 주로 함수가 실행된 결과는 콜백 함수로 전달받아 처리하기 때문에
vc.dismiss(animated: true) {
    print("화면을 닫는 것을 완료했습니다.")
}

멀티플 트레일링 클로저 → Swift 5.3

  • 여러개의 함수를 파라미터로 사용할때
  • 기존 방식에서는 마지막 클로저만 트레일링 클로저로 쓸 수 있었음
    • (클로저의 경계에서 코드가 헷갈릴 가능성이 있었음)

      func multipleClosure(first: () -> (), second: () -> (), third: () -> ()) {
          first()
          second()
          third()
      }
      
      // 기존 방식에서는 마지막 클로저만 트레일링 클로저로 쓸 수 있었음
      // (클로저의 경계에서 코드가 헷갈릴 가능성이 있었음)
      
      multipleClosure(first: {
          print("1")
      }, second: {
          print("2")
      }) {
          print("3")
      }
      
      multipleClosure {
          print("mutil-1")
      } second: {
          print("mutil-2")
      } third: {
          print("mutil-3")
      }
      
      // 아규먼트 레이블을 생략하는 경우
      
      func multipleClosure2(first: () -> (), _ second: () -> (), third: () -> ()) {
          first()
          second()
          third()
      }
      
      // 아큐먼트 레이블을 생략하지 못함
      
      multipleClosure2 {
          print("1")
      } _: {
          print("2")
      } third: {
          print("3")
      }

      클로저의 메모리

      구분값형식참조형식
      타입Value TypeReference Type
      메모리상의 저장 위치필요시에 항상 메모리의 값이 복사되어 전달 값의 저장; Stack필요시에 항상 메모리의 주소를 전달 값의 저장 ; Heap(주소를 Stack에 저장)
      메모리 관리 방식값이 들어 있는 스택의 스코프가 종료되면 메모리에서 자동 제거RC(Reference Counting)을 통해 메모리를 관리 Swift에서 사용하는 ARC 모델
      각 형식의 타입 예시스위프트 기본 타입( Int, String ....) 튜플 , 구조체 , 열거형 , 컬렉션 등클래스 , 클료져

      클로저의 캡처

    • 함수 내에서 함수를 실행하고, 값을 리턴하는 일반적인 함수

    • 아래와 같은 경우, 중첩함수로 이루어져 있고

    • 내부 함수 외부에 계속 사용해야하는 값이 있기 때문에 캡처 현상이 발생

    • (함수/클로저를 변수에 저장하는 시점에 캡처) ==> 클로저도 레퍼런스 타입

      func calculate(number: Int) -> Int {
          
          var sum = 0
          
          func square(num: Int) -> Int {
              sum += (num * num)
              return sum
          }
          
          let result = square(num: number)
          
          return result
      }
      
      calculate(number: 10)
      calculate(number: 20)
      calculate(number: 30)
      func calculateFunc() -> ((Int) -> Int) {
          
          var sum = 0
          
          func square(num: Int) -> Int {
              sum += (num * num)
              return sum
          }
          
          return square
      }
      
      // 변수에 저장하는 경우(Heap 메모리에 유지)
      var squareFunc = calculateFunc()
      
      squareFunc(10)
      squareFunc(20)
      squareFunc(30)
      
      // 변수에 저장하지 않는 경우
      // (Heap메모리에 유지하지 않음)
      
      //calculateFunc()(10)
      //calculateFunc()(20)
      //calculateFunc()(30)
      
      // 레퍼런스 타입
      var dodoFunc = squareFunc
      dodoFunc(20)

      @escaping

      함수의 파라미터 중 클로저 타입에 @escaping 키워드가 필요한 경우

    • 원칙적으로 함수의 실행이 종료되면 파라미터로 쓰이는 클로저도 제거됨

    • @escaping 키워드는 클로저를 제거하지 않고 함수에서 탈출시킴(함수가 종료되어도 클로저가 존재하도록 함)

    • 클로저가 함수의 실행흐름(스택프레임)을 벗어날 수 있도록 함

    1. 클로저가 함수의 실행흐름(스택프레임)을 벗어날 수 있도록 함

    2. 클로저를 외부변수에 저장 (@escaping 필요)

      @escaping 사용의 대표적인 경우

    • 어떤 함수의 내부에 존재하는 클로저(함수)를 외부 변수에 저장

    • GCD (비동기 코드의 사용)

      var aSavedFunction: () -> () = { print("출력") }
      
      //aSavedFunction()
      
      func performEscaping2(closure: @escaping () -> ()) {
          aSavedFunction = closure         // 클로저를 실행하는 것이 아니라  aSavedFunction 변수에 저장
          //closure()
      }
      
      //aSavedFunction()
      
      performEscaping2(closure: { print("다르게 출력") })
      
      //aSavedFunction()

      GCD 비동기 코드

      func performEscaping1(closure: @escaping (String) -> ()) {
          
          var name = "홍길동"
          
          DispatchQueue.main.asyncAfter(deadline: .now() + 1) {   //1초뒤에 실행하도록 만들기
              closure(name)
          }
          
      }
      
      performEscaping1 { str in
          print("이름 출력하기: \(str)")
      }

      @autoclosure 키워드

    • 클로저 앞에 @autoclosure 키워드 사용(파라미터가 없는 클로저만 가능)

    • 일반적으로 클로저 형태로 써도되지만, 너무 번거로울때 사용

    • 번거로움을 해결해주지만, 실제 코드가 명확해 보이지 않을 수 있으므로 사용 지양(애플 공식 문서)

    • 잘 사용하지 않음. 읽기위한 문법

    • autoclosure는 기본적으로 non-ecaping 특성을 가지고 있음

      // 클로저 앞에 @autoclosure 키워드 사용(파라미터가 없는 클로저만 가능)
      
      func someFuction(closure: @autoclosure () -> Bool) {
          if closure() {
              print("참입니다.")
          } else {
              print("거짓입니다.")
          }
      }
      
      var num = 1
      
      // 실제로 함수를 사용하려고 하면
      
      //someFuction(closure: <#T##Bool#>)
      
      someFuction(closure: num == 1)
      
      // autoclosure는 기본적으로 non-ecaping 특성을 가지고 있음
      
      func someAutoClosure(closure: @autoclosure @escaping () -> String) {
          DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
              print("소개합니다: \(closure())")
          }
      }
      
      someAutoClosure(closure: "제니")

      캡처 현상

    • 함수 내에서 함수를 실행하고, 값을 리턴하는 일반적인 함수

      func calculate(number: Int) -> Int {
          
          var sum = 0
          
          func square(num: Int) -> Int {
              sum += (num * num)
              return sum
          }
          
          let result = square(num: number)
          
          return result
      }
      
      calculate(number: 10)
      calculate(number: 20)
      calculate(number: 30)

      변수를 캡처하는 함수(중첩 함수의 내부 함수) - 캡처 현상의 발생

    • 내부 함수 외부에 계속 사용해야하는 값이 있기 때문에 캡처 현상이 발생

    • (함수/클로저를 변수에 저장하는 시점에 캡처) ==> 클로저도 레퍼런스 타입

      func calculateFunc() -> ((Int) -> Int) {
          
          var sum = 0
          
          func square(num: Int) -> Int {
              sum += (num * num)
              return sum
          }
          
          return square
      }
      
      // 함수를 변수에 할당하는 경우
      // (Heap 메모리에 유지를 해야함. 즉, 함수라 하더라도 클로저 방식으로 동작)
      var squareFunc = calculateFunc()
      
      squareFunc(10)
      squareFunc(20)
      squareFunc(30)

      캡처리스트의 형태

    1. 파라미터가 없는 경우

      { [캡처리스트] in
      
           }
    2. 파라미터가 있는 경우

      { [캡처리스트] (파라미터) -> 리턴형 in
      
           }

밸류(Value) 타입 캡처와 캡처리스트

  • 클로저는 자신이 사용할 외부의 변수를 캡처함
  • 밸류타입의 값을 캡처함
  • (즉, 값 자체를 복사해서 가지고 계속 사용)
  • 즉, 값 타입에서는 참조하는 값의 변경을 방지(외부적인 요인에 의한)하고 사용하고자 할때, 사용
var num = 1

let valueCaptureClosure = {
    print("밸류값 출력(캡처): \(num)")
}

num = 7
valueCaptureClosure()   // 몇을 출력할까요?

// 밸류타입의 참조(메모리주소)를 캡처함
// (즉, 값 자체를 복사해서 가지고 있는 것이 아니고, num의 주소를 캡처해서 계속 사용)

num = 1
valueCaptureClosure()

let valueCaptureListClosure = { [num] in      // 캡처리스트에서 밸류(value) 타입 캡처
    print("밸류값 출력(캡처리스트): \(num)")
}

num = 7
valueCaptureListClosure()      // 몇을 출력할까요?

참조(Reference) 타입 캡처와 캡처리스트

class SomeClass {
    var num = 0
}

var x = SomeClass()
var y = SomeClass()

print("참조 초기값(시작값):", x.num, y.num)

let refTypeCapture = { [x] in
    print("참조 출력값(캡처리스트):", x.num, y.num)
}

/**============================================
  x - (참조타입) 주소값 캡처, x를 직접참조로 가르킴
  y - 변수를 캡처해서, y변수를 가르킴(간접적으로 y도 동일)
==============================================**/

x.num = 1
y.num = 1

//x = SomeClass()
//y = SomeClass()

print("참조 초기값(숫자변경후):", x.num, y.num)      // 1, 1

refTypeCapture()                                // 1, 1     (Not) 0, 1

print("참조 초기값(클로저실행후):", x.num, y.num)     // 1, 1

강한 참조 사이클 문제의 해결 - 캡처리스트 + weak/unowned

var z = SomeClass()

let refTypeCapture1 = { [weak z] in
    print("참조 출력값(캡처리스트):", z?.num)
}

refTypeCapture1()                        // Optional(0)

let refTypeCapture2 = { [unowned z] in
    print("참조 출력값(캡처리스트):", z.num)
}

refTypeCapture2()                        // 0

캡처리스트에서 바인딩하는 것도 가능

var s = SomeClass()

let captureBinding = { [z = s] in   // 내부에서 변수명 바꿔서 사용가능 (외부변수와 헷갈리는 것을 방지)
    print("바인딩의 경우:", z.num)
}

let captureWeakBinding = { [weak z = s] in
    print("Weak 바인딩 경우:", z?.num)
}

captureBinding()
captureWeakBinding()

일반적인 클로저의 사용(객체 내에서의 사용, self키워드)

  • 클로저 내에서 객체의 속성 및 메서드에 접근 시에는 "self키워드"를 반드시 사용해야함
  • (강한 참조를 하고 있다는 것을 표시하기위한 목적) ===> 여기서는 Dog의 RC를 올리는 역할 +1
  • self.name
  • [self] =====> Swift 5.3이후
  • 구조체의 경우, self를 생략하는 것도 가능 (Swift 5.3이후) ⭐️
class Dog {
    var name = "초코"

    func doSomething() {
        // 비동기적으로 실행하는 클로저
        // 해당 클로저는 오래동안 저장할 필요가 있음 ==> 새로운 스택을 만들어서 실행하기 때문
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
}

var choco = Dog()
choco.doSomething()

클로저 캡처 리스트 - Strong Reference Cycle(강한 참조 순환) 해결

  • 클로저는 기본적으로 캡처 현상이 발생
  • 클로저와 인스턴스가 강한참조로 서로를 가르키고 있다면(Strong Reference Cycle),
    메모리에서 정상적으로 해제되지 않고, 메모리 누수 현상이 발생
  • 캡처리스트 내에서, 약한 참조 또는 비소유 참조를 선언해서 문제해결
class Person {
    let name = "홍길동"
    
    func sayMyName() {
        print("나의 이름은 \(name)입니다.")
    }
    
    func sayMyName1() {
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
    
    func sayMyName2() {
        DispatchQueue.global().async { [weak self] in
            print("나의 이름은 \(self?.name)입니다.")
        }
    }
    
    func sayMyName3() {
        DispatchQueue.global().async { [weak self] in
            guard let weakSelf = self else { return }   // 가드문 처리 ==> 객체없으면 일종료
            print("나의 이름은 \(weakSelf.name)입니다.(가드문)")
        }
    }
}

let person = Person()

person.sayMyName()
person.sayMyName1()
person.sayMyName2()
//person.sayMyName3()
profile
iOS 개발자 공부하는 Roy

0개의 댓글