3월11일 TIL (Closure)

이승원·2024년 3월 11일
0

TIL

목록 보기
35/75
post-thumbnail

클로저 (Closure)

  • 클로저는 이름없는 함수, 즉 코드 블록을 말한다.
  • 클로저는 상수나 변수의 참조를 캡처(Capture)해 저장할 수 있다.
    - 클로저 주변에 있는 변수나 상수를 캡처하여 저장하고, 나중에 사용할 수 있도록 한다.
    • 이것은 클로저가 생성될 때 클로저가 참조하는 변수 또는 상수의 값에 대한 복사본을 유지하고 저장하는 매커니즘이다.
  • 값 (Value) 캡처 : 클로저가 변수나 상수의 값을 캡처한다. 이때 클로저 내부에서 캡처한 값이 변경되어도 원본값은 변경되지 않는다.
  • 참조 (Reference) 캡처 : 클로저가 변수나 상수의 참조를 캡처한다. 따라서 클로저 내에서 해당 변수나 상수를 변경하면 원본 값도 변경된다.
// 값 캡처
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var total = 0
    
    // 클로저를 반환합니다.
    let incrementer: () -> Int = {
        // total 변수를 캡처하여 저장합니다.
        total += amount
        return total
    }
    
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen()) // total = 10, 결과: 10
print(incrementByTen()) // total = 20, 결과: 20

// 참조 캡처
class SimpleClass {
    var value: Int = 10
}

func createClosure() -> (() -> Int) {
    let instance = SimpleClass()
    
    // 참조 캡처를 사용하여 SimpleClass의 인스턴스를 캡처합니다.
    let closure: () -> Int = {
        // 클로저가 참조하는 인스턴스의 속성을 업데이트합니다.
        instance.value *= 2
        return instance.value
    }
    
    return closure
}

// 클로저 생성
let myClosure = createClosure()

print(myClosure()) // 20
print(myClosure()) // 40

// 클로저 내부에서 참조된 인스턴스의 속성을 변경하였으므로 원본에도 영향을 줍니다.
  • 클로저의 대한 예시코드는 아래와 같다.
{ (Parameters) -> Return type in 
	// 구현코드 
}

let sum = { (_ numA : Int, _ numB : Int) -> Void in
 	print(numA + numB)
}
sum(1,2) // 3

func sumClousure (_ num1 : Int , _ num2 : Int, closure : (Int,Int) -> Void) {
    print("num1 : \(num1), num2: \(num2)")
    closure(num1,num2)
}

sumClousure(4, 4, closure: sum)
// "num1 : 4, num2: 4"
// " 8 " 
  • 클로저는 문법을 최적화하여 최소의 코드로도 같은 작업을 수행할 수 있다.
func performClosure(param: (String) -> Int) {
    param("Swift")
}

// 그대로 호출
performClosure(param: {(str : String) -> Int in
    return str.count // 5
})

// 1) 타입 추론 (Type Inference)
// 1-1 리턴 값 타입 추론
performClosure(param: {(str : String) in
    return str.count // 5
})
// 1-2 Paramter 타입 추론
performClosure(param: {str in
    return str.count // 5
})

// 2) 한줄인 경우 return 생략 (Implict Return)
performClosure(param: {str in
    str.count // 5
})

// 3) Argument 이름 축약 (Shorthand Argument)
performClosure(param: {
    $0.count // 5
})

// 4) 트레일링 클로저
performClosure() {
    $0.count // 5
}

탈출 클로저 (Escaping Closure)

  • 탈출 클로저를 공부하기 전에 코드의 순차적 실행과 비동기 실행 순서의 대해서 알아야 한다.
  • 일반적으로 코드는 위에서 부터 아래로 순차적으로 실행이 된다.
  • 다만 네트워킹을 공부하면 알 수 있듯이 서버에서 데이터를 요구할때는 비동기처리로 진행한다.
  • 특히나 iOS 처럼 인터페이스와 직접적 연결이 있는 경우, 안드로이드도 마찬가지로, 비동기 처리를 해줘야지만 정상적으로 데이터를 받아서 표시를 할 수 있다.
  • 이부분은 추후에 더 자세히 알아보자.
  • 비동기 실행의 관한 예시 코드는 아래와 같다.
func asynchronousExecutionExample() {
    print("Start")

    // 1. 비동기로 실행되는 작업
    DispatchQueue.global().async {
        for i in 1...3 {
            print("Async Task \(i)")
        }
    }

    // 2. 순차적으로 실행되는 작업
    print("Next Task")

    // 3. 또 다른 비동기 작업
    DispatchQueue.global().async {
        let result = 5 + 3
        print("Async Result: \(result)")
    }

    // 4. 끝 부분
    print("End")
}

asynchronousExecutionExample()
  • 위 코드는 순차적으로 진행이 되지만, DisPatchQueue.global().async 부분은 다른 Thread 에서 진행이 된다. 다른 Thread에서 작업이 완료 되면 다시 실행이 된다고 생각하면 된다.
  • 따라서 출력을 해보면 아래 처럼 나온다. 다만 DisPatchQueue.global().async의 순서는 변할 수 있지만, 이 경우에는 아마도 (100% 뇌피셜)이지만, 두번째 DisPatchQueue.global().async가 더 적은 계산이 필요하기 때문에 이런 결과가 나오는거 같다.
//start
//NextTask
//End
// Async Task 1
// Async Task 2
// Async Task 3
// Async Result : 8 

Escaping Closure

  • 어떤 함수의 내부에 존재하는 클로저를 외부 변수에 저장하는 경우
  • 이스케이핑 클로는 클로저가 메서드의 인자로 전달됐을 때, 메서드의 실행이 종료된 후 실행되는 클로저
  • 위 두 경우에는 타입 앞에 @escaping 키워드를 사용한다.
  • 예시 코드

// 1. 외부 변수에 클로저 저장

var defaultFunction : () -> () = {print("출력")}
// defaultFunction이라는 변수 안에 print("출력") 클로저를 대입

func escapingFunc(closure : @escaping () -> ()) {
	defaultFunction = closure
}
// escapingFunc 함수는 closure를 인자로 받고, 해당 인자를 함수 밖에 있는 defaultFunction에 저장
// 이런 경우 @escaping 키워드를 사용해야 함. 
// -----------------------------------------------------
// 2. 메서드의 실행이 종료된 후 실행되는 클로저

func asncEscaping(closure : @escaping (String) -> ()) {
    var name = "Sam"
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        closure(name)
    }
}

asncEscaping {
    print("name: \($0)")
}

// 위 경우에는 DispatchQueue 가 현재에서 3초 있다가 실행이 되게끔 코드를 작성해서,
// 일부러 asyncEscaping이 실행이 종료된 후를 만들어놓은 상황이다. 
// 따라서 이런 경우에도 클로저 앞에 @escaping 을 사용해야 한다. 
  • 만약에 @escaping 을 사용하는 클로저에서 self의 요소, 즉 클래스 내부의 요소의 접근할 때에는 self를 명시적으로 언급해야 한다.
  • 이 이유는 @escaping 를 사용한 클로저인 경우, 함수가 끝나도 함수 외부로 탈출하여, 클로저가 함수의 실행 흐름이 종료된 후에도 계속해서 호출이 된다는걸 의미하는데, 함수가 끝나버리면 클래스 내부의 요소가 정확히 어떤건지 못 찾기 때문에, 약한 참조를 통해 self를 명시적으로 언급해야하는 것이다.
  • 이부분 또한 뒤에 더 자세히 공부할 예정이다.
var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler : @escaping() -> Void) {
	completionHandlers.append(completionHandler)
}

func someFunctionWithoutEscapingClosure(completionHandler: () -> Void){
	completionHandler()
}

class SomeClass {
	var x = 100
    func doSomething() {
    	someFunctionWithEscapingClosure({self.x = 200})
        someFunctionWithoutEscapingClosure({x = 300})
    }
}

let instance = SomeClass()
instance.doSomething()
// CompletionHandlers 에다가 {self.x = 200} 을 넣었고
// x = 300 클로저를 실행
print(instance.x)
// 300

completionHandlers.first?()
print(instance.x)
// 200
profile
개발자 (진)

0개의 댓글