[TIL]04.27

rbw·2022년 4월 27일
0

TIL

목록 보기
15/97
post-thumbnail

Memory, Value Type and Reference Type

값 형식은 스택에 저장되므로 메모리 삭제가 쉬운 편이다. 참조 형식은 힙에 저장이되고 스택에는 해당 힙의 메모리 주소가 저장이 된다. 값을 사용하지 않는다면 두 공간의 메모리 공간을 삭제해서 메모리 누수를 막는것이 중요하다.

Value Type, Reference Type

Swift의 값 타입으로는, 구조체, 열거형, 튜플이 있고 참조 타입으로는 클래스, 클로저 가 있다.

Automatic Reference Counting (ARC)

인스턴스는 하나 이상의 소유자가 있다면 메모리에 유지됩니다. 없다면 제거 됩니다. 이 시점을 파악하기 위해 소유자 수를 카운트하는데, 이를 참조 카운팅이라고 합니다.

클래스 인스턴스를 변수에 저장하면, 변수가 소유자가 됩니다. 이 시점에서 참조 카운팅은 1입니다.

인스턴스를 소유하기 위해서는 특별한 메시지를 전달 해야 합니다. 코드 레벨에서 보자면, 인스턴스가 제공하는 retain 메소드를 호출하는 것과 같습니다. 소유권을 포기할 떄도 메시지를 보냅니다 이는 release라고 합니다.

ARC의 세 가지 참조방식

Strong Reference

기본적으로 인스턴스와 소유자는 강한 참조로 연결됩니다. 소유권을 얻을때 카운팅이 1 증가하고 포기할 때 1 감소됩니다. 인스턴스는 소유자가 존재하는 동안은 메모리에서 제거 되지 않습니다.

class Person {
    var name = "min"

    deinit {
        print("person deinit")
    }
}

var person1: Person?
var person2: Person?
var person3: Person?

// 현재 참조 카운팅은 3이다.
person1 = Person()
person2 = person1
person3 = person1

// 전부 nil로 정의 하면 참조 카운팅이 0이 되므로 인스턴스는 메모리에서 사라진다.
// deinit 메소드 실행

Strong Reference Cycle

각각 다른 클래스의 멤버들이 서로를 소유하고 있고, 두 인스턴스가 nil이 된다면, 더 이상 호출은 불가능 하지만 강한 참조로 연결되어 있기 때문에 메모리에서 사라지지 않는 문제가 생긴다.

이를 강한 참조 사이클이라고 합니다.

이를 해결하는 방법은 약한 참조와 비소유 참조를 통해 해결합니다. 이 두 가지 방법은 참조 카운팅을 증가 시키지 않습니다

Weak Reference

이는, 인스턴스를 참조하지만 소유하지는 않습니다. 인스턴스를 참조하는 동안 대상 인스턴스는 언제든지 사라질 수 있습니다. 참조하고 있는 인스턴스가 해제되면 자동으로 nil로 초기화 합니다

// 문법은 다음과 같다
weak var name: Type?

Unowned Reference

이는 약한 참조와 동일한 방식이지만, 논 옵셔널 형식입니다. 속성을 논-옵셔널로 선언해야할 떄 사용합니다.

// 문법
unowned var name: Type

조금 헷갈리지만, 약한 참조는 참조하는 쪽이 없어지면 nil로 변하는 부분을 잘 기억하면 좋을듯 하다.

Unowned Optional Reference

swift5 이상에서 사용 가능한 Unowned Optional Reference에 대해서 알아보고, Weak Reference와의 차이를 알아 보겠습니다.

차이로는, 약한 참조는 값이 사라지면 nil로 업데이트 하지만 비소유 참조는 사라지더라도 속성을 업데이트 하지 않습니다. 직접 nil로 초기화 해야하므로, 가급적 사용하지 않는 편이 좋습니다.

Closure Capture List

클로저가 인스턴스를 캡처하고 있는 경우에도 강한참조가 일어나므로, 인스턴스가 정상적으로 해제되지 않는 문제가 있습니다. 이 해결 방법도 약한참조와 비소유 참조로 해결합니다.

class Car {
    var totalDrivingDistance = 0.0
    var totalUsedGas = 0.0

    // 클로저로 저장되어 있다
    // self로 클로저를 사용하면, self가 나타내는 인스턴스를 캡처한다
    //lazy var gasMileage: () -> Double = {
    //    retrun self.totalDrivingDistance / self.totalUsedGas
    //}
    
    // 강한참조 사이클을 해결하는 코드
    // 약한 참조는 옵셔널 형식이므로, self를 참조하려면 언래핑하거나, 옵셔널 체이닝을 사용해야한다.
    lazy var gasMileage: () -> Double = { [weak self] in
        guard let strongSelf = self else { return 0.0 }
        retrun strongSelf.totalDrivingDistance / strongSelf.totalUsedGas
    }
    func drive () {
        self.totalDrivingDistance = 1200.0
        self.totalUsedGas = 73.0
    }
    
    deinit {
        print("car deinit")
    }
}

var myCar: Cat? = Car()
// 이 시점에는 강한 참조가 일어나고 있지 않다. 따라서 정상적으로 해제된다
myCar?.drive()
//myCar = nil

// 강한 참조가 일어나고 nil을 저장해도 소멸자가 호출되지 않음
myCar?.gasMileage

값 형식의 클로저 캡처 리스트

값 형식의 클로저 캡처리스트의 문법을 살펴보겠습니다.

{ [list] (parameters) -> ReturnType in
    code
}

// 축약 버전
{ [list] in
    code
}

// 값 형식을 캡처하는 경우 
{ [valueName] in
    code
}
var a = 0
var b = 0
// a,b 두 변수를 캡처한다. 참조본을 전달함 
var c = { print(a, b) }
a = 1
b = 2
c() // 1 2 참조본이므로 변경된 값을 참조한다

// 클로저 캡처 리스트 문법으로 전달하면, a는 선언한 시점의 값을 전달한다 이는 복사본이다
c = { [a] in print(a, b)}
a = 3
c() // 1 2

참조 형식의 클로저 캡처 리스트

// 문법
{ [weak instanceName, unowned instanceName] in
    statements
}

Explicit Strong Capture (Swift 5.3+)

새로나온 기능으로, 클로저 내부에서 self에 접근하는 코드를 단순하게 만들어 준다.

class PersonObject {
    let name: String = "hi"
    let age: Int = 0

    // 이 경우 클로저에서는 캡처를 명시적으로 하기 위해 self를 사용해야 한다
    // 구조체에 경우 강한참조사이클이 일어나지 않는다면, self는 생략 가능 
    func doSomething() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            print(self.name, self.age)
        }
    }
}

// Explicit Strong Capture 사용 예시, 위의 예시는 Implicit Strong Capture이다.
class PersonObject {
    let name: String = "hi"
    let age: Int = 0

    func doSomething() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in
            print(name, age)
        }
    }
}

위의 경우도 weak나 unowned로 약한 참조로 만들 수 있다. weak를 사용했다면 꼭 언래핑을 하거나 옵셔널 체이닝으로 호출해야한다는걸 잊지말자.

Generic

형식에 의존하지 않는 범용 코드를 사용할 수 있게 해주는 제네릭에 대해서 알아보겠습니다.

// 제네릭 함수 선언 문법
// T는 타입 파라미터라고 부릅니다
func name<T>(parameters) -> Type {
    code
}

func swap<T>(lhs: inout T, rhs: inout T)  {
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

타입 파라미터는 실제 자료형으로 대체되는 플레이스 홀더입니다

Type Constraints

타입 파라미터에 올 수 있는 형식에 제약을 걸 수 있습니다.

// 문법
<TypeParameter: ClassName>
<TypeParameter: ProtocolName>

//앞에 선언한 swap 함수의 개선 버전
// Equatable을 선언하면 비교 연산이 가능한 형식들만 올 수 있기 때문에 에러가 사라짐
func swap<T: Equatable>(lhs: inout T, rhs: inout T)  {
    // 제약을 안걸면 에러가 일어난다
    if lhs == rhs { return } 

    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

// 대소문자를 구분없이 비교하고 싶을 때, 특수화를 통해서 특정 형식을 위한 함수를 구현한다 
// 이를 Specialization라고 한다.

func swap(lhs: inout String, rhs: inout String) {

    if lhs.caseInsensitiveCompare(rhs) == .orderedSame {
        return 
    }
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

String인 경우에 밑의 함수를 실행하므로, 다른 타입으로 swap 함수를 호출하여도 문제가 없다.

Generic Types

제네릭 타입은 형식 내부에서 사용하는 타입들을 타입 파라미터로 대체한다.

스위프트의 컬렉션은 전부 구조체로 구현 되어 있다.

// 문법
class Name<T> {
    code
}
struct Name<T> {
    code
}
enum Name<T> {
    code
}

struct Color<T> {
    var red: T
    var green: T
    var blue: T
}

let c = Color(red: 120, green: 230, blue: 45)
// 주의할 점은 이미 Int로 초기화가 되었는데, 새로 다른 형식으로 초기화 하는것은 불가능하다.

// 제네릭 타입의 확장 
// 제네릭 타입을 확장할 때는 타입 파라미터를 형식 이름 뒤에 추가하지 않습니다. 
// where 절 추가 가능, Int 형식에만 이 기능을 추가하고 싶다면 where T == Int 
extension Color where T: FixedWidthInteger { // Int를 채용함
    func getComponenets() -> [T] {
        return [red, green, blue]
    }
}

Associated Types

제네릭 프로토콜에서 필요한 연관형식에 대해서 알아보겠습니다.

연관형식도 타입파라미터와 마찬가지로 프로토콜 내에서 실제 형식으로 대체되는 플레이스 홀더입니다.

// 문법
associatedtype Name

protocol QueueCompatible {
    associatedtype Element
    func enqueue(value: Element)
    func dequeue() -> Element?
}

연관형식을 대체하는 실제 형식은 프로토콜을 채용한 형식에 따라서 결정합니다. 연관형식을 선언한 프로토콜을 채용한 형식은 typealias로 연관형식의 실제 형식을 선언한다. 하지만 실제로 내부 멤버로 알 수 있기 떄문에 생략하는 편이 많다.

// 문법
typealias AssociatedTypeName = Type 

class IntegerQueue: QueueCompatible {
    typealias Element = Int

    func enqueue(value: Int) {

    }
    func deque() -> Int? {
        return 0
    }
}

class DoubleQueue: QueueCompatible {
    func enqueue(value: Double) {

    }
    func deque() -> Double? {
        return 0 
    }
}
profile
hi there 👋

0개의 댓글