클로저

박형석·2021년 11월 30일
0

Swift

목록 보기
5/20
post-thumbnail
post-custom-banner

클로저란?

  • 생김새
{ 매개변수 -> 반환 타입 in
	//실행 코드
}
  • C언어나 Objective-C의 블록, 다른 프로그래밍 언어의 람다(lambda)와 유사
  • 정의 : 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것. (함수랑 비슷 사실 함수가 클로저)
  • 종류
    • 이름 O 값 획득 X 전연함수의 형태
    • 이름 O 값 획득 O 중첩된 함수의 형태 → 함수를 전달인자로 받아서 사용
    • 이름 X 값 획득 O 주변 문맥에 따라, 축약 문법으로 작성한 형태
  • 왜 클로저일까?
    클로저는 변수나 상수가 선언된 위치에서 참조를 획득(Reference Capture)하고 저장할 수 있다. 이를 변수나 상수의 클로징(잠금)이라고 하며 클로저는 여기서 착안된 이름이다. 사실 이 안에서 메모리 릭의 위험성이 많이 존재. ARC 때문에 괜춘...

기본 & 후행

sorted(by:) 메서드로 클로저를 이해해보자.

  • 스위프트 라이브러리의 메서드 정의
public func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Elemet]
  • 원래 함수를 쓴다면 sorted(by:)에 (Element, Element) -> Bool에 해당하는 함수를 넣어야 함
func backward(first: String, second: String) -> Bool {
 return first > second
}

// sorted(by: backward)
  • 위의 함수를 클로저로 대체
sorted(by: { first: String , second: String -> Bool in 
 return first > second
}
  • 후행 클로저를 사용
    • 함수나 메서드가 마지막 전달 인자로 위치하는 클로저를 소괄호를 닫고 나머지 작성, 소괄호 생략 가능
    • 매개변수에 클로저가 여러 개 있는 경우, 다중 후행 클로저을 사용
sorted { first: String , second: String -> Bool in 
 return first > second
}

doSomething { something: String in
 // do it
} onSuccess : { result: Any? in
 // when success
} onFailure : { error: Error in
 // when fail
}

표현 간소화

  • 타입 유추를 통한 타입 생략
sorted { first , second in 
 return first > second
}
  • 인자의 이름 단축
sorted {
 return $0 > $1
}
  • 암시적 return : 이로써 클로저가 그냥 한 줄로 적힌다면 그건 리턴값임을 알 수 있다
sorted { $0 > $1 }
  • 연산자 함수 이용 : 연산자는 함수였다... 이 경우는 함수를 파라미터로 넣어주는 거니 위 backward처럼~
sorted(by: >)

값 획득

  • 클로저는 자신이 (정의된 위치의 주변 문맥을 통해) 상수나 변수를 Capture(획득)할 수 있다.
    → 그래서 클로저는 (주변에 정의한) [상수나 변수가 더 이상 존재하지 않더라도] 해당 상수나 변수의 값을 자신 내부에 참조하거나 수정할 수 있다.

  • 이 기능이 왜 필요할까?
    클로저는 비동기 작업에 많이 사용된다. 클로저를 통해서 비동기 콜백(Async Call-back)을 작성하는 경우, 현재 상태를 미리 획득해두지 않으면, [실제로 클로저의 기능을 실행하는 순간]에는 (주변의 상수나 변수)가 이미 메모리에 존재하지 않는 경우가 발생한다. 그래서 콜백시에 사용될 수 있도록 미리 참조를 획득해놓는 것이다. (미리 참조를 획득했다면, 서로 영향을 주고 받지 않는 deep copy가 이뤄졌다는 말이다)

    이 주변이라는 말은 지역변수, 상수 뿐 아니라 멤버변수, 상수도 가능하다. 그렇기 때문에 정확하게 맥락, 즉 어디 소속인지, 누구를 참조해서 캡처를 하려는지 명확하게 표현을 해줘야 한다. 그래서 항상 클래스 안에서 변수나 상수를 참조하려고 할 때 self를 쓰라고 나온다.

    이런 참조 덕분에 캡처된 존재들은 RC가 올라간다. 메모리에서 해제되지 않고 그 클로저가 끝날 때까지 계속 유지하고 있는다. 이 때문에 비동기 작업시 메모리릭이 발생할 수 있다. self를 참조하고 있는 다른 객체, 클로저 등등에서 강한 순환 참조가 발생할 수 있기 때문이다.

클로저는 참조 타입

클로저는 참조 타입이다. 즉 함수나 클로저를 상수나 변수에 할당할 때마다 사실은 상수나 변수에 함수나 클로저의 참조를 설정(swallow copy)하는 것일 뿐이다. 매번 클로저를 복사(deep copy)하는게 아니다. 따라서 해당 클로저를 다른 상수에 할당해준다면 이 두 상수가 같은 클로저를 가리킨다는 의미다.

클로저를 클래스의 인스턴스 프로퍼티로 할당하면, 클로저는 클래스 인스턴스(self) 혹은 클래스 인스턴스의 멤버(self.property)를 획득할 수 있지만 강한 순환 참조로 엮이게 된다. 클래스는 클로저를 강한 참조로 소유하고 있고 또 클로저 역시 클래스를 강함 참조로 소유하고 있기 때문에 메모리에서 해제가 되지 않는 것이다. 메모리릭! [weak self], [unowned self]를 클로저 안에서 자주 쓰게 되는 이유이다.

클로저의 참조 타입과 값 획득 사이의 관계

  • 캡처 자체는 value capture다. 클로저가 미리 캡처를 하기 때문에, 같은 상수와 변수를 캡처한 많은 클로저가 있다고 해도 각각 클로저는 각각의 캡처 값을 가진다. 그리고 서로에게 영향을 주지 않는다. (물론 같은 형태라도 참조가 함수라면 값을 공유할 이유는 없다.)
  • 하지만 캡처해서 안에 저장해두는 형태는 참조 타입니다. 이는 클로저가 참조 타입이기 때문이다. 그렇기 때문에 해당 클로저를 재사용, 혹은 참조를 공유한 클로저를 사용한다면 안의 변수는 상태 변화를 겪게 된다.

탈출 클로저 @escaping

함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출될 때 클로저가 함수를 탈출한다고 표현한다. 주로 비동기식 작업을 실행하는 함수들은 클로저를 completionHandler를 전달인자로 사용.

  • 탈출 경우의 수 ?
    • 함수의 전달인자로 들어온 클로저를 다시 함수의 외부로 반환하는 경우
    • 함수에서 반환한 클로저가 함수 외부의 상수나 변수에 저장되는 경우
    • 함수가 작업을 종료하고 난 이후 실행해야 하는 일이 있는 경우
  • 에러
    • 위 처럼 탈출할 조건이 명확하다? @escaping을 붙여야 컴파일 에러가 안난다.
    • @escaping을 사용한 클로저의 내부를 구현할 때, 해당 타입의 프로퍼티, 메서드, 서브스크립트에 접근하려면 self 키워드를 써야 한다.
  • withoutActuallyEscaping(_:do:)
func hasElements(in array: [Int], match predicate: (Int)->Bool) -> Bool {
 return array.lazy.filter { predicate($0) }.isEmpry == false
}

이 경우 lazy 때문에 filter의 연산이 arr에 실제로 접근할 때까지 미뤄진다. 즉 match라는 클로저로 들어온 인자가 함수 종료 후에 호출되야 하는건 아니지만 filter라는 함수에서 요구를 하기 때문에 어쩔 수 없이 나가야 하는 상황이다. 이 경우에 withoutActuallyEscaping(_:do:)를 사용한다.

func hasElements(in array: [Int], match predicate: (Int)->Bool) -> Bool {
 return withoutActuallyEscaping(predinate, do: { escapablePredicate in
  return array.lazy.filter { escapablePredicate($0) }.isEmpry == false
 })
}

이렇게 withoutActuallyEscaping(_:do:)를 사용해서 비탈출 클러저를 탈출 클로저로 사용할 수 있다.

자동 클로저 @autoclosure

함수의 전달인자로 전달하는 표현(String, Int 등) 자동으로 변환해주는 클로저이다. 연산의 지연(클로저의 기본적인 특징인 실행해야 연산을 실행하는), 문법적 편의를 위한 것인데, 자주 사용하면 좋지 않고 사용하더라도 자동 클로저임을 명시해야 한다.

func serveCustomer(_ customerProvider: @autoclosure () -> String) {
 print("\(customerProvider())"
}

serveCostomer(customerLine.removeFirst()) // "Jepline"
  • 매개변수에 @autoclosure를 사용하고 있기 때문에 자동 클로저를 사용한다.
  • 위의 코드를 보면 serveCostomer의 전달인자로 클로저가 들어가야 함에도 불구하고 String이 들어간다.
  • 이 String을 자동으로 클로저로 변환해준다. 즉 전달인자가 없고 String을 반환하는 클로저로 변환해준다.
    이 둘 사이의 타입은 당연히 맞춰야 한다.
  • 자동 클로저를 사용하면, 기존의 방법처럼 클로저를 인자로 전달할 수 없다.

특징

  • 기본적으로 @noescaping 속성이다. 탈출을 원할시 @autoclosure @escaping 요렇게
  • 값을 인자없는 클로저로 만드는게 자동 클로저이기 때문에 자동 클로저는 전달인자를 갖지 않는다.
  • 자동 클로저는 호출시 자신이 감싸고 있는 코드의 결과값을 반환한다.
    자동 클로저는 전달인자를 갖지 않는다. 그리고 암시적 반환으로 return을 생략할 수 있다. 따라서 자연스럽게 자신이 감사고 있는 코드의 결과값을 반환하게 되는 것
profile
IOS Developer
post-custom-banner

0개의 댓글