티스토리 뷰

iOS/Swift-Documents

Swift Generics

malrang-malrang 2022. 7. 24. 03:35

Swift Generics

Generics

제너릭 코드 (Generic code) 는 정의한 요구사항에 따라 모든 타입에서 동작할 수 있는 유연하고 재사용 가능한 함수와 타입을 작성할 수 있다. 이는 중복을 피하고 명확하고 추상적인 방식으로 의도를 표현하는 코드를 작성할 수 있습니다.

제네릭은 Swift의 가장 강력한 기능 중 하나이고 Swift 표준 라이브러이의 대부분은 제네릭 코드로 빌드 된다. 예를 들어 Swift의 Array, Dictionary 타입은 모두 제네릭 컬렉션이다. 즉 Array에 Int, String 등 모든 타입을 저장할 수 있는 이유가 제네릭 타입이기 때문이다.
따라서 저장되는 타입에 제한이 없는 것이다.

The Problem That Generics Solve

아래의 예시 코드는 제네릭을 사용하지 않은 함수 swapTwoInts를 정의 한것이다.
swapTwoInts 함수는 In - Out 파라미터를 사용 하여 a, b 두개의 Int 값을 서로 바꾼다.

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

아래의 코드를 실행하게 되면 의도한대로 잘 동작하는 것을 볼 수 있다.

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

swapTwoInts는 Int값에만 적용할 수 있다는 한계가 있다.
만약 String 값을 바꾸고 싶다면 새로 함수를 만들어야 한다.
할수는 있지만...동일한 동작을 하는 코드를 다양한 타입에서 사용하기 위해서는 Generic 코드를 사용하면 된다.

Generic Functions

아래의 코드는 위에서 아까 구현했던 swapTwoInts 함수를 Generic코드를 사용하여 다양한 타입에 적용할 수 있도록 만든 함수다.
함수를 제네릭 으로 만들면 타입 이름 대신 <T>를 사용한다.(Int, String 대신) 이는 placeholder 라고 하며 개발자는 T가 무슨 타입인지 정하진 않았지만 a,b는 모두 같은 타입 T라는 것을 선언한 것이다.
실제 T 대신 사용될 타입은 함수가 호출될 때 정해진다.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

제네릭 함수와 기존 함수와의 차이점은 제네릭 함수는 정의할 때 <T>라는 것을 적어준다는 것인데 여기서 대괄호는 함수이름 과는 상관 없으며 Swift는 T의 타입을 알려고 하지 않는다.
타입은 함수 호출시 Swift가 알아서 유추하여 정하게 된다.

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"

위와 같이 기존의 swapTwoInts함수와 같은 방식으로 제네릭 함수를 호출할 수 있다.

Type Parameters

위의 제네릭 함수에서 <T>를 사용했다.
이는 placeholder라고 불리며 type parameter의 예이다.
타입 파라미터는 플레이스 홀더의 타입을 지정하고 이름을 지정한다. 즉 여기서 지정한 타입 파라미터의 이름은 T인 것이다.
만약 <B>라고 함수 뒤에 작성했다면 타입 파라미터의 이름은 B다.

이러한 타입 파라미터는 실제 사용할때 실제 존재하는 타입으로 변하게된다.
Int, String 등등 의 타입으로 바뀌며 실제 작업을 수행한다.
만약 타입 파라미터가 여러개 필요하다면 <T, B> 와 같이 타입 파라미터를 지정하여 2개의 타입 파라미터를 만들수 있다.

Naming Type Parameters

대부분의 경우 타입 파라미터는 설명이 포함된 이름을 가지고 있다. 예를 들어 Dictionary의 <Key, Value>와 같이 이름을 만든것처럼 말이다.

자유롭게 만들어도 되고 아까와 같은 <T>처럼 U, V 같은 단일 문자를 사용하는것이 일반적이다.

Generic Types

제네릭 함수 외에도 Swift에서 제네릭 타입을 정의할 수 있다.
정의한 제네릭 타입은 클래스, 구조체, 열거형에서 어떤 타입으로도 사용할 수 있으며 비슷한 방법으로 Array, Dictionary에서도 사용할 수 있다.

이번 섹션에서는 Stack이라는 일반 컬렉션 타입을 작성하는 방법을 알아보자.
Stack은 Array와 비슷하지만 LIFO(Last In First Out)규칙을 따르는 제한이 있다. 컬렉션에 저장된 마지막에 위치한곳에서 값을 추가하거나 제거할수 있게 되며 값을 추가하는 것을 PUSH, 값을 제거하는 것은 POP이라 한다.

위의 그림은 스택의 PUSH와 POP을 나타낸 그림이다.
1번(왼쪽) 부터 설명하자면 다음과 같다.

  1. 현재 스택에는 3개의 값이 있다.
  2. 4번째 값은 스택의 상단에 PUSH된다.
  3. 스택은 이제 4개의 값을 가지며 최근값이 가장 상단에 위치한다.
  4. 스택의 최상단 항목은 POP된다.
  5. 값을 POP 한 후에 스택은 다시 3개의 값을 갖는다.

이러한 스택을 제네릭을 사용하지 않고 Int자료형 만 가능하도록 만들어 보자.

struct IntStack {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

위와 같이 만들면 스택을 만들 수 있지만 Int타입 에서만 사용할 수 있다.
이를 모든 자료형이 사용할 수 있도록 제네릭을 함께 사용하면 다음과 같다.

struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

위의 코드에서 타입 파라미터의 이름을 Element로 지정했다.
Stack구조체 안에서 타입의 이름으로 Element를 사용하는것을 볼 수 있다.
Element 타입은 처음부터 타입의 종류가 정해진 것 이 아니고 사용될 때 정해진다.
이러한 개념을 위의 Stack 구조체로 이해해보자.

  • items 프로퍼티를 초기화 할 때 Element 타입으로 만든다.
  • push() 메서드에서 사용하는 사용하는 매개변수의 타입은 Element이다.
  • pop() 메서드에서 반환하는 값의 타입은 Element다.
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

위의 코드를 실행하면 아래의 그림과 같은 방식으로 실행된다.

Stack에서 값을 제거하고 싶다면 아래의 코드처럼 사용하면된다.

let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings

Stack에서 값을 제거하게되면 아래의 그림처럼 값이 제거된다.

Extending a Generic Type

제네릭 타입에 익스텐션을 사용할 때 타입 파라미터는 제공하지 않아도 된다.
하지만 제네릭 타입을 정의할 때 정의한 타입 파라미터 목록은 익스텐션안에서 사용할 수 있으며 타입 파라미터들의 이름도 그대로 사용할 수 있다.

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Prints "The top item on the stack is tres."

위의 코드는 위에서 정의한 Stack 구조체에서 익스텐션을 사용한 예시 이다.
위의 코드에서는 topItem 이라는 읽기 전용 연산 프로퍼티를 추가했다.
topItem 프로퍼티는 Stack에서 항목을 제거하지 않고 Stack의 최상위 항목을 반환한다.
이때 반환 값의 타입은 아까 정의한 타입 파라미터인 Element 타입이다.

위의 코드에서 볼수 있듯이 제네릭 타입에 익스텐션을 사용할 땐 타입 파라미터 목록을 정의 하지 않는다. 하지만 원래 코드에서 정의한 타입 파라미터를 그대로 사용할수 있다.

Type Constraints

위에서 구현한 swapTwoValues, Stack은 모든 타입에서 사용될수 있다.
하지만 제네릭 함수와 타입에서 사용할 수 있는 타입에 제약 조건을 주는것이 유용한 경우가 있다.
타입 제약조건을 만족하기 위해선 타입 매개변수가 특정 클래스를 상속하거나 특정 프로토콜을 준수해야 한다.

예를 들어 Swift의 Dictionary는 Key로 사용할 수 있는 타입에 제약을 두었다.
Dictionary의 Key에 사용될 수 있는 타입은 반드시 해시(Hashable) 할수 있어야한다.
즉 고유하게 표현할 수 있는 방법을 제공 해야한다는 뜻이다.
이러한 제약 조건은 Dictionary의 Key타입에 대한 타입 제약조건에 의해 생기며 제약조건을 만족하기 위해서는 Hashable 프로토콜을 만족하는 타입을 사용해야 한다.
Swift의 기본적인 타입인 String, Int, Double 등은 Hashable 프로토콜을 준수하므로 Dictionary에서 사용할 수 있다.

직접 제네릭 타입을 만들 때 고유한 제약 조건을 정의할 수 있으며 이러한 제약조건은 제네릭 프로그래밍의 많은 기능을 제공할 수 있다.

TypeConstraint Syntax

타입 파라미터를 지정할 때 :(colon)으로 구분된 타입 파라미터 이름뒤에 단일 클래스나 프로토콜 제약 조건을 작성하여 제약 조건을 만들 수 있다.

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

예를 들자면 위의 코드에는 두개의 타입 파라미터가 있다. T는 SomeClass 클래스의 서브클래스여야 하고 U는 SomeProtocol 프로토콜을 준수 하는 타입이어야 한다.
위와 같이 타입 파라미터에 제약 조건을 만들 수 있다.

Type Constraint in Action

타입 파라미터에 제약 조건을 주는 예시 코드를 보자.

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
    print("The index of llama is \(foundIndex)")
}
// Prints "The index of llama is 2"

위의 코드에서 정의한 findIndex(ofString: in:)함수는 제네릭을 사용하지 않은 함수다.

이함수의 파라미터는 String, [String] 타입을 갖는다. 하지만 이함수는 String타입에만 사용할 수 있으므로 모든 타입에 사용할 수 있는 제네릭 함수로 바꿔보자.

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

위와 같이 제니릭 함수로 만들었다.
이제 모든 타입에서 이 함수를 사용할 수 있게 되었지만 문제가있다.
Swift의 모든 타입이 == 연산자를 사용할 수 있지 않기 때문이다.
따라서 해당 연산자를 사용할 수 있는 타입만 사용할 수 있도록 제약 조건을 줘야한다.
(== 연산자는 Equatable 프로토콜을 준수하는 타입만 사용 가능하다.)

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 isn't in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 2

위와 같이 타입파라미터 T에 Equatable 프로토콜을 준수해야 한다는 제약 조건을 주면 문제를 해결할수 있다.

위의 함수를 사용자 정의 타입을 타입파라미터로 사용하고 싶다면 사용자 정의 타입을 Equtable 프로토콜을 준수하도록 해주면된다!(필수 메서드 구현해줘야 합니다!)

Associated Types

  • 프로토콜 에서의 제네릭.

프로토콜을 정의할 때 associated 타입을 선언 하는 경우가 있다.
프로토콜에서 제네릭 타입을 정의할 땐 associatedtype키워드를 사용 해주어야 한다.

Associated Types in Action

아래의 코드는 프로토콜에서 associatedtype 을 사용한 예시다.

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

위의 코드에서 정의한 Container 프로토콜은 append()메서드, count프로퍼티, Int인덱스 값을 사용하는 서브 스크립트를 요구사항으로 정의했다.
요구사항을 만족 하기 위해서 Item 이라는 Associated 타입을 정의했다.

아래의 코드는 위의 Container 프로토콜을 준수하는 타입이다.

struct IntStack: Container {
    // original IntStack implementation
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // conformance to the Container protocol
    typealias Item = Int
    mutating func append(_ item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}

Container 프로토콜을 준수하는 제네릭을 사용하지 않은 구조체 IntStack 를 구현했다.
구조체는 프로토콜에서 요구한 Item 타입의 실제 타입으로 Int를 사용하게 된다. 하지만 이렇게 구현하면 다른 타입에 대해서는 사용할 수 없기 때문에 제네릭을 사용하여 다시 구현해보자.

struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // conformance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

위와 같이 제네릭을 사용한 Stack 구조체를 정의했는데 여기서는 타입 파라미터로 Element라는 이름을 사용 했다.
이제는 Container 프로토콜을 준수하는 Stack 구조체에 다양한 타입을 사용할 수 있다.

Extending an Existing Type to Specify an Associated Type

Associated 타입을 사용한 프로토콜도 익스텐션을 사용하여 기존 타입에 프로토콜을 준수하도록 추가할 수 있다.
예를 들어 Array 타입에 아까 만든 Container 프로토콜을 추가로 채택해보자.

extension Array: Container {}

위와 같이 Array 타입에 Container 프로토콜을 추가로 채택할 수 있다.
Array 타입에는 이미 append()메서드, count프로퍼티, 서브스크립트가 존재 하기 때문에 Container가 요구하는 것을 모두 준수한다.

Adding Constrints to an Associated Type

프로토콜에 정의된 Associated 타입에도 타입 제약 조건을 줄수 있다.

protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

위와 같이 Item에 Equatable 프로토콜을 준수해야 한다는 제약조건을 추가할 수 있다.

Using a Protocol in Its Associated Type's Constraints

프로토콜은 요구 사항의 일부로 나타날 수 있다.

protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

위의 코드는 Container 프로토콜에 요구사항을 추가한SuffixableContainer프로토콜을 정의한 예시다.
추가한SuffixableContainer프로토콜은 Suffix라는 associated 타입과 suffix() 메서드를 추가로 요구한다.
Suffix 타입은 SuffixableContainer 프로토콜을 준수해야 하고 Item 타입은 Container 프로토콜의 Container프로토콜의 Item 타입과 동일해야한다.

extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30

위의 코드는 Stack 타입에 SuffixableContainer 프로토콜을 추가로 채택하는 코드다.
이코드에서 Suffix의 associated 타입도 Stack 이므로 suffix() 메서드는 Stack 타입을 반환한다.

extension IntStack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack<Int> {
        var result = Stack<Int>()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack<Int>.
}

위의 코드는 제네릭을 사용하지 않은 IntStack 구조체에 익스텐션을 사용하여 SuffixableContainer 프로토콜을 추가로 채택하는 코드다.
Suffix의 타입으로 IntStack대신 Stack<Int>를 사용한것을 볼수있다.

Generic Where Clauses

타입 제약 조건을 사용하면 제네릭 함수, 서브 스크립트, 또는 타입에서 associated 타입 파라미터에 대한 요구사항을 정의할 수 있다.

Associated 타입에 대해 요구사항을 정의하는 것도 유용할 수 있다.
이러한 정의는 제네릭 where절을 정의하여 할 수 있다. 제네릭 Where절을 사용하면 associated 타입이 특정 프로토콜을 준수해야 하거나 특정 타입 파라미터와 동일해야 한다고 요구할수 있다.
제네릭 Where절은 where키워드로 시작하고 그뒤에 associated 타입에 대한 제약조건, 동등 관계를 써주면 된다.

   func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
}
// Prints "All items match."

위의 코드에서 allItemsMatch 함수의 본문을 여는 중괄호 바로앞에 있는 where절에서 associated타입에 대한 제약조건을 정의하는 것을 볼 수 있다.
위의 코드에 정의된 함수는 두 개의 Container 인스턴스에 모든 항목이 같은 순서로 존재하는지에 대한 여부를 반환하는 함수다.
이 함수의 두가지 타입 파라미터에 대한 요구사항은 아래와 같다.

  • C1은 Container 프로토콜을 준수해야 한다.
  • C2는 Container 프로토콜을 준수해야 한다.
  • C1의 항목과 C2의 항목은 동일해야 한다.
  • C1의 항목들은 Equatable 프로토콜을 준수해야 한다.

이러한 요구사항을 통해 allItemsMatch 함수는 Container타입이 다른 경우에도 비교할 수 있게 된다.
실제 함수를 사용한 예를 보면 타입이 다른 두 개의 인스턴스를 비교하는 것을 볼 수 있다.

Extensions with a Generic Where Clause

제네릭 where절을 익스텐션 에서도 사용할 수 있다.

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// Prints "Top element is tres."

위의 코드는 제네릭을 사용한 Stack 구조체에 익스텐션을 사용할 때 where절도 사용한 코드다.
익스텐션으로 추가한 isTop() 메서드는 파라미터로 받은 값이 현재 인스턴스의 제일 위에 존재하는 값인지를 확인해주는 메서드다.
여기서 타입 매개변수로 사용할 Element는 Equatable 프로토콜을 준수해야 한다.
만약 그렇지 않다면 컴파일 에러를 발생하고 제네릭 where절은 프로토콜에 익스텐션을 사용할 때도 사용할 수 있다.

아래의 예시 코드는 프로토콜에서 제네릭 where절을 사용한 코드

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "Starts with something else."

Contextual Where Clauses

제네릭 타입을 정의할 때 where절로 일부에만 제약조건을 줄 수 있다.

extension Container {
    func average() -> Double where Item == Int {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
    func endsWith(_ item: Item) -> Bool where Item: Equatable {
        return count >= 1 && self[count-1] == item
    }
}
let numbers = [1260, 1200, 98, 37]
print(numbers.average())
// Prints "648.75"
print(numbers.endsWith(37))
// Prints "true"

위의 average()메서드와 endWith() 메서드에 다른 제약조건을 줄 수 있다.

extension Container where Item == Int {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
}
extension Container where Item: Equatable {
    func endsWith(_ item: Item) -> Bool {
        return count >= 1 && self[count-1] == item
    }
}

Container를 esxtension하여 각각 제약조건을 줄수도 있다.

Associated Types with a Generic Where Clause

Associated 타입에 제네릭 where 절을 사용할 수 있다.

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }

    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}

위의 코드처럼 Associated타입에 제네릭 where절을 사용할 수 있다.
Iterator 타입에 where절로 제약조건을 준 것을 볼 수 있다.

protocol ComparableContainer: Container where Item: Comparable { }

어떤 프로토콜을 상속 하려는 경우 where절을 사용하여 상속된 associated 타입에 제약 조건을 추가할 수 있다.

Generic Subscripts

서브 스크립트에도 제네릭을 사용할 수 있고 where절을 사용하여 제약조건을 줄 수도 있다.

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result = [Item]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}

위와 같이 서브 스크립트에 제네릭과 where절을 사용할 수 있다.
위의 코드에서 서브 스크립트는 다음과 같은 제약 조건이있다.

  • <>로 묶인 매개변수 Indices는 Sequence프로토콜을 준수하는 타입이어야 한다.
  • 서브 스크립트는 매개변수로 Indices타입의 인스턴스를 사용한다.
  • 파라미터 indices에 전달된 값은 Int타입이다.

참고한자료 및 문서

https://bbiguduk.gitbook.io/swift/language-guide-1/generics

https://icksw.tistory.com/69?category=876233

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

Swift 5.5 Functions -6  (0) 2022.02.04
Swift 5.5 Control Flow -5  (0) 2022.01.27
Swift 5.5 Collection Types -4  (0) 2022.01.27
Swift 5.5 Strings and Characters -3  (0) 2022.01.25
swift5.5 공식문서 Basic Operators -2  (0) 2022.01.23
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
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 31
글 보관함