[Swift] Closure

정환우·2021년 8월 11일
0

iOS

목록 보기
24/24
post-thumbnail

스위프트에서 정말 많이 쓰는 클로저 문법. 쓸 때마다 너무 헷갈려서 한번 제대로 정리를 해보려고 한다!

참고한 자료(거의 베낀 수준) : 야곰님의 스위프트 프로그래밍 3판

클로저(Closure)

클로저란 무엇일까? 클로저는 변수나 상수가 선언된 위치에서 참조를 획득하고 저장할 수 있다. (Reference Capture)
이게 대체 뭔소린가...

아마 이 본문을 다 읽고나면 이해가 되지 않을까!

클로저에는 크게 세 가지 형태가 존재한다.

  • 이름이 있으면서 어떤 값도 획득하지 않는 전역함수의 형태
  • 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있는 중첩된 함수의 형태
  • 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태

이러한 형태들이 존재한다고 한다. 애플은 클로저가 정갈하고 깔끔한 스타일이라고 주장하는데, 나는 굉장히 반대...하지만 클로저는 스위프트를 사용함에 있어서 필수적이기 때문에...싫어도 꼭 알고 자유롭게 사용할 줄 알아야 한다!

가장 중요한 클로저의 표현방법은 4가지가 있는데,

  • 매개변수와 반환 값의 타입을 문맥을 통해 유추할 수 있기 때문에 매개변수와 반환 값의 타입을 생략할 수 있다.(쓸데없이 똑똑하네..)
  • 클로저에 단 한 줄의 표현만 들어있다면 암시적으로 이를 반환값으로 취급한다.
  • 축약된 전달인자 이름을 사용할 수 있다.
  • 후행 클로저 문법을 사용할 수 있다.

클로저의 기본 형식

{ (매개변수들) -> (반환 타입) in
	실행코드
    }

클로저의 기본 형식이 이렇다는 것은 알아두자.

기본 클로저

먼저, 기본적으로 클로저를 이용하여 동일한 기능을 하는 코드를 어떻게 간결하게 표현하는지 알아보자.

미리 알아두어야 할 점: swift는 매개 변수로 함수가 올 수 있다!!

우리가 사용할 함수는 sorted(by: ) 함수이다.
이 함수는 (by:) 항목에 들어간 규칙대로 정렬을 하는 기본함수이다. 애플 공식문서에서 함수의 기본형을 확인해보자.

func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]

보면은 배열의 타입과 같은 두 개의 매개변수를 가지며 Bool 타입을 반환하는 함수를 전달인자로 받을 수 있는 것인데, 이 것을 클로저로 작성을 할 수 있다는 얘기다.

먼저 익숙한 방식인 함수로 작성을 해보고, 그걸 클로저로 바꿔보자.

func compares(fst: String, second: String) -> Bool { return (fst > second) }

// names 배열은 비교할 이름 배열이라고 가정하고, 여기선는 따로 넣어주지 않겠다.

let test:[String] = names.sorted(by: compares)

이 함수의 정의는 첫 번째 문자열이 두번째 문자열보다 크다면 true를 반환하는, 그러니까 큰게 앞으로 와야한다는 것이다.

이 함수를 클로저로 변환해보면 ,

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

여기서 보면 매개 변수 개수와 타입, 반환 타입이 모두 같다. 그러니까 훨씬 간결한 축약형이라는 것.

만약 compares라는 함수를 자주 사용할 것이 아니라면 굳이 따로 정의를 해서 찾아다닐 필요가 없이 구현할 때 바로바로 클로저로 정의를 해서 넣어줄 수 있다는 것. 용도를 따지면 정말 간편하긴 하다.

후행 클로저

함수나 메서드의 마지막 전달인자로 위치하는 클로저는 메서드의 소괄호를 닫은 후 작성해도 된다. 이것이 후행 클로저. 훨씬 간결하고 가독성이 좋게 변환할 수 있다.

// 소괄호를 생략 안한 경우.
let test:[String] = names.sorted(){ (fst:String, second: String) -> Bool in return fst>second }

// 소괄호까지 생략한 경우
let test:[String] = names.sorted{ (fst:String, second: String) -> Bool in return fst>second }

소괄호까지 생략한 게 훨씬 보기 좋은 것 같다.

클로저 표현 간소화

문맥을 통한 타입 유추

앞서 말했듯이, 쓸데없이 똑똑한 우리 클로저는 문맥으로 타입을 유추할 수 있을 경우에는 굳이 타입을 명시하지 않아도 된다.

let test:[String] = name.sorted{ (fst,second) in return fst > second
}

여기서 보면 매개변수의 타입과 반환 타입을 생략해 주었는데, 엄청 간단해 졌다. 이걸 클로저 문법을 배우지 않고 읽기나 쓰기에는 힘든데, 배우고 보니 엄청 간결하고 좋다.

단축 인자 이름

매개변수의 이름을 생략할 수 있다.

스위프트에서는 클로저에서 매개변수의 이름을 명시해주지 않았을 때, 암묵적으로 $0, $1 이런식으로 매개변수 이름을 정의하도록 약속하였다. 물론 숫자는 순서대로 0,1,2,3,.... 이렇게 된다.

이를 활용하면 장점은 실행코드를 구분하기 위해 사용되었던 in 키워드마저 생략이 되어버린다.

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

뭐지...이게뭐야... 말이되나 이게.. 대박이다

암시적 반환 표현

여기서 더 줄일수 없을 거라고 생각하면 오산이다. 저기서 의미 없어보이는 return이라는 키워드까지 없애버릴 수 있다.

클로저가 반환 값을 갖는 클로저이고, 내부의 실행문이 단 한줄이라면 이 키워드마저 제거가 가능하다. 암시적으로 반환 값이라고 생각하기 때문이다!!

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

와 처음에 사용했던 함수랑 비교해보면 진짜 말도 안되게 간결해지긴 한다.

값 획득

클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득할 수 있다고 한다. 이게 무슨 의미냐 하면, 주변에 정의한 상수나 변수가 더 이상 존재하지 않더라도 그 값을 자신 내부에서 수정하거나 참조할 수 있다는 것이다.

왜 이렇게 사용하냐 하면, 클로저가 비동기 작업에 많이 사용되기 때문이라고 한다. 비동기 작업 Call-Back을 작성하는 경우, 현재 상태를 미리 획득해두지 않으면 실제 클로저의 기능을 실행하는 순간에는 주변 상수나 변수가 이미 메모리에 존재하지 않는 경우가 발생할 수 있다!

이 책에서 예시로 중첩 함수 주변의 변수나 상수를 획득해 놓는 예시를 들었다.

func makeIncremeter(forIncrement amount:Int) -> ( () -> Int ) {

var runningTotal = 0
func incremeter() -> Int {
	runningTotal += amount
    	return runningTotal
    }
    
return incremeter
}

여기서 중첩함수인 incremeter는 주변에 있는 runningTotalamount값을 획득 한다고 한다.

makeIncremeter 함수의 반환 타입을 보면 () -> Int함수객체 반환을 의미한다.
반환하는 함수는 매개변수를 받지 않고 반환 값이 Int 타입인 함수인 것이다!

makeIncremeter 함수가 실행 될 때 중첩함수는 참조를 획득하고, 이 함수의 실행이 끝나도 사라지지 않는다. 게다가 중첩함수가 호출이 될 때마다 계속해서 사용할 수 있다!

좋은 예시를 책에서 들어주었다.


let incrementByTwo(): (()->Int) = makeIncremeter(forIncrement: 2)
let incrementByTwo2(): (()->Int) = makeIncremeter(forIncrement: 2)
let incrementByTen(): (()->Int) = makeIncremeter(forIncrement: 10)

// 3가지 상수에 함수를 할당해주었다.

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

let first:Int = incrementByTwo2() // 2
let second:Int = incrementByTwo2() // 4
let third:Int = incrementByTwo2() // 6

let first:Int = incrementByTen() // 10
let second:Int = incrementByTen() // 20
let third:Int = incrementByTen() // 30

각각의 함수는 언제 호출이 되더라도 자신만의 변수를 갖고 카운트하게 되며, 다른 함수의 영향을 전혀 받지 않는다. 왜냐하면 참조를 미리 획득했기 때문이다.

여기서 드는 의문점 : 클래스 인스턴스의 프로퍼티의 경우에는 ?

이 경우에는 강한참조 순환 문제가 발생할 수 있으나, 이 문제는 획득목록을 통해 없앨 수 있다고 한다.
자세한 내용은 이 책 뒤에서 다룬다고 하는데, 시간이 되면 한번 포스팅해볼것.

클로저는 참조다.

클로저와 함수는 참조타입이다.
참조가 무엇인지는 굳이 설명하지 않겠고, 예시로 바로 들어가자.

let incrementByTwo(): (()->Int) = makeIncremeter(forIncrement: 2)
let sameinc(): (()->Int) = incrementByTwo()

// 같은 클로저를 참조하고 있다.

let test:Int = incrementByTwo() // 2
let test2:Int = sameinc() // 4

같은 클로저를 참조하는 경우, 동작 결과가 같은 것을 알 수 있다.

    

0개의 댓글