달력

0

« 2025/4 »

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

'iOS Development/Swift'에 해당되는 글 2

  1. 2025.03.23 [Swift] associatedtype 이야기
  2. 2018.11.24 inout parameter는 reference를 전달하는 것이 아니다. 4
2025. 3. 23. 08:22

[Swift] associatedtype 이야기 iOS Development/Swift2025. 3. 23. 08:22

1. Intro

: 현재는 iOS 개발 실무를 하고있지 않아서, 알고있던 지식들이 희미해지는 것을 느낍니다. 그냥 놓아버리기에는 아깝다고 생각하여, 공부삼아 정리한 내용들을 tistory에 정리해나가고자 합니다. 첫 시작은 associatedtype으로 정했습니다.

 

2. associatedtype syntax의 등장

: associatedtype syntax가 등장하기 전, typealias를 이용하여 Associated Type(protocol에서 사용하는 type placeholder)를 선언했었습니다.

 

protocol MyProtocol {
    typealias Item
}

 

 이렇게 작성된 Code를 처음 본다고 가정해봅시다. Item은 type 별칭일까요? 아니면, Associated Type일까요? 헷갈립니다. 이를 해결하기 위해, 누군가가 Associated Type을 선언하기 위한 syntax를 제안했었습니다(SE-0011, link).

 

protocol MyProtocol {
    associatedtype Item
}

 

 이 제안은 받아들여졌고, Swift 2.2부터 associatedtype을 사용할 수 있게 되었습니다.

 

3. associatedtype과 constraint

: 개발자들은 associatedtype syntax 추가에 만족하지 않고, 보다 복잡한 제약조건을 associatedtype에 추가할 수 있길 원했습니다. 그중 몇몇 개발자들이 associatedtype에 직접 where 조건을 추가하는 것을 제안(SE-0142, link)했었고, 이는 받아들여져서 Swift 4.0부터 아래와 같은 Code를 작성할 수 있게 되었습니다.

 

protocol Human {
    associatedtype Age where Age: Equatable
    func introduce() -> Age
}

struct Syj: Human {
    func introduce() -> Int {
        return 0
    }
}

 

 얼마지나지 않아, associatedtype과 관련된 기능이 하나 더 추가됩니다. 이전까지 associatedtype은 자신이 속한 protocol을 제약조건으로 사용할 수 없었습니다. 이것에 대해 이야기하는 제안서(SE-0157, link)를 보면, 아래와 같은 Code 작성이 불가능했었습니다.

 

// SE-0157이 받아들여지기 전에는 Compile되지 않는 Code였음.
protocol Sequence {
    associatedtype SubSequence: Sequence
        where Iterator.Element == SubSequence.Iterator.Element, SubSequence.SubSequence == SubSequence

    func dropFirst(_ n: Int) -> Self.SubSequence

 

 그래서 어쩔 수 없이 아래에 작성한 Code처럼, 좀 장황하게 Code를 작성했어야 했었습니다.

 

protocol Sequence {
    associatedtype SubSequence

    func dropFirst(_ n: Int) -> Self.SubSequence
}

struct SimpleSubSequence<Element> : Sequence {
    typealias SubSequence = SimpleSubSequence<Element>
    typealias Iterator.Element = Element
}

struct SequenceOfInts : Sequence {
    func dropFirst(_ n: Int) -> SimpleSubSequence<Int> {
        // ...
    }
}

 

 SE-0157은 채택되었고 Swift 4.1부터 associatedtype은 자신이 속한 protocol을 제약조건으로 사용할 수 있게 되었습니다.

 

4. Opaque Result Type

: 2019년, 어떻게하면 Swift Generic의 사용성을 더 개선할 수 있을지를 이야기해보는 글이 Swift Forum에 올라왔었습니다(link - Improving the UI of generics). Forum에 내용중 하나를 제안했었는데(SE-0244, link), return에 대한 Type-Level Abstraction 문제를 이야기하고 해결책을 제시했었습니다.

 

 제안서에서 말하는 문제가 무엇일까요? 아래의 Code를 살펴봅시다.

 

protocol Shape {
    func draw(to: Surface)
}

struct Rectangle: Shape { /* ... */ }
struct Union<A: Shape, B: Shape>: Shape { /* ... */ }
struct Transformed<S: Shape>: Shape { /* ... */ }

protocol GameObject {
    associatedtype Shape: Shapes.Shape
    var shape: Shape { get }
}

 

 위의 Code에서, GameObject protocol을 따르게 하려면 아래와 같이 장황하게 Return type을 명시해야 했었습니다.

 

struct Star: GameObject {
    var shape: Union<Rectangle, Transformed<Rectangle>> {
        return Union(Rectangle(), Transformed(Rectangle()))
    }
}

 

 shape의 type이 장황하죠? 정확한 Return type을 명시하는 것은 별로 중요하지 않고, Shape protocol을 따른다는 것이 중요합니다. 따라서 지금의 저 장황한 Return type은 Code를 읽는 사람에게도 별로 도움이 되지 않습니다. 그래서 SE-0244에서는 다음과 같은 syntax를 제안합니다.

 

struct Star: GameObject {
  var shape: some Shape {
    return Union(Rectangle(), Transformed(Rectangle()))
  }
}

 

 SE-0244는 채택되었고, Swift 5.1부터 some Protocol syntax를 사용할 수 있게 되었습니다.

 

5. Primary Associated Types

: Protocol은 associatedtype을 가질 수 있지만 constraint를 설정하려면 where을 이용해야 했었습니다.

 

func process<S: Sequence>(sequence: S) where S.Element = String { 
    // ... 
}

func concatenate<S : Sequence>(_ lhs: S, _ rhs: S) -> S where S.Element == String {
    // ...
}

 

 위의 Code는 단순하지만, where이 길어진다면 가독성이 떨어지게 되겠죠. 개선의 필요성과 해결책을 이야기한 제안서(SE-0346, link)에서는 primary associatedtype을 Protocol 선언 시 명시하고, generic type처럼 사용할 수 있는 Syntax를 제안했습니다.

 

// 기존 Syntax
protocol Container {
    associatedtype Item
    func append(_ item: Item)
}

struct StringContainer: Container {
    typealias Item = String
    func append(_ item: String) { }
}

func process<T: Container>(container: T) where T.Item == String { }

// SE-0346에서 제안하는 Syntax
protocol Container<Item> {
    func append(_ item: Item)
}

struct StringContainer: Container<String> {
    func append(_ item: String) { }
}

func process(container: some Container<String>) { }

 

 확실히, SE-0346에서 제안한 Syntax가 Code 가독성을 개선시켜주는 것 같네요. SE-0346은 채택되었고 Swift 5.7부터 primary associatedtype을 정의할 수 있게 되었습니다.

 

 이후, SE-0358(link)에서는 Swift Standard Library의 여러 Protocol에 primary associatedtype을 정의하자고 제안하였습니다. SE-0358도 Swift 5.7에 채택되었습니다.

 

99. Reference

1) Swift Evolution - Proposals

  1. SE-0011 : replace typealias associated (link)
  2. SE-0142 : associated types constraints (link)
  3. SE-0157 : recursive protocol constraints (link)
  4. SE-0244 : opaque result types (link)
  5. SE-0346 : light weight same type syntax (link)
  6. SE-0358 : primary associated types in stdlib (link)

 

2) Swift Documents

  1. protocol (link)
  2. opaque types (link)

 

3) Others

  1. Improving the UI of generics (link)
  2. Generics manifesto (link)
:
Posted by syjdev

 함수내에서, 값(Value)으로 전달받은 인자(Argument)는 참조(Reference)로 전달받는 인자와 차이가 있다. 참조로 전달받은 인자는 변경이 일어났을 때 본래의 객체도 같이 변경되지만 값으로 전달받은 인자는 그렇지 않다.

 

 

1
2
3
4
5
var integer = 5
func multiply(integer: Int, multiplier: Int) {
    //error, integer is defined as a let.
    integer = integer * multiplier
}
cs

 
 애초에 값으로 전달받은 인자는 상수라서 변경조차 불가능하다. 값으로 전달받은 인자를 함수 내에서 수정하려면, 함수의 매개변수(Parameter)에 'inout'이라는 키워드를 붙이면 가능하다.

 

 

1
2
3
4
5
6
7
var integer = 5
func multiply(integer: inout Int, multiplier: Int) {
    integer = integer * multiplier
}
multiply(integer: &integer, multiplier: 2)
print(integer) //10
 
cs



 매개변수에 inout 키워드가 붙었으니, multiply에는 integer의 참조가 전달된 것일까? 

inout Parameter에 대한 Apple의 설명을 보면 이야기가 좀 다르다.



In-Out Parameters: In-out parameters는 다음과 같이 전달됩니다.
1) 함수가 호출됬을 때, 값으로 전달받은 인자는 복사(Copy)됩니다.

2) 함수 내에서는 복사본이 변경됩니다.

3) 함수가 반환(Return)될 때, 복사본의 값이 본래의 값에 할당(Assign)됩니다.

 

 

 Apple의 설명을 확인하기 위해, 간단한 코드를 작성하면...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var number: Int = 2 {
    didSet {
        print("number was assigned.")
    }
}
 
func function(arg: inout Int) {
    print(number) //2
    arg = 5
    print(number) //2
    arg = 10
}
 
function(arg: &number) //"number was assigned."
 
cs

  

 function 내에서 전달받은 인자를 5로 바꾸어도, number는 변하지 않는다. function이 호출된 다음 number의 didSet이 호출되는 것을 볼 수 있는데, 이것을 통해 function 반환 후 본래의 값에 새 값이 할당된다는 것을 확인할 수 있다.

 

 이것은 참조가 전달된 Call-By-Reference와 다르며, Copy-In Copy-Out 또는 Call-By-Value Result라고 불려진다.

 

 

※ 2023.01.24에 추가된 내용

 Swift Docs에 적혀있는, In-Out Parameters의 Optimization에 따르면 Physical Address에 위치한 변수를 In-Out Parameter의 arg로 전달하면 Call-By-Reference로 동작한다고 적혀있다(Call-By-Reference는 Copy로 인한 Overhead는 없지만 Copy-In Copy-Out의 결과와 동일하다).

 

 Compilier의 Optimization Level도 바꾸어 보고, 코드도 이리저리 수정해가며 테스트를 해보았지만... In-Out Parameter를 정의한 함수에서 Call-By-Reference로 동작하는 것을 확인하는 데에 실패했다. Compiler Optimization 결과에 따라 In-Out Parameter의 동작이 달라질 수 있는 것 같으니, 인위적으로 상황을 재현하는 것은 어려울 것 같다.

 

 Swift Docs에 적힌대로, In-Out Parameter를 사용한 함수를 작성할 때, Call-By-Reference를 고려하지 말고 Copy-In Copy-Out로 동작한다고 생각하고 작성하자.

'iOS Development > Swift' 카테고리의 다른 글

[Swift] associatedtype 이야기  (0) 2025.03.23
:
Posted by syjdev