Swift - 함수 (3_Closure)

이한솔·2023년 8월 2일
0

Swift 문법 🍎

목록 보기
11/32

Closure

Closure: 일회용 함수를 작성할 수 있는 구문
일회용함수 (익명함수): 한 번만 사용할 구문들의 집합이면서, 그 형식은 함수로 작성되어야 하는 제약조건이 있을 때 만들어 사용할 수 있는 함수

클로저는 외부 함수 내에서 내부 함수를 반환하고, 이 내부 함수가 외부 함수의 지역 변수나 상수를 참조할 때 만들어진다.



클로저 종류

  1. 전역 함수
    이름이 있으며, 주변 환경에서 캡처할 어떤 값도 없는 클로저
// 전역 함수
func greet() {
    print("Hello, World!")
}

// 사용 예
greet()  // "Hello, World!"
  1. 중첩 함수
    이름이 있으며 자신을 둘러싼 함수로부터 값을 캡처할 수 있는 클로저
func outerFunction() -> () -> Void {
    let outerValue = "Hello"
    
    func innerFunction() {
        print(outerValue)
    }
    
    return innerFunction
}

// 사용 예
let closure = outerFunction()
closure()  // "Hello"
  1. 클로저 표현식
    이름이 없으며 주변 환경으로부터 값을 캡처할 수 있는 경량 문법으로 작성된 클로저
// 클로저 표현식
let incrementer: (Int) -> Int = { value in
    return value + 1
}

// 사용 예
let result = incrementer(5)  
print(result) // 6


클로저 표현식

클로저 표현식은 대부분 인자값으로 함수를 넘겨주어야 할 때 사용하지만
직접 실행할 수 있는 두가지 방법이 있다.

  1. 일급 함수로서의 특성을 활용해서 상수나 변수에 클로저 표현식을 할당한 다음 실행한다.
  2. 클로저 전체에 소괄호를 붙이고 뒤에 호출 연산자(소괄호)를 붙이면 클로저 표현식이 실행된다.
// 클로저 표현식
{ (매개변수) -> 반환 타입 in 
    실행할 구문
}

// 인자값과 반환값이 없는 클로저, 반환값은 없어도 명시적으로 표시해줘야한다.
{ () -> () in
    print("클로저가 실행됩니다")
}


// 매개변수가 있는 클로저 표현식
let c = { (s1: Int, s2: String) -> Void in 
    print("s1:\(s1), s2:\(s2)")
}

c(1, "closure") // 출력값: s1:1, s2:closure


let f = { () -> Void in
    print("클로저가 실행됩니다")
}

// 클로저 실행 첫번째 방법
f() // "클로저가 실행됩니다"

// 클로저 실행 두번째 방법
({ () -> Void in
    print("클로저가 실행됩니다")
})()


클로저 경량문법

클로저 표현식은 주로 인자값으로 사용되는 객체여서 간결성을 극대화하기 위해 여러 부분 생략이 가능하다.

var value = [1,9,5,7,3,2]

func order(s1: Int, s2: Int) -> Bool {
    if s1 > s2 {
        return true
    } else {
        return false
    }
}

// sort(by: ) 메서드를 이용해 정렬함
value.sort(by: order)
// [9,7,5,3,2,1]


// 1번 클로저 표현식
{(s1: Int, s2: Int) -> Bool in
    if s1 > s2 {
        return true
    } else {
        return false
    }
}

// 1번 클로저 표현식을 이용해 정렬함
value.sort(by: {
    (s1: Int, s2: Int) -> Bool in
    if s1 > s2 {
        return true
    } else {
        return false
    }
})
// [9,7,5,3,2,1]


// 2번 클로저 표현식: s1 > s2 구문은 Bool 값을 리턴하므로 코드가 간결하게 바뀜
{ (s1: Int, s2: Int) -> Bool in 
    return s1 > s2
}

// 2번 클로저 표현식을 이용해 정렬함
value.sort(by: { (s1: Int, s2: Int) -> Bool in return s1 > s2 })


// 3번 클로저 표현식: 타입 어노테이션 생략
{ s1, s2 in return s1 > s2 }

// 3번 클로저 표현식을 이용해 정렬함
value.sort(by: { s1, s2 in return s1 > s2 })


// 4번 클로저 표현식: 매개변수 생략
// $0, $1, $2...를 내부 상수로 사용 -> 입력받은 인자값 순서대로 사용
// in 키워드로 실행 구문과 선언 구문을 분리할 필요도 없으므로 in 키워드도 생략 (return도 생략 가능)
{ return $0 > $1 }


// 5번 클로저 표현식: return 생략
value.sort(by: { $0 > $1 })


// 6번 표현식: 연산자 함수를 사용해 더 간결하게 표현
value.sort(by: >)


trailing Closure

함수의 마지막 인자값이 클로저일때만 적용된다. (인자값이 하나여도 사용 가능하다.)

func calculate (a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}

var result: Int

// 후행클로저는 함수 소괄호 외부에 클로저 구현 가능
result = calculate(a: 10, b: 10) {(left: Int, right: Int) -> Int in return left + right
}

// 반환타입 생략 가능
result = calculate(a: 10, b: 10) {(left: Int, right: Int) in return left + right}

// 단축 인자이름 생략 가능, in 키워드로 실행 구문과 선언 구문을 분리할 필요도 없으므로 in 키워드도 생략
result = calculate(a: 10, b: 10) {return $0 + $1}

// 암시적 반환 표현으로 return 생략
result = calculate(a: 10, b: 10) {$0 + $1}


// 고차함수 사용해보기
let aArray = [1, 3, 5, 7, 9]

// 기존 메소드
func addOne(num: [Int]) -> [Int] {
    var answer = [Int]()
    for i in num {
        answer.append(i + 1)
    }
    return answer
}

print(aArray) // 출력값: [2, 4, 6, 8, 10]


// 1. 함수 대입
func addNum(num: Int) -> Int {
    return num + 1
}

let bArray = aArray.map(addNum)
print(bArray) // 출력값: [2, 4, 6, 8, 10]

// 2. 클로저 대입
let cArray = bArray.map({(n1: Int) -> Int in return n1 + 1})
print(cArray) // 출력값: [3, 5, 7, 9, 11]

// 3. 클로저 단축, 후행클로저
let dArray = cArray.map {$0 + 1}
print(dArray) // 출력값: [4, 6, 8, 10, 12]


// 1. 클로저 사용해서 문자열로 변환
print(cArray.map {(num1: Int) -> String in return "\(num1)"})
// 출력값: ["3", "5", "7", "9", "11"]

// 2. 클로저 단축
print(dArray.map {"\($0)"}) 
// 출력값: ["4", "6", "8", "10", "12"]


escaping Closure

@escaping: 인자값으로 전달된 클로저를 전달해 두었다가 나중에 다른 곳에서도 실행할 수 있도록 허용해주는 속성이다.
@escaping Closure: 클로저가 함수의 인자로 전달됐을 때, 함수의 실행이 종료된 후 실행되는 클로저이다.

// 함수를 파라미터로 전달받는 doSomething이라는 함수, 사용 가능
func doSomething(closure: () -> ()) {
    closure()
}

// nonEscapingClosure를 외부 변수에 담을 경우 
var nonEscapingClosure = {}

func doSomething(closure: () -> ()) {
    nonEscapingClosure = closure // Error: Assigning non-escaping parameter 'closure' to an @escaping closure
}

이렇게 작성하면 nonEscapingClosure = closure() 구문에서 에러가 발생한다!
인자값으로 전달된 클로저는 함수 안에서만 사용 가능하기 때문이다.

💡 이유는?
함수의 인자값으로 전달된 클로저는 기본적으로 탈출불가하기 때문에 함수 내에서 직접 실행을 위해서만 사용해야하고 함수 내부에서도 변수나 상수에 대입할 수 없다.

💡 왜?
변수나 상수에 대입되면 내부 함수를 통한 캡처 기능을 활용해 클로저가 함수 바깥으로 탈출할 수 있기 때문이다. (탈출이란 함수 내부 범위를 벗어나서 실행되는 것을 의미한다.)
동일한 의미에서 인자값으로 전달된 클로저는 내부 함수에서 사용할 수 있도록 허용할 경우, 이 역시 캡처를 통해 탈출될 수 있기 때문에 중첩된 내부 함수에서 사용할 수도 없다.

💡 하지만!
코드를 작성하다 보면 클로저를 변수나 상수에 대입하거나 중첩 함수 내부에서 사용해야 할 경우도 있다. 이때 @escaping 속성을 사용하면 된다. @escaping Closure를 사용할때는 명시적으로 self를 적어줘야한다.

var nonEscapingClosure = {}

func doSomething(closure: @escaping () -> ()) {
    nonEscapingClosure = closure
} 

이제 입력된 클로저는 변수나 상수에 할당 가능하며 중첩된 내부 함수에 사용할 수 있고 함수 바깥으로 전달할 수도 있다.탈출 가능한 클로저가 된 것이다.

💡 왜 인자값으로 전달되는 클로저의 기본 속성이 탈출불가하도록 설정된것일까?
이유는 해당 클로저가 탈출할 수 없다는 것은 컴파일러가 더 이상 메모리 관리상의 일들에 관여할 필요가 없다는 뜻이다. 또한, 이 클로저는 해당 함수가 끝나서 리턴되기 전에 호출될 것이 명확하기 때문에 클로저 내에서 self에 대한 약한 참조(weak reference)를 사용해야 할 필요가 없다.
그렇기때문에 컴파일러가 코드를 최적화하는 과정에서 성능이 향상된다.

0개의 댓글