[TIL]07.11

rbw·2022년 7월 11일
0

TIL

목록 보기
34/98
post-thumbnail

캡처리스트 Capture list

[weak self] 같은 캡처 리스트에 대해 잘 몰랐던 부분을 작성해보려고 합니다

var a = 0
var b = 0
let closure = { [a] in
    print(a, b)
}

a = 10
b = 10
print(closure()) // 0, 10

위 코드처럼 a를 캡처 시, 이 캡처의 내부범위는 클로저가 생성될 때, 외부 범위의 값으로 초기화를 하기 때문에 a의 현재 값은 0 이므로, 나중에 값이 바뀌어도 변하지 않는다.

하지만 캡처된 변수 유형에 참조의 의미가 있다면 동일한 객체를 참조하므로, 해당 객체에 따라 값을 참조한다. (변할 수 있다는 의미!)

또한 캡처 리스트 내부에서 표현식이 사용 가능합니다.

myFunction { weak parent = self.parent in 
    print(parent!.title) 
}

위 코드 처럼 클로저가 생성될 때 표현식이 평가되고, 지정된 강도(weak, unowned)로 값이 캡처 됩니다.

  • 추가로 clousre 내에서 self의 멤버를 참조할 때 마다, self 키워드를 작성하라고 swift는 요구합니다. 이는 실수로 self를 캡처할 수 있따는 것을 기억하는데 도움을 줍니다.

Closure의 매개변수 목록과, 반환 유형이 제공되는 경우, 캡처 리스트를 앞에 둔다.

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate]
    (index: Int, stringToProcess: String) -> String in
    // body
}

만약 컨텍스트가, 매개변수 유형과 반환 유형을 유추 가능하다면 생략하고, 그 후에 캡처리스트를 앞에 배치하고 in 키워드를 작성합니다.

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate] in
    // body
}

unowned weak 캡처리스트를 사용하여 강한 참조 해결

클래스 인스턴스의 파라미터에 클로저를 할당하고, 해당 클로저의 본문이 인스턴스를 캡처하는 경우에도 강한 참조 주기가 발생하는 것을 볼 수 있습니다. 이는 클로저 또한 참조 유형이고, 클로저의 본문이 인스턴스 속성에 액세스 하기 때문에 발생이 가능합니다. (메서드 호출도 그 이유가 된다.)

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
}

위 코드에서 HTMLElement 인스턴스와 asHTML값으로 사용된 클로저 간에 강한 참조 사이클을 생성합니다.

이의 해결은 캡처리스트를 활용하는 것입니다.

lazy var asHTML: () -> String = {
    [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

다음으로, 미소유 참조와 약한 참조를 살펴보겠습니다.

먼저 unowned 를 살펴보면,

  • 클로저와 그 클로저가 캡처할 인스턴스가 항상 서로를 참조시에 unowned를 작성한다.
  • 항상 동시에 할당이 해제됩니다.

다음 weak를 살펴보면,

  • 캡처된 참조가 미래의 어느 시점에서 nil이 될 가능성이 있는 경우 weak를 사용한다.
  • 약한 참조는 항상 optional type이고, 참조하는 인스턴스가 할당 해제시 자동으로 nil이 됩니다. (만약, 참조가 절대로 nil이 안된다면 unowned로 캡처해야 합니다.)

KeyPath

잘 모르고 쓰던 KeyPath에 대해서 알아보겠슴당

이는 값에 대한 참조가 아닌 프로퍼티에 대한 참조 입니다. -> 어떤 타입의 값에 대한 Path !

KeyPath의 종류

  • AnyKeyPath: 타입이 지워진 KeyPath
  • PartialKeyPath: 부분적으로 타입이 지워진 KeyPath
  • KeyPath: 읽기전용
  • WritableKeyPath: 읽기 및 쓰기 가능
  • RefrenceWritableKeyPath: 클래스의 인스턴스에 사용 가능, 변경 가능한 모든 프로퍼티에 대한 read/write 제공

KeyPath의 표현식

표현식은 \type name.path 와 같고, swift에서는 타입유추가 가능하다면 생략이 가능하다.

let str: KeyPath<String, String> = \.description
// \String.description과 동일

KeyPath의 사용 예시

struct Cluster {
    var name: String
}

struct Cadet {
    let cluster1: cluster
    let cluster2: cluster
}

여기서 cadet 내부 프로퍼티를 구하는 함수를 만든다면 다음과 같습니다.

func getCluster1(cadet: Cadet) -> Cluster {
    return cadet.cluster1
}

func getCluster2(cadet: Cadet) -> Cluster {
    return cadet.cluster2
}

하지만, KeyPath를 사용한다면 다음과 같이 작성이 가능합니다.

func getCluster(cadet: Cadet, keyPath: KeyPath<Cadet, Cluster>) -> Cluster {
    retrun cadet[KeyPath: keyPath]
}

// 사용 예시
print(getCluster(cadet: cadet, keyPath: \.cluster).name)

이런식으로 받은 KeyPath 값에 따라서, Cadet 내부 프로퍼티에 접근이 가능합니다.

고차함수와의 KeyPath 사용 예시

함수의 형태로의 KeyPath는 \Root.Value(Root) -> Value 로 나타낼 수 있슴다

map 함수의 경우 (Element) -> Value의 매개변수를 받는걸 허용합니다. 이걸 바탕으로 KeyPath를 전달이 가능합니다.

let names = users.map { $0.name }

// 위의 map과 동일하다.
let nameUsingKeyPath = users.map(\.name)

KeyPath에서의 Path

이는 프로퍼티의 이름, 서브스크립트, 옵셔널 체이닝 메서드, 강제 언래핑 표현식이 올 수 있다

\User.name
\[Int][0]
\User.address?.street
\User.address!.street

기본적으로 객체와 구조체의 인스턴스에 대한 값을 참조시에 주로 사용한다. 계산 속성도 참조 가능하며 필요한 만큼 반복 호출이 가능하다.

KeyPath 타입의 추론 방식

이는 프로퍼티와 서브스크립트, 그리고 Root 타입에서 유추한다.

  • 만약 프로퍼티 or 서브스크립트가 읽기 전용(let, get) 이라면 KeyPath로 추론된다.
  • 변경 가능한(var get/set)의 경우
    • 값 타입 체계(enum, struct)라면, WritableKeyPath 추론된다.
    • 참조 타입 체계(class)라면, ReferenceWritableKeyPath로 추론된다.

값 접근

모든 유형에서 사용 가능한 subscript(keyPath:)를 사용하여 키패스를 통해 값으로 액세스 가능하다

let role = user[keyPath: \User.role]

KeyPath의 Identity

\.self 문법을 사용하여서 프로퍼티 대신 전체 인스턴스를 참조 할 수 있다.

이 KeyPath의 결과는 WritableKeyPath이므로, 이를 사용해서 한 번에 모든 데이터에 액세스 하고 변경이 가능하다.

var foo = "Foo"
let stringIdentity = \String.self
//  WritableKeyPath<String, String>

foo[keyPath: stringIdentity] = "Bar"
print(foo) // Bar

struct User {
  let name: String
}
var user = User(name: "John")
let userIdentity = \User.self
// WritableKeyPath<User, User>

user[keyPath: userIdentity] = User(name: "Doe")
print(user) // User(name: "Doe")

Protocol 대안으로의 KeyPath 사용 예시

이는 SwiftUI에서 ForEach를 사용할 때 주로 활용하는 방식입니다. SwiftUI에서의 Identifiable의 요구 조건은 Hashable을 만족하는 ID 변수 입니다. 이를 KeyPath를 통해 대체 초기화가 가능합니다.

import SwiftUI

struct User {
    let name: String
}
struct ContentView: View {
    
    let users: [User] = [
        User(name: "John"),
        User(name: "Alice"),
        User(name: "Bob"),
    ]

    var body: some View {
        ScrollView {
            ForEach(users, id: \.name) { user in
                Text(user.name)
            }
        }
    }
}

User 구조체를 고유하게 식별 가능하게 하는 속성에 대한 경로를 지정하여서 위 처럼 사용이 가능합니다.

\.name\User.name 과 같습니당


참조

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

https://sarunw.com/posts/what-is-keypath-in-swift/#conclusion

profile
hi there 👋

0개의 댓글