티스토리 뷰

반응형

이번 글에서는 Swift 공식 문서의 22번째 단원인 Generics을 읽고 정리한 글을 쓰려고 한다.

Apple Swift 공식 문서 22단원 - Generics

Generics

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

 

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


The Problem That Generics Solve

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를 정의한 것이다. swapTwoInts 함수는 In-Out 매개변수를 사용하여 a,b의 값을 바꾼다. 실제 위의 코드에서 사용한 것을 보면 잘 동작하는 것을 볼 수 있다.

 

swapTwoIntsInt 값에만 적용할 수 있다는 한계가 있다. 만약 String 값을 바꾸고 싶다면 새로 함수를 작성해야 하는 것이다. 할 순 있지만 조금 귀찮다. 즉 동일한 동작을 하는 코드를 다양한 타입에서 사용하기 위해선 Generic 코드를 사용하면 된다.


Generic Functions

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

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

 

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

 

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

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

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

 


Type Parameters

아까 위에서 본 제네릭 함수에서 <T>를 사용한 것을 봤을 것이다. 이는 placeholder라고 불리며 type parameter의 예이다. 타입 매개변수는 플레이스 홀더의 타입을 지정하고 이름을 지정한다. 즉 여기서 지정한 타입 매개변수의 이름은 T인 것이다. 만약 <B>라고 함수 뒤에 썼다면 타입 매개변수의 이름은 B이다.

 

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


Naming Type Parameters

대부분의 경우 타입 매개변수는 설명이 포함된 이름을 가지고 있다. 예를 들어 Dictionary<Key, Value>과 같이 이름을 만든 것도 확인할 수 있다. 물론 이는 자유롭게 만들면 되고 아까와 같은 <T>처럼 단일 문자를 사용하는 것이 일반적이다.


Generic Types

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

 

이번 섹션에서는 Stack이라는 일반 컬렉션 타입을 작성하는 방법을 알아볼 것이다. StackArray와 비슷하지만 LIFO(Last In First Out)의 규칙을 따르는 제한이 있다. 따라서 컬렉션 끝에서만 항목을 추가하고 제거할 수 있게 되며 값을 추가하는 것은 PUSH, 값을 제거하는 것은 POP으로 불린다.

 

위의 그림은 스택의 PUSHPOP을 나타낸 그림이다. 왼쪽부터 설명을 하면 다음과 같다.

  1. 스택에 3개의 값이 있다.
  2. 네 번째 값이 스택의 top에 push 된다.
  3. 가장 마지막에 들어온 네 번째 값이 제일 위에 존재하게 된다.
  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로 지정했다. 때문에 구조체 안에서 타입의 이름으로 Element를 사용하는 것을 볼 수 있다. Element 타입은 처음부터 타입의 종류가 정해진 것이 아니고 사용될 때 정해진다. 이러한 개념을 위의 Stack 구조체로 이해해보자.

  • items 프로퍼티를 초기화할 때 Element타입으로 만든다.
  • push(_ :) 메서드에서 사용하는 매개변수의 타입은 Element이다.
  • pop() 메서드에서 반환하는 값의 타입은 Element이다.

 

제네릭 타입이기 때문에 Stack을 사용하여 Array,Dictionary와 유사한 방식으로 Stack을 만들 수 있다.

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

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

 

let fromTheTop = stackOfStrings.pop()
// fromTheTop에는 "cuatro"가 들어있게 되고 스택에는 3개의 값만 남는다.

스택에서 값을 제거하고 싶다면 위의 코드처럼 사용하면 된다. 그렇게 되면 위의 그림처럼 값이 제거된다.

 


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이라는 읽기 전용 계산 프로퍼티를 추가했다. 이 프로퍼티는 스택에서 항목을 제거하지 않고 스택의 최상위 항목을 반환한다. 이때 반환 값의 타입은 아까 정의한 타입 매개변수인 Element 타입이다.

 

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

 


Type Constraints

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

 

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

 

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

 

Type Constraint Syntax

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

 

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

예를 들어 위의 코드에는 두 개의 타입 매개변수가 있다. TSomeClass 클래스의 서브 클래스여야 하고 USomeProtocol 프로토콜을 준수하는 타입 이어야 한다. 위와 같이 타입 매개변수에 제약조건을 만들 수 있다.

 

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의 모든 타입이 ==연산자를 사용할 수 있지는 않기 때문이다. 따라서 타입 매개변수로 해당 연산자를 사용할 수 있는 타입만 사용할 수 있도록 제약조건을 줘야 한다.

 

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

위와 같이 타입 매개변수 TEquatable 프로토콜을 준수하라는 제약조건을 주면 문제를 해결할 수 있다.

 


Associated Types

프로토콜을 정의할 때 정의의 일부로 associated 타입을 선언하는 경우가 있다. 프로토콜에서 이러한 타입을 정의할 땐 associatedtype 키워드를 함께 사용해줘야 한다.

 

Associated Types in Action

그럼 associated 타입에 제네릭을 사용하는 예를 살펴보자.

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

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

 

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 프로퍼티, Int인덱스를 사용하는 서브 스크립트가 존재하기 때문에 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 }
}

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

 

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 프로토콜의 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인스턴스에 모든 항목이 같은 순서로 존재하는지에 대한 여부를 알려주는 함수이다. 이 함수의 두 가지 타입 매개변수에 대한 요구사항은 다음과 같다.

 

  • C1Container 프로토콜을 준수해야 한다.
  • C2Container 프로토콜을 준수해야 한다.
  • 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(_ :) 메서드는 매개변수로 받은 값이 현재 인스턴스의 제일 위에 존재하는 값인지를 확인해주는 메서드이다. 여기서 타입 매개변수로 사용할 ElementEquatable 프로토콜을 준수해야 한다. 만약 그렇지 않다면 컴파일 에러를 발생한다.

제네릭 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."

위의 코드와 같이 프로토콜에서 제네릭 where절을 사용할 수 있다.

 


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()메서드와 endsWith(_ :)메서드에 서로 다른 제약조건을 줄 수 있다. 이렇게 한 번에 추가해도 되지만 이전에 배운 방식대로 따로따로 추가할 수도 있다.

 

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

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절을 사용할 수 있다. 위의 코드에서 서브 스크립트는 다음과 같은 제약조건이 있다.

  • <>로 묶인 매개변수 IndicesSequence프로토콜을 준수하는 타입 이어야 한다.
  • 서브 스크립트는 매개변수로 Indices타입의 인스턴스를 사용한다.
  • 매개변수 indices에 전달된 값은 Int 타입이다.
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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
글 보관함