티스토리 뷰

Programming/Swift

Swift Beginner | Closure

글그리 2020. 11. 18. 20:26

함수형 언어는 함수를 1급 객체로 다루기 때문에 함수 자체가 여기저기에 사용될 수 있다. 변수를 파라미터로 전달할 때 변수 이름으로 넘겨주지 않고, 값을 직접 넘겨줄 수 있듯이 함수도 따로 이름에 담지 않고 연산 내용을 직접 전달할 수 있다.

closure는 함수보다 좀 더 추상화된 개념이며, 함수는 이름이 있는 전역 closure라고 할 수 있다. (근데 왜 책은 closure보다 함수를 먼저 알려주는지 모르겠다.) 따라서 아래 두 표현은 같은 내용이라고 할 수 있다.

// function
func foo(count: Int) -> String { 
	return “there are \(count) flowers over there.” 
}

// closure
let foo = { (count: Int) -> String in 
	return “there are \(count) flowers over there.”
}

기본적인 표현법은 함수 표현법과 비슷하다. func 키워드와 이름이 빠졌다고 보면 된다. 단 처음부터 끝까지 중괄호로 묶고 함수 내용과 선언부는 in으로 구분한다.

이제부터 코드가 점점 어려워지기 시작한다. swift는 코드를 간결하게 만들기 위해 불필요한 괄호나 연산자를 생략할 수 있도록 되어있다. 예를 들어 위 함수 내부에는 String을 반환하는 코드만 있기 때문에 return type과 return연산자가 생략될 수 있다.

let foo = { (count: Int) in
	// return can be omit.
	"there are \(count) flower over there"
}

아직 더 생략할 수 있다. swift standard library에는 sorted라는 함수가 있는데, 이 함수는 Boolean값을 반환하는 closure를 파라미터로 받는다. 아마 배열을 돌면서 이 closure로 두 값을 검사하고 swap하는 식으로 정렬하는 것 같다.

let alpha = ["b", "e", "a", "c", "f"]

// full expression
let reversedAlpha = alpha.sorted(by: {(a: String, b: String)->Bool in 
	return a > b
})

// omit type and return type
let sortedAlpha = alpha.sorted(by: {a, b in a < b})

// omit closure head
// $0 = first parameter $1 is second parameter
let realShort = alpha.sorted(by: {$0 < $1})

for a in sortedAlpha {
	print(a)			// > a, b, c, e, f
}

for a in reversedAlpha {
	print(a)			// > f, e, c, b, a
}

for a in realShort {
	print(a)			// > a, b, c, e, f
}

reverseAlpha에 사용된 sort closure나, sortedAlpha, realShort에 사용된 closure는 모두 같지만 다르게 작성됐다.

 

 

 

Capturing Value

closure는 자신 주변에 있는 자신이 관여하는 변수들을 생성 시점에 저장(capture)한다. closure가 만들어질 때 마다 논리적으로 접근하는 모든 값이 저장 되며, 개별 메모리를 가진다.

func makeIncrementer(_ amount: Int) -> ()->Int {
	var total = 0
	
	return {
		total += amount
		return total
	}
}

let incrementByTen = makeIncrementer(10)

print(incrementByTen())	
// 10

let incrementByFive = makeIncrementer(5)

print(incrementByTen())
// 20
print(incrementByFive())
// 5

원래 함수 내에서 생성된 지역 변수는 함수가 끝나면서 메모리에서 제거된다 라고 알고 있다. 그런데 makeIncrementer에 의해 생성된 incrementByTen 이라는 함수는 makeIncrementer 함수 내에서 정의한 total이라는 변수와 makeIncrementer의 파라미터인 amount에 접근하여 값을 변경하는데 makeIncrementer 함수가 종료한 후에 호출해도 문제 없이 동작한다.

이는 makeIncrementer 함수가 호출되는 시점에 incrementer를 반환 하면서 이 함수와 이 함수가 접근하는 메모리를 capture했기 때문이다. 생성 시점에 capture되기 때문에 incrementByTen과 incrementByFive는 각각 다른 total 변수를 가지게 된다.

closure는 reference type이기 때문에 위에서 생성한 closure처럼 값을 capture한 closure를 다른 closure에 대입해도 값이 그대로 이동한다.

let copycat = incrementByTen

print(copycat())	// > 30

 

 

 

closure escaping

closure는 생성 시점에 외부 변수를 capture하여 접근할 수 있다고 했다. 그러면 반대로 closure를 외부에서 접근할 수 있을까?

원래 함수의 paremter로 전달된 closure는 함수가 끝날 때 메모리에서 사라진다. 당연한 이야기지만 foo로 전달한 closure를 외부에서 접근할 수 없다.

func foo(with: (Int) -> Void) {
    with(10)    // local closure
}

foo {a in
    print(a)
}
// print

// can't access 'with' closure from here

@escaping 키워드를 사용하면 가능하다. 즉, 기본 옵션은 @nonescaping이다. 짧은 예제를 만들기가 참 애매한데 함수의 외부에서 closure를 사용해야 할 일이 뭐가 있을까. 어떤 함수를 실행한 후 정리를 위해 또다른 루틴을 실행해야 할 경우를 생각해보자. 예를 들어 http 통신에서 request를 실행한 후에 response를 처리하는 루틴을 추가하는 것 처럼 말이다.

sys class는 foo를 실행하면 자기자신을 반환하도록 되어있다. 그리고 escaper라는 기본 closure를 가지고 있는데, foo가 종료하고 실행해야 한다. foo를 호출하면서 전달한 closure는 @escaping으로 선언 되었기 때문에 escaper에 대입할 수 있고, foo가 종료하고 난 후에도 메모리에 살아있다.그래서 foo를 호출하고 바로 escaper를 호출할 수 있다.

class sys {
    // default escaper
    var escaper: (Int) -> Void = { escapeValue in
        print(escapeValue)
    }
    
    func foo(customEscaper: @escaping (Int) -> Void) -> Self{
        print("run foo")

				// customEscaper can escape foo scope
        escaper = customEscaper
        
        return self
    }
}

let a = sys()
// make closure and call escaper
a.foo { e in
    print("custom escaper ", e)
}.escaper(10)
// run foo
// custom escaper

 

 

 

auto closure

위 closure에서 sorted를 호출하는 예제를 보면 함수에 closure를 전달하기 위해 {}로 closure의 범위를 표시하고 있다. closure의 내용이 많으면 여러 줄로 표현하면서 {}로 감싸주는 것이 좋겠지만 한 줄일 경우 괄호를 생략할 수 있다.

그래서 전달받은 함수를 자동으로 closure로 만들어주는 키워드로 @autoclosure를 사용한다. 이 키워드를 사용하면 괄호가 없어도 자동으로 closure로 만들어주기 때문에 함수 내부에서는 똑같이 사용하면 된다.

var names = ["c++", "java", "swift", "python"]

func printWithPop(arr arrInLine: ()->String) {
	print("this will pop : \(arrInLine())")
}

printWithPop(arr: {
	// remove return removed String
	names.remove(at: 0)
})
// this will pop : c++

@autoclosure를 사용해서 괄호를 생략 해보자.

pushWork함수는 todoList에 작업들을 추가하는 함수 이며, 이 작업은 closure이다. pushWork를 호출하는 2줄을 보면 names 배열에서 0번째 값을 제거하는 작업을 두번 넣었다. closure는 생성될 때가 아니라 호출될 때 실행되기 때문에 이 두 번의 함수 호출 에서는 names가 수정되지 않는다. 실제로 for문 전에 names의 count를 출력 해보면 여전히 4인 것을 볼 수 있을 것이다.

for문을 돌면서 print(work())가 실행될 때 비로소 names에서 값들이 제거되기 시작한다.

todoList는 ()->String 형태를 가지는 closure배열이기 때문에 remove뿐 아니라, String을 반환하는 어떤 closure도 사용할 수 있다.

var todoList: [()->String] = []
var names = ["c++", "java", "c#", "swift"]

func pushWork(work: @autoclosure @escaping ()->String) {
	todoList.append(work)
}

pushWork(name: names.remove(at: 0))
pushWork(name: names.remove(at: 0))
// just add closure names don't change yet.

for work in todoList {
	// now names.remove run
	print(work())
}
// c++
// java

'Programming > Swift' 카테고리의 다른 글

Swift Beginner | 타입캐스팅 is as  (0) 2020.11.18
Swift Beginner | 함수의 시작과 끝 guard defer  (0) 2020.11.18
Swift Beginner | 함수(2)  (0) 2020.11.18
Swift Beginner | 함수(1)  (0) 2020.11.18
Swift Beginner | 반복문  (0) 2020.11.18
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함