iOS

some 과 any 그리고 Any?(1)

스엠 2022. 10. 12. 20:27

 

우연히 블로그를 돌아다니며 코드 읽기를 하던중 any라는 키워드를 보게 되었습니다.

swift 4.x 대 부터 개발을 시작하면서 Any는 보더라도 any는 처음 보더라구요 그래서 노트 해둔 뒤 늦게나마 글을 쓰게 되었습니다.

some은  SwiftUi가 나온 시점에서 Any는 뭐 유서 깊으니까 그러려니 했습니다. 

하지만 any라는 이건 뭘까요?

그럼 우선 some, Any에 대해서 알아보겠습니다.

 

some

swiftUi를 한번이라도 보시고 사용해보셨으면 대충이나마 알거라 생각합니다.

WWDC에 따르면

  • And the some keyword that we use here is a switch feature that lets swift infer out inter return type automatically

이렇게 나와 있네요. 해석해보면 다음과 같습니다. 

: some 키워드는 스위프트가 자동적으로 타입을 추론하여 리턴값으로 반환해주는 일종의 스위치다.

 

이 문장만 보면 굉장히 만능해결사 같고 모호하죠. 

우선 제대로 된 작동 원리를 알고자 한다면 Opaque Types(불투명 타입)에 관하여 먼저 인지해야 합니다.

 

이것도 역시 도큐먼트에서 퍼오자면 다음과 같습니다.

A function or method with an opaque return type hides its return value’s type information. Instead of providing a concrete type as the function’s return type, the return value is described in terms of the protocols it supports. Hiding type information is useful at boundaries between a module and code that calls into the module, because the underlying type of the return value can remain private. Unlike returning a value whose type is a protocol type, opaque types preserve type identity—the compiler has access to the type information, but clients of the module don’t.

 

주저리 주저리 길게 설명 되어 있지만 핵심은 첫문장에 있는 것 같아요.

:불투명한 리턴 타입을 가진 함수는 리턴 값의 정확한 정보를 감춘다. 

 리턴값의 명확한 타입을 제공하는 대신에 리턴값이 채택하고 있는 프로토콜을 사용하여 설명을 한다.

음 뭐 한마디로 Int, String 이런거 대신에 내가 반환하는 값은 Int의, String의 protocol을 채택하고 있어~~ , 뭐 요런 느낌인것 같습니다. 

 

예시를 한번 보겠습니다. 

protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result: [String] = []
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}
let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
// *
// **
// ***

우선 위와 같이 ASCII art shape을 그려주는 모듈을 작성하고 있다고 가정해 봅시다. 

여기서 만약에 뒤집어진 삼각형을 그리고 싶다면 어떻게 할까요?

다음과 같이 generic을 이용해서 코드를 짤수 있습니다. 

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
// ***
// **
// *

하지만  generic을 이용해서 코드를 구현할 경우 FlippedShape를 만드는데 쓰인 정확한 genericType이 노출된다는 단점이 있습니다. 

 

한 발자국 더욱 가서 삼각형과 뒤집어진 삼각형을 붙이는 코드를 짜고 싶다면 다음과 같이 될 겁니다. 

Struct JoinedShape<T: Shape, U: Shape>: Shape {
    var top: T
    var bottom: U
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *

여기까지 보면 그냥 제너릭 이용하면 되겄네 하실겁니다. 하지만 어떤것이 문제가 될까요? 

 

 

Exposing detailed information about the creation of a shape allows types that aren’t meant to be part of the ASCII art module’s public interface to leak out because of the need to state the full return type. 

:이러한 Shape를 만드는 자세한 정보(코드)는 ASCII art module의 퍼블릭 인터페이스가 아님에도 불구하고 명확한 리턴값을 명시하고 있기 때문에 외부로 노출됩니다.

이게 뭔말인가 싶죠?;; 저도 아직 잘 모르겠으니 쭉쭉 읽어 봅시다.

 

The code inside the module could build up the same shape in a variety of ways, and other code outside the module that uses the shape shouldn’t have to account for the implementation details about the list of transformations.

: 모듈 안에 있는 코드는 똑같은 모양에 대해서 여러 방법으로 구현될 수 있으며, 모듈 밖에서 shape를 사용하고 있는 코드는 

모양 변형 목록에 대한 구체적인 설명을 할 필요가 없습니다.

아직도 감이 안잡히네요 ...

 

Wrapper types like JoinedShape and FlippedShape don’t matter to the module’s users, and they shouldn’t be visible.

: JoinedShape와 FlippedShape같은 Wrapper Type은 모듈을 사용하는 유저에게 보이면 안됩니다. 

 

 

The module’s public interface consists of operations like joining and flipping a shape, and those operations return another Shape value.

:모듈의 퍼블릭 인터페이스는 joining, flipping같은 기능으로 구성되며 다른 Shape Value로 반환합니다. 

 

으으 어렵네요. 개인적으로 해석하자면 FlippedTriangle, JoineShape이렇게 명확한 반환 타입이 아니라 그냥 뭉뜽그려서 Shape라는 protocol 타입으로 반환을 해야 한다!! 이런 소리인거 같긴 합니다. 

이렇게 하면 확실히 코드가 유연해지긴 하겟죠. + 코드의 은닉성도 향상된다고 봅니다. 

 

//제너릭을 이용한 경우 이렇게 세부타입이 무조건 노출됨
let flippedTriangle: FlippedShape<Triangle> = FlippedShape(shape: triangle)

//하지만 opaqueType을 쓰면 세부 타입 노출이 되지 않음 은닉성 업
func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}

let flippedTriangle: Shape = flip(triangle

 

아직 더 남아 있으니 계속 가봅시다.

 

왜 불투명한 값(Opaque type)이 필요한지 알았으니 이번엔 불투명한 값을 반환하는 법에 대해서 알아봅시다. 

오패큐 타입을 단순하게 제너릭의 반대라고 생각할 수 있습니다. 

func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }

 

max함수를 부르는 곳에서 x,y 그리고 T에 대한 정보를 다 입력합니다. 하지만 오패큐 타입은 반대입니다.

 

오패큐 타입을 반환하는 함수는 함수 구현부에서 타입을 정하고 이것을 추상화된 방식으로 반환해줍니다. 

함수를 콜 할때 타입을 지정하냐 아니면 함수 자체에서 임의로 타입을 결정하냐가 차이인 겁니다.

struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array<String>(repeating: line, count: size)
        return result.joined(separator: "\n")
    }
}

func makeTrapezoid() -> some Shape {
    let top = Triangle(size: 2)
    let middle = Square(size: 2)
    let bottom = FlippedShape(shape: top)
    let trapezoid = JoinedShape(
        top: top,
        bottom: JoinedShape(top: middle, bottom: bottom)
    )
    return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
// *
// **
// **
// **
// **
// *

 

예시를 보면  makeTrapezoid() 함수는 명확한 타입이 아니라 some Shape을 리턴 함으로써, 아 뭔진 모르겠는데 Shape protocol을 준수하는 것을 보냈어, 리턴 타입을 유연하게 한걸 볼 수 있습니다.

추가적으로 Trapezoid모형을 만들기 위해서 2개의 triangle과  square를 이용했지만 리턴 타입이 some Shape로 되어 있으므로 shape 프로토콜만 준수하고 있다면 다른 모형을 써서 Trapezoid를 구현할 수 있게 되었습니다. 

Generic으로 했었다면 T, U가 명시되어있었기 때문에 이렇게 유연한 작업을 하지 못 했을 것이라 생각합니다. 

 

 

여기까지보면 그냥 Protocol 넘기는 것과 차이가 없는거 아니야? 라고 생각하실 수 도 있습니다.

결론을 우선 말하면 오패큐타입과 프로토콜타입의 차이점은 identity를 가지고 있냐라고 보시면 될 것 같습니다. 

오패큐 타입은 identity를 가지고 있고 프로토콜 타입은 가지고 있지 않습니다. 

 

따라서 프로토타입은 == 과 같은 연산이 불가능하고 

protocol 안에  associated Type이라도 있는 경우

( swiftUI에서 some으로 반환하는 이유도 view 안에  associatedType이 있기 때문입니다)

프토콜 타입의 반환은 아예 가능하지가 않습니다. 

 

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]
}

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

 

 

음 그래서 결론을 말하면 some은 

  • generic 보다 유연하게 코드를 바꾸고 싶을때 사용할 수 도 있고
  • protocol에 associatedType이 있어 반환을 하지 못할때 사용할 수 도 있고 
  • protocol로 넘어오는 값을 서로 비교하거나 Self가 필요한 연산자를 실행해야 할때 대체할 수 있고 
  • 아니면 protocol인데 특정한 identity가 필요한 경우 예를들어 배열 같은 경우

제 수준에서는 요렇게 4가지 경우  some을 사용하면 되지 않나라고 생각됩니다. 

막상 제대로 공부하고 쓰려고 하니 생각보다 굉장히 어려운 내용이었네요. 

 

'iOS' 카테고리의 다른 글

KeyChain vs UserDefaults(1)  (0) 2022.11.17
some 과 any 그리고 Any?(2)  (0) 2022.10.26
ARC란 무엇인가?(3)  (0) 2022.05.21
ARC란 무엇인가?(2)  (0) 2022.05.21
ARC란 무엇인가?(1)  (0) 2022.05.21