본문 바로가기

TIL

Opaque And Boxe Type (2)

Boxed Protocol Types

there exists a type T such that T conforms to the protocol

→ Boxed Protocol Types은 **existential Type(존재 타입)**이라고도 불리며, 프로토콜 이름 앞에 any를 붙임

✏️ 예시

protocol Shape {
    func draw() -> String
}

struct VerticalShapes: Shape {
		// Boxed Protocol Type인 `Shape` 요소의 배열 타입
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\\n\\n")
    }
}

let largeTriangle = Triangle(size: 5)
let largeSquare = Square(size: 5)
let vertical = VerticalShapes(shapes: [largeTriangle, largeSquare])
print(vertical.draw())

1️⃣ 배열의 각 요소로 **Shape 프로토콜을 준수하는 다른 타입이 올 수 있음**

2️⃣ 이때, 런타임 유연성을 제공하기 위해서 Swift에서 필요에 따라 간접 참조를 추가

이 간접 참조를 Box라고 칭하며 **성능 비용(Performance Cost)**을 갖고 있음

3️⃣ VerticalShapes 타입에는 Shape 프로토콜에 의해 요구되는 메서드, 프로퍼티, 서브 스크립트를 사용할 수 있음

하지만 Shape에 의해 요구되지 않은 Triangle의 size 프로퍼티와 같은 프로퍼티 혹은 메서드는 접근 불가

활용가능한 세가지 타입

1️⃣ Generic을 사용하여 특정 Shape 타입의 요소 배열을 만들고, 특정 타입의 식별자가 배열로 상호작용하도록 노출

struct VerticalShapes<S: Shape> {
    var shapes: [S]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\\n\\n")
    }
}

2️⃣ Opaque Type 키워드인 some을 사용하여 특정 Shape 타입의 요소로 배열을 만들고, 특정 타입 식별자는 숨김

struct VerticalShapes: Shape {
    var shapes: [some Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\\n\\n")
    }
}

3️⃣ Boxed Protocol Type 키워드인 any를 사용하여 다른 타입의 요소를 저장할 수 있는 배열을 만들고, 특정 타입 식별자는 숨김

struct VerticalShapes: Shape {
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\\n\\n")
    }
}

⭐️ 이때 Boxed Protocol Type은 유일하게 다른 종류의 Shape을 혼합할 수 있는 접근 방식

⭐️ 특히 Boxed Protocol Type의 타입을 알고 있는 경우 as를 통한 타입 캐스팅이 가능함

if let downcastTriangle = vertical.shapes[0] as? Triangle {
    print(downcastTriangle.size)
}
// Prints "5"

Opaque Types vs. Boxed Protocol Types

두개의 타입을 함수의 반환값으로 사용할 때 유사한듯 하지만 타입 정체성을 유지하는지 여부에서 차이가 남

✏️ Opaque Type은 하나의 특정 타입을 참조하며, 해당 함수 호출자는 어떤 타입인지 알 수 없음. 하나의 특정 타입을 참조하기 때문에 기본 타입에 대해 보다 강력한 보증을 할 수 있음

✏️ Boxed Protocol Type은 해당 프로토콜을 준수하는 모든 타입을 참조할 수 있기 때문에, 저장하는 값의 기본 타입에 대해 더 많은 유연성을 제공할 수 있음

1️⃣ Opaque Type 반환 예시

protocol Shape {
    func draw() -> String
}

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}

→ 하나의 특정 타입을 참조

2️⃣ Boxed Protocol Type 반환 예시

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        if shape is Square {
            return shape.draw()
        }
        let lines = shape.draw().split(separator: "\\n")
        return lines.reversed().joined(separator: "\\n")
    }
}

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

 protoFlip 항상 같은 타입의 값을 반환함. 이때 flip과 달리 하나의 특정 타입을 참조할 필요가 없으며 Shape 프로토콜 준수만 하면 되기 때문에, 보다 반환되는 값의 타입의 유연성이 존재

3️⃣ 경우에 따라 반환값 타입이 다른 예시

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }

    return FlippedShape(shape: shape)
}

let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing  // Error

→ 구체적인지 않은 타입을 반환하기 때문에 타입 정보에 의존하는 작업이 사용될 수 없음. ex) Equatable 사용 불가. 즉 모든 타입을 유연하게 반환할 수 있다는 점이 장점이자 단점이 되는 것

예제의 마지막 라인에서 몇가지 이유로 에러가 발생합니다. 즉각적인 문제는 Shape 는 프로토콜 요구사항의 부분으로 == 연산자를 포함하지 않습니다. 이것을 추가하려고 하면 다음 문제는 == 연산자는 좌항과 우항 인수의 타입을 알아야 합니다. 이러한 종류의 연산자는 일반적으로 프로토콜을 채택하는 구체적인 타입과 일치하는 Self 타입의 인수를 사용하지만 프로토콜에 Self 요구사항을 추가하면 프로토콜을 타입으로 사용할 때 발생하는 타입 삭제가 허용되지 않습니다.

Boxed Protocol Type을 사용할 수 없는 위치에 Opaque Type으로 대체

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container { }

// Error: Protocol with associated types can't be used as a return type.
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}

// Error: Not enough information to infer C.
func makeProtocolContainer<T, C: Container>(item: T) -> C {
    return [item]
}

 associatedtype을 갖는 프로토콜은 반환 타입으로 사용 불가능하며, Generic 타입에 대한 정보가 없어 Generic 반환 타입으로 사용 불가능

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"

→ 반환 타입으로 Opaque Type으로 사용하면 [T]배열을 반환하지만 타입 지정을 거부하므로 에러가 생기지 않음

 

참고링크

Documentation

728x90

'TIL' 카테고리의 다른 글

Storyboard  (1) 2024.01.25
Opaque And Boxe Type - 이해용 샘플 코드  (0) 2024.01.19
Opaque And Boxe Type (1)  (0) 2024.01.19
UICollectionView  (0) 2023.12.15
MarkDown 접기  (0) 2023.12.11