ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS/Swift] Struct에 대해 알아보기
    야매 iOS 2023. 4. 19. 02:11

    Struct

    Struct를 Codable과 함께 모델을 구성하는 데 주로 사용하는 편이라 Struct에 대해 시간내서 알아본 기억이 없는 것 같다.

    Struct에 대해 기본적으로 알고 있는 것을 제외하고 이번에 새로 알게된 또는 알고 있었지만 제대로 어떻게 동작하는지 처음 안 것을 기록하려 한다.

    Immutable

    Immutable은 한 번 초기화 된 이후 해당 타입의 값이 변경되지 않음을 의미한다.

    Struct의 가장 큰 특징이자 좀 더 넓게는 Value type의 특징이다.

    // Origin
    struct Origin {
        var x = 0 
        var y = 0
    
        func change(x: Int?, y: Int?) {
    		if let x {
                self.x = x
            }
    
            if let y {
                self.y = y
            }
        }
    }
    
    var origin = Origin
    origin.change(x: 4, y: 4)

    origin.change(x: 4, y: 4)을 호출하면 의도한대로 x와 y값이 변경되지 않는다.

    사실 change 메서드를 구현한 순간 Xcode에서 Cannot assign to value: 'self' is immutable 에러를 뱉을 것이다.

    위에서 언급했듯이 struct는 value 타입이고 value 타입은 immutable하기 때문에 한번 초기화 된 이후 값의 변경을 막는다.

    하지만 struct는 mutating 키워드를 통해 내부 값을 변경할 수 있는 수단을 마련했다.

    struct의 mutating

    mutating 키워드를 method 이름 앞에 위치시키면 struct의 내부값 또는 struct 전체를 변경할 수 있다.

    mutating이 어떻게 동작하는지 이해를 조금 쉽게 알려면 inout에 대한 이해가 필요하다. mutation은 사실상 inout self이기 때문에 inout을 알면 도움이 된다

    inout

    보통 함수를 사용해 새로운 값을 만들고 변수의 새로운 값 할당은 아래의 과정을 거친다.

    1. 함수 내부에서 어떤 로직을 실행함
    2. 새로운 값을 반환함
    3. 반환한 값을 기존 변수에 할당함

    하지만 inout이 적용된 파라미터에 변수를 전달하면 아래의 과정을 거친다

    1. 함수 내부에서 어떤 동작을 함
    2. inout 파라미터에 바로 값을 할당한다
    func change(numParam: inout Int, with newNum: Int) {
            numParam = newNum
        print("substitude num with \\(numParam)")
    }
    
    var num = 0
    change(num: &num, with: 2)

    → Direct로 inout 파라미터에 값을 할당할 수 있다.

    위의 예시로 들자면 change 함수 내부에서 새로 받은 newNum을 inout 매개변수로 전달 받은 numParam에 바로 할당하는 것을 볼 수 있다.

    하지만 함수 내부에서 numParam에 새로운 값을 할당했을 때 num에 바로 적용되지 않는다.

    change 함수가 끝나고 그 scope를 벗어나게 됐을 때 num에 새로운 값이 할당된다.

    inout parameter는 함수가 호출 될때 그 복사본을 생성하고 함수가 종료되었을 때 그 복사본을 적용한다.

    func change(numParam: inout Int, with newNum: Int) {
        numParam = newNum
        print("substitude num with \\(numParam")
        Thread.sleep(forTimeInterval: 3)
    }
    
    var num = 0 {
        willSet {
            print("in willSet num = \\(num) is changed to \\(newValue)")
        }
    }
    change(num: &num, with: 1)
    /*
    substitude num with 1
    ...3초 후
    in willSet num = 0 is changed to 1
    */

    num에 바로 적용이 된다면 in willSet num = 0 is changed to 1이 출력된 후 3초라는 시간이 흘렀을 것이다.

    mutating

    mutating은 inout과 비슷하게 동작한다.

    mutating method를 호출한다면, mutating method가 반환된 이후 변경사항이 적용된다.

    struct Origin {
        var x = 0 
        var y = 0 
    
        mutating func change(x: Int?, y: Int?) {
            if let x {
                self.x = x
            }
    
            if let y {
                self.y = y
            }
    
            print("changed origin with \\(self)")
            Thread.sleep(forTimeInterval: 3)
        }
    
        mutating func substitude(x: Int, y: Int) {
            self = Origin(x: x, y: y)
            print("substitude with \\(self)")
            Thread.sleep(forTimeInterval: 3)
        }
    }
    
    var origin = Origin() {
        willSet {
            print("in willSet origin = \\(origin) is changed to \\(newValue)")
        }
    }
    
    origin.change(x: 2, y: nil)
    origin.change(x: nil, y: 2)
    
    origin.substitude(x: 4, y: 4)
    
    /* Output
    changed origin with Origin(x: 2, y: 0)
    ...3초 후
    in willSet origin = Origin(x: 0, y: 0) is changed to Origin(x: 2, y: 0)
    changed origin with Origin(x: 2, y: 2)
    ...3초 후
    in willSet origin = Origin(x: 2, y: 0) is changed to Origin(x: 2, y: 2)
    substitude with Origin(x: 4, y: 4)
    ...3초 후
    in willSet origin = Origin(x: 2, y: 2) is changed to Origin(x: 4, y: 4)
    */

    위의 출력을 본다면 inout과 같이 메서드 scope를 벗어나야 적용된다는 것을 알 수 있다.

    그리고 여기서 또 눈여겨 봐야되는 것은 mutating 키워드를 사용해 struct 내부의 프로퍼터를 변경시키거나 struct 자체를 변경시킬 때 struct 값을 재사용하는 것이 아닌 새로운 값을 생성한 후 할당한다는 점이다.

    @escaping closure와 mutating

    Struct의 mutating 메서드 내부에 있는 비동기 closure 내부에서 self를 전달한다면 비동기적으로 struct의 값이 바뀔수도 있겠다는 생각이 들었다.

    mutating func substitude(x: Int, y: Int) {
            DispatchQueue.main.async {
                    self.x = x
                    self.y = y
            }
    }

    위의 코드는 정말 예시 그냥 예시다.

    비동기 closure 내부에서 self에 접근한 것을 볼 수 있다.

    하지만 이렇게 작성하는 순간 Xcode에서 Escaping closure captures mutating 'self' parameter에러를 내뱉는 것을 알 수 있다.

    이에 대해 에러를 왜 뱉는지에 대한 히스토리는 아래의 링크를 타고 들어가면 알 수 있다.

    https://github.com/apple/swift-evolution/blob/main/proposals/0035-limit-inout-capture.md

     

    GitHub - apple/swift-evolution: This maintains proposals for changes and user-visible enhancements to the Swift Programming Lang

    This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - GitHub - apple/swift-evolution: This maintains proposals for changes and user-visible enhance...

    github.com

    Xcode에서 이를 금지시킨 백그라운드를 이해하기 위해선 closure 특히 클로저에서 변수 또는 상수가 어떻게 캡처되는지 알아야 한다.

    Closure and Capture

    closure는 미리 정의되어 있는 주변 context로부터 상수와 변수를 capture할 수 있다.

    • capture는 스냅샷을 찍는다는 느낌보단 계속 그 값을 잡고 있다는 의미로 해석하면 된다.

    상수와 변수가 정의된 scope를 벗어나도 capture를 해서 closure의 body는 상수 또는 변수 값에 접근하거나 값을 변경시킬 수 있다.

    최적화를 위해 closure에 의해 값이 변경되지 않는다면 그 값을 capture 하지 않고 복사본을 저장한다.

    func makeIncrementer(forIncrement amount: Int) -> () -> Int {
        var runningTotal = 0
        func incrementer() -> Int {
            runningTotal += amount
            return runningTotal
        }
        return incrementer
    }
    let incrementer = makeIncrementer(forIncrement: 10)
    incrementer()
    // 10
    incrementer()
    // 20
    incrementer()
    // 30

    위 코드의 예시를 보면 runningTotal이 capture 되어서 incrementer를 호출할 때마다 값이 10씩 증가하는 것을 볼 수 있다.

    Background

    그럼 이제 @escaping closure 내부에서 inout 파라미터를 캡처하는 것이 왜 금지되었는지에 대한 백그라운드를 설명하고자 한다.

    • 내가 이해한 것을 바탕으로 하고 현재 이를 실행시킬 수 없기 때문에 틀릴 수 있다.

    TL;DR

    @escaping closure에서 암시적으로(implicit) inout 파라미터를 캡처할 수 없는 이유는 예상치 못하는 동작이 발생하기 때문이다.

    Explanation

    이전에는 @escaping closure에서 inout parameter를 암시적으로 캡처할 수 있었다.

    하지만 이를 캡쳐했을 때의 한계점은 실제 inout parameter를 캡처할 수 있는 시간은 정해져 있다는 것이다.

    • inout parameter가 살아있는 시간
    • 즉, inout parameter를 받아온 메서드/함수가 끝나지 않았을 때를 의미하는 것 같다.

    inout parameter를 캡처할 수 있는 시간이 끝나면 closure는 inout paramter의 값을 복사하고 그 복사본을 캡처한다.

    정리를 하자면

    • inout parameter가 original scope를 벗어나지 않는다면 실제 inout paramter를 캡처해 예상된 동작을 한다.
    • original scope를 벗어난다면 inout parameter의 복사본을 캡처해 예상하지 못한 동작을 한다.
    func captureAndCall(inout x: Int) {
      let closure = { x += 1 }
      closure()
    }
    var x = 22
    captureAndCall(&x)
    print(x) // => 23

    위의 코드를 보면 closure는 inout parameter가 활성화되어 있을 때(살아있을 때) 호출된다.

    그러므로 x의 값이 23으로 변경된 것을 볼 수 있다.

    func captureAndEscape(inout x: Int) -> () -> Void {
      let closure = { x += 1 }
      return closure
    }
    
    var x = 22
    let closure = captureAndEscape(&x)
    print(x) // => 22
    closure()
    print("still \\(x)") // => still 22

    하지만 위의 코드는 클로저를 반환하고 반환한 클로저를 호출한다.

    클로저를 생성한 함수는 이미 종료된 이후이므로 inout parameter는 활성화되어 있지 않다. (살아있지 않다)

    그러므로 클로저는 실제 inout parameter값을 캡처하지 않고 그의 복사본을 캡처한 상태다.

    그러므로 closure를 호출해도 실제 x가 변경이 되지 않는 것을 알 수 있다.

     

     

    좀 더 실험해보고 싶지만 실행 자체가 막혀있으므로 못하는 것이 조금 아쉽다.

     

    출처

    https://forums.swift.org/t/pitch-only-allow-capture-of-inout-parameters-in-noescape-closures/1223

     

    Pitch: only allow capture of inout parameters in @noescape closures

    I think the time has come for us to limit the implicit capture of 'inout' parameters to @noescape closures. In the early days before @noescape, we designed a semantics for capturing "inout" parameters that attempted to balance the constraints on inout with

    forums.swift.org

    https://github.com/apple/swift-evolution/blob/main/proposals/0035-limit-inout-capture.md

     

    GitHub - apple/swift-evolution: This maintains proposals for changes and user-visible enhancements to the Swift Programming Lang

    This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - GitHub - apple/swift-evolution: This maintains proposals for changes and user-visible enhance...

    github.com

    https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Capturing-Values

     

    Documentation

     

    docs.swift.org

    '야매 iOS' 카테고리의 다른 글

    [iOS/Swift] GCD Queue  (0) 2023.08.06
    [iOS/Swift] 동시성 프로그래밍  (0) 2023.08.06
    [iOS/Swift] Combine 알아보기 ix  (0) 2023.03.26
    [iOS/Swift] Combine 알아보기 viii  (0) 2023.03.26
    [iOS/Swift] Combine 알아보기 vii  (0) 2023.03.26
Designed by Tistory.