본문 바로가기
Swfit

some에 대해 알아보기 (any와 비교)

by GGShin 2024. 1. 22.

some은 any처럼 protocol 앞에 붙일 수 있는 키워드이고 기능도 any와 비슷해 보여 설명을 읽어봐도 잘 와닿지 않았습니다. Opaque type이라고도 불리는 some은 보통 '역제네릭 타입, caller에게 concrete type을 숨기기 위함이다' 라는 말로 주로 설명이 되어있습니다.모두 맞는 말이지만 이해는 잘 안되고 "그냥 any 쓰면 안되는 건가?" 라는 생각만 들더라구요. 그러던 중에 Apple에서 제공한 영상을 보고 확실히 용법 차이가 이해가 되어서 기록해봅니다. 

 

그 둘 간의 차이를 표로 먼저 정리해서 보겠습니다.

  some (default로 사용 권장) any
타입 정보 유무 O

Hold fixed concrete type.
(구체적인 타입에 대한 정보를 가짐.)

Infer the underlying type.
(underlying type 추론 가능.)
X




Holds an arbitrary concrete type.
(구체적인 타입 정보를 갖지 않음.)
타입 관계 보장 여부 O

Gurantees type relationship.
(타입 간 관계 보장.)
X

Erases type relationships.
(타입 간 관계 지움.)

 

some은 타입에 대한 정보를 계속 가지고 있고, 타입 간 관계가 정의되어 있는 경우에 그 관계 역시 보장되어서 compiler가 알고 있습니다. 반면에 any는 타입을 지우는 기능을 하기 때문에 구체적인 타입 정보도 compiler가 모르게 될 뿐 아니라 타입이 가지고 있는 다른 타입과의 관계도 알 수가 없습니다. 동일한 protocol을 준수한다고 해도, 구체적인 타입별로 implementation이 다를테니, 구체적 타입을 모른다면 타입 간 관계성도 모르는 게 당연합니다.

 

그래서 some을 사용하면 compiler가 타입 체크를 해주고, any를 사용하면 run time에 구체적인 타입을 알게됩니다. 그러다보니, 코드 작성시에 에러를 줄일 수 있는 것은 some이 훨씬 유리하기에 Apple에서는 some을 default로 사용하기를 권장하고 있습니다. (let을 default로 사용하고 필요에 따라 var로 변경하여 사용하는 것처럼!)

 

둘 다 다형성 원칙을 따르는 코드를 설계하도록 도와주고,

some은 타입을 보장해주므로 조금 더 compile time에 안전성을 보장 받을 수 있다고 이해할 수가 있습니다. 

 

사용 위치별 예시


1) Parameter에서 사용하기 (from Swift5.7)

protocol Animal {
   associatedtype Feed: AnimalFeed
   func eat(_ food: Feed)
}
struct Horse: Animal {...}

func feed(_ animal: some Animal) {...} // Parameter로 사용된 some.
feed(Horse()) //Caller에 의해서 구체적인 타입이 정해지게 됩니다.

feed(_:) function에서처럼 Parameter에서 opaque type이 사용된 경우에, 

구체적인 underlying type은 function이 호출되는 곳에서 사용된 value에 의해 추론됩니다. 

(For parameters with opaque type, the underlying type is inferred from the argument value at the call site.)

 

2) Local variables에서 사용하기

struct Horse: Animal {...}
struct Chicken: Animal {...}

var animal: some Animal = Horse()
animal = Chicken() // Compile error! 
//이미 구체적인 타입이 Horse로 정해졌기 때문에 (compiler가 알고 있음)
//Animal을 구현한 Chicken일지라도 타입이 Horse가 아니라서 사용 불가!

 

이 예시를 보면 some과 any의 차이가 극명하게 느껴집니다.

 

some Animal type인 animal 변수를 선언할 때 Horse()를 대입했기 때문에 compiler는 "앞으로 animal 변수에는 Horse type 값만 할당될 수 있는 것이다." 라는 것을 알게 됩니다. 

만약에 animal 변수가 any Amimal type이었다면, Horse뿐만 아니라 다른 타입들도 Animal만 conform한다면 다 할당이 될 수 있게 됩니다.

 

동일한 이유에서, array의 타입이 any Animal인 경우와 some Animal인 경우도 차이가 있습니다.

 

var animalList: [any Animal] = [Horse(), Chicken(), Mouse()] // OK! 

var animalList: [some Animal] = [Horse(), Chicken(), Mouse()] // Error!

var animalList: [some Animal] = [Horse(), Horse(), Horse(), Horse()] // OK!

 

any Animal array는 구체적인 타입은 달라도 Animal만 conform하는 모든 타입들이 같이 담길 수 있으나, some은 구체적인 타입도 동일해야만 합니다.

 

protocol이 아닌 'Any' 타입을 쓸 때와 Int, String 등의 구체적인 타입을 명시할 때랑 비슷하게 느껴집니다. 

[Any]에는 타입 관계 없이 모든 객체들이 들어갈 수 있지만 [Int]에는 Int type으로만 제한되는 것 처럼 말입니다.

 

Type Erase
결국 any keyword가 하는 역할은 "type erase"입니다.
"Type erase"는 서로 다른 concrete types을 동일한 representaion으로 사용하는 방식을 의미합니다. 
Type erasing을 하면 concrete type은 compile time에는 지워지고, run time에서 concrete type을 알 수 있게 됩니다.

 

3) Results에서 사용하기

Result type에서 사용되는 opaque type의 underlying type은 실제로 function이 구현된 곳에서 명시된 return type에 따라서 결정됩니다.

func makeView(for farm: Farm) -> some View {
   FarmView(farm)
}

 

 

혹시 SwiftUI에서 view를 만들다가 "Function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types" 라는 compile error를 보신 적이 있으신가요? 

이것 역시 some keyword의 영향인데요, return type은 항상 동일해야만 하기 때문에 다른 경우에는 해당 에러가 발생하게 됩니다.

 

조건에 따라서 다른 view를 return하려고 할 때 보통 저러한 에러를 만나게 됩니다.

func makeView(for farm: Farm)-> some View { // Error!
    if condition {
        return FarmView(farm)
    } else {
        return EmptyView()
    }
}

 

해당 function 안에서 동일한 type의 값들만 return 되야 하기 때문입니다.

 

NOTE: 조건에 따라 다른 종류의 view를 리턴하고자 한다면, @ViewBuilder 를 사용해주면 됩니다.
@ViewBuilder
func makeView(for farm: Farm) -> some View {
    if condition {
        return FarmView(farm)
    } else {
        return EmptyView()
    }
}​

 

 

 

타입 보장


some을 사용해서 type이 정해지면, 해당 타입의 연관 타입(associatedtype)도 보장이 됩니다.

무슨 의미인지 구체적인 예시로 설명해보도록 하겠습니다.

 

struct Cow: Animal {
    typealias Feed = Grass
    
    func eat(_ food: Grass) {
        //...
    }
}

struct Horse: Animal {
    typealias Feed = Carrot
    
    func eat(_ food: Carrot) {
        //...
    }
}

 

모든 동물들은 먹지만, 다른 종류의 음식을 먹습니다. 이때 각 동물이 먹는 먹이의 종류를 associatedtype으로 명시해주면, 해당 타입도 유지가 됩니다. 

 

var animal: some Animal = Horse()

 

로 되었을 때, animal.eat(_:)의 파라미터로는 반드시 Carrot 객체가 들어가야 하고, 다른 객체가 들어가면 아무리 AnimalFeed를 conform하더라도 컴파일 에러가 나게 됩니다.

 

마무리!

이렇게 some은 제네릭 처럼 사용하면서도 컴파일 타임에서 타입 체크를 도와주는 유용한 툴이라는 것을 잘 알 수 있었습니다.

코드 작성하면서 잘 활용해 보도록 해야겠습니다.


참고자료

https://developer.apple.com/videos/play/wwdc2022/110352/

 

Embrace Swift generics - WWDC22 - Videos - Apple Developer

Generics are a fundamental tool for writing abstract code in Swift. Learn how you can identify opportunities for abstraction as your code...

developer.apple.com

 

반응형