KWiOS
KWiOS0101
KWiOS
  • 분류 전체보기 (108)
    • Algorithm (41)
      • 이코테 (14)
      • 이코테 문제풀이 (21)
      • 프로그래머스 (6)
    • CS (1)
      • 모두를 위한 컴퓨터 과학(CS50 2019) (0)
    • iOS (15)
    • Swift (36)
      • Swift문법 (32)
      • 기타 (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 6

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
KWiOS

KWiOS0101

Swift/Swift문법

Swift문법 - Closure

2023. 1. 28. 16:11

Closure

일정 기능을 하는 코드를 하나의 블럭으로 모아놓은 것으로 보통 익명함수 또는 이름이 없는 함수라고 말한다.

클로저는 1급 객체이기 때문에 자료형을 가지고 있다.

 

클로저의 종류

이름이 있는 클로저 - 함수

이름이 없는 클로저 - 클로저

클로저는 이름이 있는 클로저, 이름이 없는 클로저 둘다 포함

 

1급 객체의 특징

1. 상수나 변수에 대입할 수 있다.

2. 함수의 파라미터 타입으로 전달할 수 있다.

3. 함수의 반환 타입으로 사용할 수 있다.

 

1. 상수나 변수에 대입 

let closure = {() -> () in 
	print("클로저 호출")
}

// 새로운 변수나 상수에 대입 가능 
let closure2 = closure

 

2. 함수의 파라미터 타입으로 전달

let closure = {() -> () in
    print("클로저 호출")
}

func testClosure(closure: () -> ()) {
    closure()
}

// 함수 대입
testClosure(closure: closure)

 

3. 함수의 반환 타입으로 사용 

let closure = {() -> () in
    print("클로저 호출")
}

func testClosure() -> () -> () {
    return closure
}

// 변수나 상수에 대입해 호출
let callClosure = testClosure()
callClosure()

 

클로저의 기본 문법 

{(파라미터) -> 리턴형 in 
	// 실행 구문
}

// 클로저는 헤드와 바디로 구분
(파라미터) -> 리턴형 // 이부분이 헤드
실행 구문 // 이부분이 바디
기본 자료형은 파라미터가 없고 리턴형이 없다.
() -> () <- 클로저의 기본 자료형

 

클로저 작성시 아규먼트 레이블을 사용하지 않고 파라미터 이름만 사용

파라미터와 아규먼트는 둘다 함수에 전달하거나 함수 내부에서 전달받는 값을 의미하지만 위치에 따라서 다르게 불린다.

파라미터 -> 매개변수로써 함수의 정의에서 사용
아규먼트 -> 전달인자로써 함수의 호출에서 사용

 

변수나 상수에서 클로저 작성 방법

// closure 아규먼트 레이블을 사용하게되면 error!! 
let closure = {(closure str: String) -> () in
    print(str)
}

// 이렇게 아규먼트 레이블을 사용하지 않고 파라미터만 사용
let closure = {(str: String) -> () in
    print(str)
}

// 호출시 파라미터 생략 
closure("안녕 클로저")

 

함수에서 클로저 작성 방법

func testFunc(closure: (String) -> ()) {
    // 호출시 파라미터 생략
		closure("안녕")
}

// closure 아규먼트 레이블을 사용하게되면 error!! 
testFunc(closure: {(closure str: String) -> () in
    print(str)
})

// 이렇게 아규먼트 레이블을 사용하지 않고 파라미터만 사용
testFunc(closure: {(str: String) -> () in
    print(str)
})

 

클로저를 호출하는 방법

1. 클로저를 직접 호출

({() -> () in
    print("클로저 호출")
})()

// 클로저를 ()로 감싸고 마지막에 ()로 호출

 

2. 상수나 변수에 대입해 호출

let closure = {() -> () in
    print("클로처 호출")
}

closure() // 호출

// ()호출구문 사용하지 않으면 호출 안됨

 

3. 함수에서 호출

let closure = {() -> () in
    print("클로처 호출")
}

func testFunc(closure: (String) -> ()) {
    closure("안녕")
}

// 1
testFunc(closure: {(str: String) -> () in
    print(str)
})

// 2
testFunc(closure: closure)

 


인라인 클로저

함수를 호출할 때 클로저가 파라미터의 값 형식으로 함수 호출구문 () 안에 작성되어 있는 것을 인라인 클로저라 한다.

func testClosure(closure: () -> ()) {
    closure()
}

// 파라미터에 클로저 자료형 자체를 작성 (인라인 클로저)
testClosure(closure: {() -> () in
    print("클로저 호출")
})

 

트레일링 클로저

함수의 가장 마지막에 클로저를 꼬리처럼 덧붙여서 작성하는 것을 트레일링 클로저라고 하며,

함수의 마지막 파라미터가 클로저일 경우 트레일링 클로저로 작성 가능하다.

func testClosure(closure: () -> ()) {
    closure()
}

// 함수 호출구문 내부가 아닌 바깥쪽에 작성 (트레일링 클로저)
testClosure() { () -> () in
    print("클로저 실행")
}

 

인라인 클로저와 트레일링 클로저 비교

func testClosure(closure: (String) -> ()) {
    closure("클로저 호출")
}

// 인라인 클로저 
testClosure(closure: {(str: String) -> () in
    print(str)
})

// 트레일링 클로저 
testClosure() { (str: String) -> () in
    print(str)
}

 

함수의 파라미터가 여러개일 경우 트레일링 클로저 작성 방법

func testClosure(text: String, closure: (String) -> ()) {
    closure("클로저 호출")
}

// 함수 마지막 파라미터가 클로저
testClosure(text: "안녕", closure: {(str: String) -> () in
    print(str)
})

// 함수 마지막 파라미터를 트레일링 클로저로 작성
testClosure(text: "안녕") { (str: String) -> () in
    print(str)
}

 

클로저의 경량 문법

문법을 최적화하여 클로저를 단순하게 사용할 수 있게 해주는 문법으로 총 5단계를 거친다.

 

예시 함수 

func testFunc(closure: (Int,Int) -> Int) {
    closure(1,2)
}

testFunc(closure: {(first: Int, second: Int) -> Int in
    return first + second
})

1. 파라미터의 형식과 리턴 형식을 생략한다.

testFunc(closure: {(first, second) in
    return first + second
})

2. 파라미터 이름을 생략하고 in 키워드도 생략 후 숏 핸드 아규먼트 네임으로 대체한다.

testFunc(closure: {
    return $0 + $1
})

3. 클로저에 포함된 코드가 단일 리턴문이라면 리턴 키워드를 생략한다.

testFunc(closure: {
    $0 + $1
})

4. 함수의 마지막 파라미터가 클로저라면 트레일링 클로저로 작성한다. (아규먼트 레이블이 남아 있다면 생략)

testFunc() {
    $0 + $1
}

5. 클로저를 제외한 파라미터가 없다면 호출구문을 생략한다.

testFunc {
    $0 + $1
}

 


AutoClosure

파라미터로 전달된 일반 구문 & 함수를 클로저로 랩핑하여 사용하는 클로저이다.

오토 클로저를 사용하기 위해서는 함수의 파라미터 타입 앞에 @autoclosure 키워드를 사용해야 한다.

 

오토클로저의 특징으로는 원래 일반 구문은 작성하자마자 동작하게 되지만 오토클로저로 작성하게되면 함수 내에서 클로저가 실행되기 전까지 실행되지 않으므로 지연된 실행이라는 특징을 가진다.

 

클로저가 아닌 일반 구문 전달 가능

func testFunc(closure: @autoclosure () -> (Bool)) {
    closure() // true
}

testFunc(closure: 100 < 200)

 

오토클로저를 사용할때 파라미터 타입이 있을 경우 Error!!

파라미터는 반드시 없어야 한다.

func testFunc(closure: @autoclosure () -> ()) {
    // 실행 구문
}

// 만약 아래와 같이 타입을 지정해주었을 경우 error!!
func testFunc(closure: @autoclosure (String) -> ()) {
    
}

testFunc(closure: <<error type>>)

// 리턴형은 상관없음 
func testFunc(closure: @autoclosure () -> String) {
    
}

testFunc(closure: <<String>>)

 


Non-escaping Closure

함수가 종료되기 전에 호출되어야 하는 클로저를 의미한다.

 

아래의 경우에 사용하면 에러가 발생한다.

1. 함수 내부에서 직접 실행되기 위해서만 사용되므로 변수나 상수에 대입할 수 없다.

2. 중첩 함수에서 클로저를 사용할 경우 중첩 함수가 리턴되지 않는다.

3. 함수의 실행 흐름을 탈출하지 못하므로 함수 종료 전에 무조건 실행되어야 한다.

 

1. 함수 내부에서 직접 실행되기 위해서만 사용되므로 변수나 상수에 대입 X

func testFunc(closure: () -> ()) {
    let copyClosure: () -> () = closure  // error!!
}

2. 중첩 함수에서 클로저를 사용할 경우 중첩 함수 리턴 X

func testFunc(closure: () -> ()) -> () -> () {

    func testFunc2() {
        closure()
    }
    
    return testFunc2 // error!!
}

3. 함수의 실행 흐름을 탈출하지 못하므로 함수 종료 직전에 무조건 실행되어야 함

func testFunc(closure: () -> ()) {

    print("start")

    DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // error!!
        closure()
    }

    print("end")
}
위 에러들은 모두 클로저의 주변 값 캡쳐 방식에 의해 발생!! 위에 경우를 모두 에러없이 사용하고 싶다면 escaping Closure를 사용해야 한다.

 


escaping Closure

escaping Closure클로저는 함수의 실행을 벗어나서 함수가 끝난 후에도 실행되는 클로저를 의미한다.

 

Non-escaping Closure에서 에러가 발생하는 경우들에 사용

1. 변수나 상수에 대입할 수 있음

func testFunc(closure: @escaping () -> ()) {

    let closure2: () -> () = closure
}

2. 중첩 함수에서 클로저를 사용할 경우 중첩 함수를 리턴할 수 있음

func testFunc(closure: @escaping () -> ()) -> () -> () {

    func testFunc2() {
        closure()
    }
    
    return testFunc2
}

3. 함수의 실행 흐름을 탈출한 후에도 실행할 수 있음

func testFunc(closure: @escaping () -> ()) {

    print("start")

    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
        closure()
    }

    print("end")
}

 


클로저의 값 캡쳐

클로저가 함수 내부의 값을 사용할 경우 클로저는 함수 내부에 있는 값을 캡쳐하여 사용한다.

func testFunc(closure: @escaping () -> ()) {

    print("start")

    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
        closure()
    }

    print("end")
}
위 코드에서 클로저는 num의 값을 내부적으로 사용하고 있는데 이때 num의 값을 캡쳐했다 라고 볼 수 있다. 

클로저는 값을 캡쳐할 때 값 타입, 참조 타입에 관계없이 참조 캡쳐한다.

값 타입 - struct, enum, tuple, Swift의 기본 자료형 타입(Int, Float, Double, Bool, String, Array, Dictionary, Set)

참조 타입 - class, function, closure

 

클로저 실행 전에 캡쳐한 변수 값을 변경 했을 경우

func testFunc() {
    
    var num = 100
    print(address(of: &num), num) // 0x600002c549f0 100
    
    let closure = { print(address(of: &num), num) } // 0x600002c549f0 200
    
    num = 200

    print(address(of: &num), num) // 0x600002c549f0 200
    closure()
}

testFunc()
메모리 주소를 확인해보면 클로저 내부에서 값을 캡쳐할 경우 참조 캡쳐하기 때문에 메모리 주소가 같은것을 볼 수 있다. 따라서 클로저를 실행하기 전에 num 값을 변경하면 클로저 내부 num값도 변경된다.

 

클로저 내부에서 캡쳐한 값을 변경했을 경우 

func testFunc() {
    
    var num = 100
    print(address(of: &num), num) // 0x6000022450f0 100
    
    let closure = {
        num += 50
        print(address(of: &num), num) // 0x6000022450f0 150
    }
    
    closure()
    print(address(of: &num), num) // 0x6000022450f0 150
}

testFunc()
클로저 내부에서 num 값을 변경하면 클로저 외부의 num 값도 변경된다.

 


클로저의 캡쳐 리스트 

클로저의 강한 참조 순환 문제를 해결하기 위해 사용하며, 클로저 내부에서 외부 값을 참조할 때 참조하는 값이 변경되면 클로저 내부에서도 참조하는 값도 바뀌므로 이를 방지하고자 할 때 사용한다.

in 키워드 앞에 []안에 캡쳐할 멤버를 넣어주면 된다.

func testFunc() {
    
    var num = 100
    print("1 =",num) // 100
    
    let closure = { [num] in
        print("3 =",num) // 100
    }
    
    num = 200
    print("2 =",num) // 200
    closure()
}

testFunc()
캡처 리스트를 사용해서 값을 캡쳐하게 되면 클로저를 작성할때의 값을 캡쳐하기 때문에 클로저 호출 전에 num값을 변경하더라도 클로저 내부의 num의 값은 변경되지 않는다.

 

주의해야할점!!

캡처리스트를 사용해서 캡쳐할 경우 상수로 캡쳐되기 때문에 아래 코드와 같이 값을 변경할 수 없다.

그리고 참조 타입을 캡쳐리스트로 캡쳐했을 때 값 타입으로 캡쳐되지 않고 무조건 참조 타입으로 캡쳐된다.

// 테스트 클래스 생성
class TestClass {
		// num 변수 100으로 초기화
    var num: Int = 100
}

// TestClass 인스턴스 생성
let testClass = TestClass()

// 캡처 리스트로 캡쳐 
let closure = { [testClass] in
    print(testClass.num)
}

// num 값 변경 
testClass.num = 200
closure() // 200

 

최종적으로 정리하면

값 타입 -> 클로저가 생성될 때 캡쳐

참조 타입 -> 클로저가 호출될 때 캡쳐

'Swift > Swift문법' 카테고리의 다른 글

Swift문법 - String and Characters  (0) 2023.02.12
Swift문법 - Tuples  (0) 2023.02.05
Swift문법 - Functions  (0) 2023.01.22
Swift문법 - Optional  (1) 2023.01.15
Swift문법 - Control Transfer Statements, Labeled Statements  (0) 2023.01.08
    'Swift/Swift문법' 카테고리의 다른 글
    • Swift문법 - String and Characters
    • Swift문법 - Tuples
    • Swift문법 - Functions
    • Swift문법 - Optional
    KWiOS
    KWiOS

    티스토리툴바