Swift에는 클래스를 확장하는데 도움이 되는 기능이 포함되어 있다. 프로토콜은 메소드의 구현을 의무화할수 있기에, 확장(extension)는 메소드를 나중에 추가한다. 이 두개를 합한 기술을 설명한다.


프로토콜(Protocol)

Swift는 클래스에 포함된 메소드를 호출해 처리한다. 어떤 오브젝트를 처리할 때 "그 안에 어떤 메소드가 준비되어 있는가"를 생각하게 된다.

여기에 완전히 다른 클래스 객체가 여러개가 있었다고 하자. 그것들 모와서 어떠한 처리할 때 "모두에 공통된 메소드"를 제공하게 되면 매우 도움이 될 것이다.

그렇지만, 클래스가 다른 경우 각각에 같은 메소드가 준비되어 있을 지에 대해 전혀 알 수가 없다. 각각의 클래스가 상속 관계에 있으면, 슈퍼 클래스에 메소드를 준비해 두는 것으로 어떻게든 되겠지만, 상속 관계가 없는 클래스에 대해서는 같은 메소드가 있다는 것을 보증할 수 없다. "프로그래머가 신경을 써서 공통의 것을 준비해 둔다"가 아니라, "반드시 이 메소드가 준비되어 있다"고 확신할 수 있는가 라는 것이다.

이것을 가능하게 하는 것이 "프로토콜"이다. 프로토콜은 속성과 메소드 선언만 작성한 클래스와 같은 모양을 하고 있다. 작성 방법을 정리하면 이렇게 될 것이다.

protocol 프로토콜 이름 {
    ...... 속성과 메소드의 선언 ......
}

메소드는 {} 안에 구현 부분은 필요 없다. 단순히 "func ○○ (✕✕) -> △△;"라고 선언 부분만 있으면 된다.

이렇게 작성된 프로토콜은 클래스에 통합 이용된다. 이것은 클래스 상속과 기본적으로 유사하게 작성을 한다.

class 클래스명 : 프로토콜명 {...}

클래스의 상속을 하고 있는 경우는 "클래스명 : 슈퍼 클래스 이름, 프로토콜 이름 ......"와 같이, 상속 클래스명 뒤에 쉼표로 구분하여 작성한다. 또한 프로토콜은 다중를 설정할 수 있기에 이런 경우에도 각각을 쉼표로 구분하여 작성해 주면 된다.

이렇게 프로토콜을 설정된 클래스는 프로토콜에 포함된 모든 메소드를 구현해야 한다. 구현하지 않으면 오류가 발생하여 실행할 수 없다. 여러 클래스에 프로토콜을 설정하면, 그 클래스에 반드시 프로토콜의 메소드가 구현되어야 한다는 것이다.

 

그러면 실제로 프로토콜을 사용한 간단한 예제를 만들어 보자.

protocol MyProtocol {
    func printData ()
}

class PersonData: MyProtocol {
    var name:String = ""
    
    init(name:String) {
        self.name = name
    }
    
    func printData() {
        print("[name: \(name).]")
    }
}

class MemoData: MyProtocol {
    var content:String = ""
    
    init(content:String) {
        self.content = content
    }
    
    func printData() {
        print(content)
    }
}

var data:[MyProtocol] = []
data.append(PersonData(name: "kimkc"))
data.append(MemoData(content: "아침 9시부터 회의"))
for obj in data {
    obj.printData()
}

여기에서는 MyProtocol라는 프로토콜을 정의하고 거기에 데이터를 출력하는 printData라는 메소드가 작성되어 있다. 그리고 이 프로토콜을 구현한 클래스로 "PersonData "과 "MemoData"를 작성했다. PersonData는 개인 정보를 관리하는 것이고, MemoData 메모를 보관하는 것이다. 어느 쪽도 전혀 상속 관계도 없고, 사용할 수 있는 속성도 다르다.

클래스 정의 후에 이를 이용한 간단한 처리가 있다. PersonData과 MemoData를 배열로 모와서 반복적으로 내용을 출력한다.

여기에서는 MyProtocol의 배열을 만들고, 거기에 PersonData과 MemoData을 보관하고 있다. 프로토콜은 클래스과 같이 유형(타입)으로 지정할 수 있다.

MyProtocol 배열에는 MyProtocol를 구현한 클래스의 인스턴스라면 어떤 것이라도 넣을 수 있다. 그리고 MyProtocol에 캐스팅된 인스턴스에서 for문으로 printData를 호출하고 있다.

이 처럼, '프로토콜의 인스턴스로 캐스팅하고 메서드를 호출'이라는 방식으로 PersonData과 MemoData는 상속 관계도 아무것도 전혀 무관한 클래스를 한꺼번에 처리 할 수 있게 되는 것이다.



확장(Extension)

프로토콜은 클래스 메소드의 구현을 의무화하는 것이었지만, 확장 (Extension)는 클래스에 직접 메소드를 덧붙이는 기능이다. 이는 사용법도 매우 간단하고, 아래와 같이 작성한다.

extention 클래스 이름 {
    ...... 추가 내용 ......
}

"클래스명"에는 기능을 추가하는 클래스명을 지정한다. 정말 간단하다.

추가할 수 있는 것은 메소드뿐만 아니라, 속성도 가능하다. 단, 속성의 경우 "Computed 속성"만 추가할 수 있다. 메소드처럼 get/set 처리를 준비하는 유형(타입)의 속성이다.

이 확장은 자작 클래스뿐만 아니라 Swift 시스템 라이브러리로 제공되는 클래스와 iOS 프레임워크의 클래스 등도 확장할 수 있다.

예를 들어, Int 클래스를 확장하여 1부터 주어진 숫자의 합계를 얻는 메소드 getTotal을 추가해 보도록 하자.

extension Int {
    func getTotal()->Int {
        var total:Int = 0
        for i in 1...self {
            total += i
        }
        return total
    }
}

var num = 1234
print(num.getTotal())

여기에서는 extension Int와 같이 하여 Int 클래스를 확장하고 있다. 이렇게 함으로써, Int 형의 값 모두에 getTotal 메소드가 추가 되었다. var num = 1234와 같이 일반적으로 작성된 Int 변수도 getTotal를 사용할 수 있게 된 것이다. 예를 들어, "100까지의 합을 알고 싶다"고 한다면 단지 "100.getTotal()"실행하면 된다.

Int와 String 같이, Swift의 가장 기본적인 것에 대해서도 이렇게 확장에서 쉽게 기능을 추가 할 수 있다.





프로토콜과 확장을 결합

이 프로토콜과 확장을 결합하면, 재미있는 것을 할 수 있다.

확장은 메소드와 속성뿐만 아니라 프로토콜을 추가하는 것도 가능하다. 이는 이미 있는 여러 클래스에 공통된 기능을 구현하여 함께 처리할 수도 있다.

이것도 실제 사용 예제를 살펴 보자.

protocol MyDataPrintable {
    func printData ()
}

extension String: MyDataPrintable {
    func printData() {
        print("문자열:\(self).")
    }
}

extension Int: MyDataPrintable {
    func printData() {
        print("숫자:\(self).")
    }
}

var str = "Hello"
var num = 12345
str.printData()
num.printData()

위에 예제는 MyDataPrintable라는 프로토콜을 작성하고, 거기에 printData 메소드가 작성되어 있다. 그리고 확장을 사용하여 이와 같이 String과 Int에 MyDataPrintable 프로토콜을 추가한다.

extension String: MyDataPrintable {...}
extension Int: MyDataPrintable {...}

이렇게 하면 모든 String 값이나 Int 값이 MyDataPrintable로 취급할 수 있게 된다. 즉, String과 Int를 같은 프로토콜 클래스로 동일하게 모와서 처리할 수 있게 되는 것이다.

이와 같이, 확장 및 프로토콜은 단순히 자신의 클래스를 강화할 뿐만 아니라 Swift에 포함되어 있는 모든 클래스에 자신의 확장을 실행 시킬 수 있고, 나름대로 커스텀마이징 하는 것을 허용한다. 꽤 강력한 기능이므로 꼭 기억해 두도록 하자.

'Swift' 카테고리의 다른 글

[Swift] 프로토콜 및 확장  (0) 2017.12.10
[Swift] 함수 리터럴 및 클로저  (0) 2017.12.10
[Switf] 구조체, 열거형, 튜플  (0) 2017.12.09
[Swift] 배열과 사전  (0) 2017.12.09
[Swift] 클래스 기본  (0) 2017.12.09
[Swift] 함수  (0) 2017.12.09

Swift에는 "클로저(Closure)"라는 기능이 있다. 이것은 함수를 "값"으로 처리하는 구조이다. 이 클로저와 값으로 함수의 사용에 대해 설명한다.


함수는 "값"이다!

Swift 함수에는 매우 중요한 고유의 특성이 있다. 그것은 "값으로 취급"하는 것이다. 함수는 처리를 요약 한 것이지만, 이 함수 자체도 Swift에서는 "값"이다.

실제로 간단한 예제를 실행해 보자.

func calc(num:Int)->Int {
    var res = 0
    for n in 0...num {
        res += n
    }
    return res
}
 
var f1 = calc
 
print(f1(10))

위에 예제는 calc라는 함수를 정의하고 있다. 그리고 이 calc을 변수 f1에 대입한 다음에 이 변수 f1에 인수로 호출한다.

이제 제대로 calc의 처리가 실행되고 결과를 얻을 수 있다. 즉, 변수 f1에 calc 함수가 할당되어 제대로 작동하고 있다.

이 "함수는 값이다"라는 특징 덕분에, Swift에는 다양한 형태로 함수를 취급할 수 있다. 우선 이 "함수는 값"이라는 점을 확실히 기억하도록 하자.



클로저 (Closure)

이 "값으로써의 함수"의 기본을 알았으니, 이제 Swift의 중심 기능인 "클로저(Closure)"를 사용해 보기로하자.

클로저라고 하는 것은 "값으로써 함수를 인수로 지정하고 다른 함수에 전달하여 사용한다"는 방식이다. 함수는 값이기에 함수 자체를 인자로 지정한 함수도 만들 수 있다.

이렇게 함수의 처리 자체를 인수로 지정하고 다른 함수에 전달하는 기능이 클로저이다.

이는 실제로 보지 않으면 왠지 생소하게 느껴질 것이다. 아래에 간단한 사용 예제를 보도록 하자.

func calc(num:Int)->Int {
    var res = 0
    for n in 0...num {
        res += n
    }
    return res
}

func printResult(function:(num:Int)->Int, n:Int) {
    print(function(num: n))
}

printResult(calc, n: 123)

여기에서는 calc와 printResult라는 2개의 함수를 정의하고 있다. calc는 이전까지의 샘플과 동일하고 0부터 인수 값까지의 합을 계산하여 반환하는 예제이다. 클로저를 사용하는 함수는 printResult이다.

이 printResult에서는 함수와 Int 값을 인수로 전달된다. 함수의 형태는 (num:Int)->Int으로 지정되어 있다. 이것으로 calc 함수를 인수로 전달하여 호출하면 내부적으로 함수를 실행하고 결과가 출력되는 거다.

이 printResult은 단순히 "calc의 결과를 출력한다"는 것은 아니다. 인수 전달은 calc가 아니어도 상관없다. Int 값을 인수로 전달 Int 값을 반환하는 함수라면 무엇이든 전달해서 사용할 수 있다.

즉, 클로저를 사용하면 "처리를 외부로 분리할 수 있다"라는 장점이 있다. 이것이 클로저를 사용하는 큰 장점이다.




함수 리터럴

클로저를 사용할 때 함수를 값으로 전달해야 한다. 앞의 예제처럼 함수를 미리 정의 해두고, 그것을 인수로 지정해도 물론 가능하다.

하지만 너무 복잡한 처리가 아니라면, 그리고 한 곳에서만 사용하고, 다른 곳에서 쓸 수도 없다면 일부러 함수를 정의하는 것이 귀찮을 수도 있다.

이러한 경우 클로저를 사용한 함수를 사용할 때 함수를 인수로 그 곳에 직접 작성할 수도 있다. 이는 "함수 리터럴"형식으로 작성한다. 함수 리터럴의 작성은 매우 간단하다.

{... 처리 작성 ...}

이것뿐이다. 사실은 Swift에서는 {} 내에 처리를 작한 것은 모든 함수로 취급한다. 다만, 이것만을 인수의 값으로 전달할 수 없기 때문에, 실제로는 다음과 같이 쓰는 경우가 많을 것이다.

{인수 in ... 처리 작성 ...}

인수가 여러 개인 경우 쉼표로 구분하여 작성한다. 이와 같이 함수 리터럴을 사용하여 함수의 값을 직접 인수를 작성하면, 미리 필요한 함수를 정의해 두지 않아도 된다.

아래에 그 이용 예제를 보도록 하자.

func printResult(function:(num:Int)->Int, n:Int) {
    print(function(num: n))
}

printResult({n in n * 2}, n: 10)

printResult({n in
    var re:Int = 0
    for num in 0...n {
        re += num
    }
    return re
}, n: 100)

여기에서는 이전 printResult을 정의해서 그것을 호출 처리로 쓰고 있다. 보면 알 수 있듯이, calc 함수는 더 이상 사용하지 않는다. 그 대신에, printResult를 호출할 때 함수 리터럴로 처리를 직접 작성하고 있다.

함수 리터럴은 단순히 하나의 계산식만으로 결과를 얻을 수 있는 경우는 한줄에 함께 쓸 수 있다. in 후에 단지 간단한 식을 쓰는 것만으로 끝난다.

여러 행을 전달하는 처리가 되면 나름대로 복잡하게 되지만, 한줄의 함수 리터럴이라면 간단히 사용하기 쉽다는 것을 알 수 있다.

클로저와 함수 리터럴에 익숙해지지 않으면 왠지 어려울 것 보일 수도 있지만, "함수를 값으로 처리하는 구조다"라는 것만 기억한다면 그렇게 복잡한 것은 아니다. 실제로 샘플을 여러 번 수정해 가면서 실행해 보면 금방 익숙해 질 것이다.

'Swift' 카테고리의 다른 글

[Swift] 프로토콜 및 확장  (0) 2017.12.10
[Swift] 함수 리터럴 및 클로저  (0) 2017.12.10
[Switf] 구조체, 열거형, 튜플  (0) 2017.12.09
[Swift] 배열과 사전  (0) 2017.12.09
[Swift] 클래스 기본  (0) 2017.12.09
[Swift] 함수  (0) 2017.12.09

Swift는 복잡한 구조를 가진 값이 많이 등장한다. 그 중에서, 구조체, 열거형(enum), 튜플(Tuple) 같은 것에 대해 설명한다.


구조체(struct)

Swift는 클래스나 배열 등의 것 외에도 복잡한 성질을 가진 값이 있다. 이 대해 정리해 보겠다. 우선 "구조"부터 설명하겠다.

구조체는 값을 저장한 속성과 처리를 구현하는 메소드이다. 구조체를 이용하려면 먼저 정의를 작성하고 이를 바탕으로 인스턴스를 생성한다. ~라고하면 "어? 클래스와 같은 아니야?"라고 생각할지도 모르겠다.

분명히, 기본적인 구조와 구성는 클래스와 거의 동일하고, 속성과 메소드의 처리도 전혀 차이가 없자만, 섬세한 점에서 차이가 있다.

정의 struct를 사용

클래스의 정의는 "class 이름"형태로 작성되지만, 구조체의 경우 "struct 이름"이다. 속성과 메소드 작성은 클래스와 동일하다.

struct 이름 {
    ...... 속성, 메서드의 작성 ......
}

상속되지 않는다.

구조체는 클래스와 같은 객체 지향 구조를 가지고 있지 않는다. 어디까지나 "값과 처리를 하나로 묶기 위한 것"이며, 계승하여 새로운 구조체를 만들거나 할 수 없다.

이니셜라이저 필요 없다.

클래스는 인자가 없는 기본 이니셜라이저는 자동으로 제공되지만, 속성 값을 인수 가진 이니셜라이저 등은 추가 작성하지 않으면 안되지만, 이 구조체는 속성을 포함하면 그 값을 인수로 가지는 이니셜라이저가 자동으로 생성된다.

참조는 않는다.

이것은 중요하다. 클래스의 경우, 인스턴스를 작성하면 해당 참조가 변수에 할당되었다. 인스턴스는 메모리의 어딘가에 보관되어 그 위치를 나타내는 값(참조)을 변수에 넣고 조작한다. 따라서 인스턴스가 포함된 변수를 다른 변수에 할당한 경우에도 참조가 복사 될 뿐이므로, 두 변수도 동일한 인스턴스를 참조한다.

구조체는 이런 참조를 하지 않는다. 따라서 인스턴스를 할당한 변수를 반면에 변수에 할당하면 구조 자체가 복사되어 두 변수는 서로 다른 인스턴스를 갖게 된다. 이 차이는 확실히 알고 있어야 한다.

아래에 간단한 구조의 이용 예제를 보도록 하자.

struct MyData {
    var age:Int
    var name:String
     
    func getData() ->String {
        return "[\(name)(\(age))]"
    }
}
 
var data = MyData(age: 99, name: "Taro")
var data2 = data
data2.name = "Hanako"
data2.age = 24
println(data.getData())
println(data2.getData())

여기에서는 MyData 구조를 만들고 그 인스턴스를 만들어 사용하고 있다. 인스턴스 생성은 MyData(age : 99, name : "Taro")으로 하고 있지만, 이를 위한 이니셜라이저는 없다. Swift가 자동으로 이니셜라이저를 제공 해 주고 있는 것을 알 수 있다.

또한, 작성한 인스턴스 data를 그대로 data2에 대입하고 있지만, 속성 값을 변경하여 출력시켜 보면, 양자가 각각 별도의 인스턴스를 보유하고 있는 것을 알 수 있다.



열거형(enum)

enum은 한국어로 '열거형'라는 한다. 이는 여러 값 중 하나를 선택하는 경우에 사용된다.

이 enum은 "enum"라는 키워드를 사용하여 정의한다. 다음과 같은 형태이다.

enum 이름 {
    case1
    case2
    ...... 중략 ......
}

enum 후에 이름을 지정한다. 그리고 {} 내에 case를 사용하여 준비하는 값을 작성한다. 또는 case 후에 여러 값을 함께 쓸 수도 있다. 모두 동작은 동일하다.

enum 이름 {
    case1, 값2, ...
}

이렇게 생성된 enum은 '이름, 값'이라는 식으로 하여 값을 지정 사용할 수 있다.

enum은 "기본값"을 지정할 수 있다.

enum 이름 : 유형 {
    case1 = 기본값
    case2 = 기본값
    ......
}

클래스의 상속과 같은 느낌으로, enum의 이름 뒤에 기본값의 형식을 지정한다. 그리고 각각의 값 뒤에 등호(=)로 기본값을 할당한다. 이렇게 생성된 enum 값은 "rawValue"라는 속성을 호출하는 것으로 기본값을 얻을 수 있다.

그럼 실제 사용 예제를 보도록 하자.

enum Janken {
    case Choki
        case Goo
    case Paa
}

enum 가위바위보 : String {
    case Choki = "가위"
    case Goo = "바위"
    case Paa = "보"
}

var me = Janken.Goo
var you = 가위바위보.Goo
print(me)
print(you.rawValue)

위에 Janken과 가위바위보에 두 가지의 enum을 정의하고 사용하고 있다. 가위바위보에서는 각 값에 String의 기본값을 지정하고 있다.

Janken.Goo를 print하면 Goo라고 표시되지만, 가위바위보.Goo는 기본값인 "바위"가 표시된다.



튜플(Tuple)

튜플은 이미 여러번 등장 했었다. 종류가 다른 여러 값을 한곳에 모아 처리 할 수 있는 것이다. 이것은 다음과 같이 작성한다.

(값1, 값2, ...)

이와 같이, 단지 값을 () 안에 쉼표(,)로 구분하여 써 나가는 것만으로 튜플을 만들 수 있다. 이 안에 있는 값은 "〇〇.1"과 같이 튜플의 값을 저장한 변수 뒤에 점으로 번호를 지정하여 얻을 수 있다.

그러나 번호 지정 방식은 제대로 값 순서를 이해하고 있지 않으면 실수도 있다. 그래서 튜플에는 사전처럼 키 이름을 붙여 두는 것도 가능하게 되어 있다.

(키1 : 값1, 키2 : 값2, ...)

이렇게 작성된 튜플은 "〇〇.키"와 같이 숫자가 아닌 키를 사용하여 값을 꺼낼 수 있다. 이것은 단지 배열과 사전의 차이 같은 느낌 생각하면 좋을 것이다.

그럼 아래에 튜플도 사용 예제를 보도록 하자.

func MakeTuple(name:String, age:Int)->(name:String, age:Int) {
    return (name:name, age:age)
}

var me = MakeTuple("Yamada", age: 99)
var you = MakeTuple("Hanako", age: 36)

print(me.name)
print(you.age)

위 예제는 정해진 형식의 튜플을 만드는 기능을 제공하고, 이를 이용하여 튜플을 만들어 값을 표시하는 예제이다.

튜플은 원하는대로 값 구성을 만들 수 있기 때문에, 너무 자유로워서 반대로 내용이 뿔뿔이 흩어지고 질 수도 있는 문제도 있지만, 이런 식으로하면 같은 형식의 튜플을 만들 수 있다.


'Swift' 카테고리의 다른 글

[Swift] 프로토콜 및 확장  (0) 2017.12.10
[Swift] 함수 리터럴 및 클로저  (0) 2017.12.10
[Switf] 구조체, 열거형, 튜플  (0) 2017.12.09
[Swift] 배열과 사전  (0) 2017.12.09
[Swift] 클래스 기본  (0) 2017.12.09
[Swift] 함수  (0) 2017.12.09

다수의 값을 함께 관리하는 것이 '배열'과 '사전(Dictionary) "이다. 이에 대해 기본 작업에 대해 설명한다.


배열의 기본 사항

여러 값을 한꺼번에 관리하는 데 사용되는 것이 '배열'이다. 배열은 동일한 유형의 값을 다수 정리하여 보관한다. 배열에는 여러 값을 저장할 수 있는 장소가 준비되어 있어, 이를 0부터 시작되는 일련 번호(인덱스 번호)가 붙여진다. 이를 이용하여 특정 번호의 값을 얻거나, 다른 값을 저장하거나 하는 것이 가능하다.

배열은 최초에 "몇 개의 값을 저장할지"를 지정해서 생성한다. 예를 들어 "10개 저장할 수 있는 배열"을 만들면, 인덱스 번호 0~9의 저장고(1~10가 아니다는 것을 주의하자!)에 값을 저장할 수 있다. 저장고가 확보되지 않은 번호를 지정하여 값을 읽거나 쓰려고 하면 오류가 발생한다.

그럼 배열의 기본적인 사용법을 간단하게 정리해 보겠다.

배열 만들기

var 변수:유형 = [유형] ()
var 변수:유형 = [유형](count : 수, repeatedValue : 값)
var 변수:유형 = [값 1, 값 2, ...]

값 읽고 쓰기

변수 = 배열[번호]
배열[번호] = 값

배열은 유형으로 유형명의 앞뒤를 [] 기호로 묶는다. 예를 들어 String의 값을 저장하는 배열이라면, [String]과 같이 유형을 지정한다.

배열의 생성은 유형 이렇게 해서 만들 수 있다. 그러나 이것으로 만들어지는 것은 빈공간의 배열이므로 일반적으로 제공하는 보관 저장소의 수와 초기 값의 값을 지정하여

[유형] (count : 준비하는 보관 장소의 수, repeatedValue : 초기 값)

이런 상태로 지정해 작성한다. 또는, [값1, 값2, ...]과 같이 배열을 리터럴로 작성하여 만들 수 있다. 예를 들어, 다음의 2개는 같은 배열이 된다.

var arr1 : [Int] = [Int] (count : 10, repeatedValue : 0)
var arr2 : [Int] = [0,0,0,0,0,0,0,0,0,0]

이 정도의 요소 수인 경우에는 리터럴으로 작성하는 것이 편리할 것이다. 그러나 예를 들어 "저장할 수 있는 요소의 수가 1만개"라고 경우에는 [0,0,0 ......]와 같이 하는 것이 불가능하기 때문에, (count : 10000, repeatedValue : 0)로 작성하는 것이 간단하다.

또한 Swift의 특징으로, 대입하는 값에서 유형이 명확하게 알 수 있다면 변수 선언에 있는 유형은 생략할 수 있어서, 위의 문장은 다음과 같이 작성해도 된다.

var arr1 = [Int] (count : 10, repeatedValue : 0)
var arr2 = [0,0,0,0,0,0,0,0,0,0]



배열과 for-in 구문

배열은 인덱스 번호를 지정하여 하나씩 꺼내 처리할 수 있지만, 데이터 관리 등에 이용하는 경우에는 모든 요소를 동일하게 처리하는 작업이 필요하다.

이러한 경우에 사용되는 것이 for-in 구문인데, 아래와 같은 방식이다.

for 변수 in 배열 {
    ...... 수행할 처리 ......
}

for-in는 배열에서 순서대로 값을 꺼내 변수에 할당하고, 그 후에 {} 부분을 실행하는 것을 반복하고 있다. 이 {} 안에 변수를 사용하는 형태로 작업을 준비하면 배열의 모든 요소에 대해 동일한 작업을 할 수 있다.

아래에 간단한 예제를 보도록 하자.

let data:[Int] = [10, 20, 30]
var total:Int = 0
for num in data {
    total += num
}
println("total: \(total)")

배열 data의 값을 모두 꺼내서 합계하여 표시하는 것이다. for num in data라고 해서 data부터 순서대로 값을 num으로 꺼내 처리하고 있는 것을 알 수 있다.




배열 요소 작업

배열은 만들 때 요소 수를 지정하면 나중에 보관할 수 있는 저장고를 늘리거나 줄일 수 없다. ......라고 하는 것은 옛날 이야기이다. Swift는 나중에 배열의 요소를 변경할 수 있다. 그것은 배열 메소드을 이용한다.

메소드? 그렇다 배열도 사실 오브젝트이다. 그래서 그 안에 포함되어 있는 속성과 메소드를 호출해 처리할 수 있다. 추가로, 연산자 덧셈하기 가능하다.

배열끼리 1개로 정리

변수 = 배열 + 배열

배열의 끝에 값을 추가

배열.append(값)

지정된 위치에 값을 삽입

배열.insert(값, atIndex : 삽입 위치)

마지막 항목을 삭제

배열.removeLast()

지정된 인덱스 번호의 값을 삭제

배열.removeAtIndex(번호)

지정한 범위의 값을 제거

배열.removeRange(레인지)

모든 삭제

배열.removeAll()

현재의 요소 수 얻기

변수:Int = 배열.count

첫 번째 요소 · 마지막 요소 얻기

변수 = 배열.first
변수 = 배열.last

지정 위치에 값을 삽입하는 insert는 atIndex에 삽입 위치를 나타내는 정수를 지정한다. 첫번째 앞(인덱스 0의 요소 앞)이 0, 인덱스 0과 1사이가 1, ......라는 식으로 지정된다.

또한 지정 범위의 값을 삭제하는 removeRange와 제거하는 요소의 인덱스 번호를 사용하여 범위를 제공한다. 예를 들면, removeRange(3 ... 5)이라면, 인덱스 번호 3~5의 요소를 제거한다.

우선, 여기 메소드와 속성이 대충 사용할 수 있게 되면, 배열도 자유 자재로 다룰 수 있게 될 것이다.



사전(Dictionary)

배열은 단순히 값을 순서대로 정렬하여 관리하는 것만으로, 저장된 값을 인덱스 숫자로 이용해서 꺼냈다. 때로는, 이 숫자보다는 이름으로 관리하는 편이 좋은 경우도 있다.

그런 경우에 사용되는 것이 "사전 (Dictionary)"이다. 사전은 "키"라는 값을 사용하여 관리한다. 각각의 값은 인덱스 번호 대신에 키가 붙여져 있고, 키를 사용하여 값을 제거하거나 변경하는 것이다.

사전 만들기

var 변수 : [유형 : 유형] = [유형 : 유형]()
var 변수 : [유형 : 유형] = [키1 : 값1, 키2 : 값2, ...]

값에 액세스

변수 = 사전 [키]
사전 다음 키 = 값

for-in 의한 반복

for (변수1, 변수2) in 사전 {
    ...... 반복 처리 ......
}

사전은 []로 두 가지 데이터 유형을 지정한다. 이는 키와 값의 유형을 지정한다. 예를 들어, String의 키에 Int 형의 값을 보관한다면, [String : Int]와 같이 지정한다. 그대로, [String : Int]()를 변수에 할당하면 값이 아무것도 없는 공백의 사전이 만들어 진다.

사전는 배열과 달리, 최초에 정해진 수의 저장 공간를 준비할 필요는 없다. 키를 사용하여 값을 저장하 면, 그 키가 사전에 추가된다(기존에 같은 키가 있으면, 그 값이 변경된다).

for-in는, 배열과 사용 방법이 조금 다르다. for 후에 (변수1, 변수2)와 같이 튜플 변수를 제공한다. 이렇게하면 사전에서 검색한 요소의 키와 값을 각각의 변수에 할당한다.

아래에 실제 사용 예제를 보도록 하자.

let data:[String:Int] = ["국어":98,"수학":76,"영어":54]
var total = 0
for (key, val) in data {
    total += val
    println("add \(key)")
}
println("Total: \(total)")

3과목의 점수를 사전에 모와서, 그것로 부터 요소를 꺼내서 합계를 계산한다. 실행하면 값을 꺼낼 때마다 "add 〇〇"과 키가 출력되고, 마지막으로 총 합계가 표시된다.


사전을 활용하는 방법

사전도 배열과 같은 개체이므로 속성과 메소드를 가지고 있다. 이 메소드들은 호출하여 사전의 내용을 조작할 수 있다. 그러나 배열과 달리 사전은 키를 지정하여 간단히 새로운 값을 추가할 수 있으므로 값의 삽입하는 메소드는 없다.

지정된 키 값을 삭제

사전.removeValueForKey(키)

모든 값을 삭제

사전.removeAll()

키 값을 모두 얻기

변수 = 사전.keys

보관되어 있는 값을 모두 얻기

변수 = 사전.values

요소 수를 얻기

변수 : Int = 사전.count

이들 중 좀 유심히 봐야 하는 것은 "keys"와"values"이다. 이 두개는 키와 값을 함께 저장하는 속성이다. 이 두개의 반환 값은 LazyBidirectionalCollection라는 생소한 컬렉션 클래스의 인스턴스인데, 이것은 그대로 for-in에서 차례로 값을 얻을 수 있다.

아래에 사전과 관련되 샘플을 보도록 하겠다.

var data:[String:Int] = ["국어":98,"수학":76,"영어":54]
let keys = data.keys
let vals = data.values
for key in keys {
    println(key)
}
for val in vals {
    println(val)
}

사전에서 keys와 values에서 각 값을 얻어서, 그 값들 for-in으로 출력한다. 이로 보관된 키와 값이 표시되는 것을 확인할 수 있다.

'Swift' 카테고리의 다른 글

[Swift] 함수 리터럴 및 클로저  (0) 2017.12.10
[Switf] 구조체, 열거형, 튜플  (0) 2017.12.09
[Swift] 배열과 사전  (0) 2017.12.09
[Swift] 클래스 기본  (0) 2017.12.09
[Swift] 함수  (0) 2017.12.09
[Swift] 제어 구문  (0) 2017.12.09

객체 지향 언어에서는 '클래스'를 정의하여 객체를 만들고 사용할 수 있다. 이 "클래스"의 기본적인 사용법에 대해 알아보겠다. 그리고, 클래스의 특성을 더욱 이해하기 위해 "상속", "오버라이드", "Computed 속성", "클래스 속성/클래스 메소드"에 대해서도 설명한다.


클래스(class) 정의

Swift는 객체 지향 언어이다. 이는 물론 객체(object)를 사용할 수 있다는 의미가 된다.

Swift의 객체 지향은 일반적으로 "클래스 기반"라는 것이다. "클래스"는 객체의 설계도에 해당하는 것을 정의 해두고,이를 바탕으로 개체를 만든다.

클래스에서 생성된 객체를 "인스턴스"라고 한다. Swift는 정의된 클래스에서 실제로 작업할 수 있는 인스턴스를 만들어 사용한다는 것이다.

클래스에는 값을 저장하는 변수와 처리를 하는 함수를 넣어 둘 수 있다. 클래스에 제공되는 변수를 "속성(property)", 클래스에서 제공되는 함수를 "메소드(method)"라고 한다.

클래스의 정의

클래스 정의의 가장 간단한 형태는 다음과 같다.

class 클래스명 {
    ...... 속성과 메소드를 작성한다 ......
}

"class 〇〇"라고 작성한 후에 {}를 붙여 그 안에 클래스에 속성과 메소드를 작성하는 형태이다.

아래에 "Helo"클래스의 정의를 보도록 하자.

import Cocoa

class Helo {
    var name = "Taro";
    
    func say(){
        print("Hello, " + name + "!");
    }
}

이 클래스에는 name이라는 속성과 say라는 메소드가 작성되어 있다. 이것들이 Helo 클래스에 제공되는 기능이라는 것이다.

say 메소드 안에 name 속성이 사용되고 있다. 이런 식으로 클래스에 있는 메소드의 내부는 그 클래스에 있는 속성과 메소드를 그대로 사용할 수 있도록 되어 있다.

어시스트 에디터(Assistant Editer)

여기에는 println이라는 것을 사용하고 있는데, 이것은 값을 표준 출력으로 내보내는 함수이다. Xcode의 플레이 그라운드에서 실행하는 경우, "어시스트 에디터(Assistant Editer)"라는 것을 표시하는 것이 편리하다.

Xcode의 [View] 메뉴에서 [Assistant Editor]-[Show Assistant Editor]를 선택한다다. 화면 오른쪽에 새로운 영역이 표시된다. 이것이 "어시스트 에디터"이다. println 등이 실행되면 여기에 "Console Output"라는 항목이 자동으로 표시되고 println한 결과가 나타나게 된다.


클래스 이용

클래스를 실제로 사용하기 위해서는 해당 클래스를 바탕으로 "인스턴스"를 만들고 그 안에 속성과 메소드를 이용하는 방법을 이해하지 않으면 안된다.

인스턴스 생성

클래스에서 인스턴스를 생성하려면 해당 클래스명을 함수처럼 호출할 뿐이다. 예를 들어, "Abc"라고 하는클래스가 있었다면, 다음과 같이 인스턴스를 생성할 수 있다.

var obj:Abc = Abc();

인수에는 일반적으로 아무것도 지정하지 않다 (인스턴스 작성 시에 인수를 지정할 수 있다. 이것은 나중에 설명하겠다). 이런 식으로 클래스명의 함수를 호출하여 인스턴스를 생성할 수 있다.

속성 / 메소드 호출

속성과 메소드의 호출은 인스턴스를 얻은 변수 뒤에 점(.)으로 속성 및 메서드를 호출하도록 작성한다. 예를 들어, 클래스 Abc에 efg라는 속성 xyz라는 메소드가 있었다고 하면 이런 식으로 사용할 수 있다.

var obj:Abc = Abc();
var x = obj.efg; // efg 값을 추출한다
obj.efg = 〇〇; // efg 값을 변경한다
obj.xyz(); // xyz를 호출한다

이전에 Helo 클래스를 사용하는 예제를 보도록 하자.

import Cocoa

class Helo {
    var name = "Taro";
    
    func say(){
        print("Hello, " + name + "!");
    }
}

var obj:Helo = Helo();
obj.say();
 
obj.name = "Hanako";
obj.say();

여기에서는 Helo 인스턴스를 만들어 say 메소드로 메시지를 표시한다. 그 후에 인스턴스의 name 속성을 다시 변경하여 say를 호출한다.

인스턴스를 생성할때, 그 안에 클래스의 내용이 통째로 제공된다. 이는 여러 인스턴스를 만들 때마다 각 인스턴스마다 독립적으로 속성이 저장되는 것을 의미한다. 예를 들어, 이 예제라면 인스턴스마다 name 값은 다르게 설정할 수 있다. 그렇게 되면, 같은 Helo 클래스에서 만든 인스턴스이기에 say로 표시되는 메시지는 인스턴스마다 달라진다.


클래스 상속

클래스를 사용하면 무엇이 유용한 것인가. 관계가 있는 함수나 변수를 한곳에 모아 넣을 수 있다는 것도 있지만, 그 이상으로 큰 것은 "프로그램의 재사용을 쉽게 할 수있다"라는 것이다. 클래스를 복사하면 어디서나 동일한 기능을 사용할 수 있게 된다.

이 "클래스의 재사용"라는 것을 크게 확장하는 것이 "상속"라고 하는 기능이다. 상속은 이미 클래스의 모든 기능을 이어 받아 새로운 클래스를 정의하는 것이다. 즉, 이미 편리한 클래스가 있으면, 그것을 계승하여 자신의 고유한 기능을 덧붙여서 새로운 클래스를 쉽게 만들 수 있다.

상속을 사용한 클래스의 정의는 아래와 같은 식으로 작성한다.

class 클래스명:슈퍼 클래스명 {
    ...... 클래스의 내용 ......
}

상속 근원이 되는 클래스를 "슈퍼 클래스"라고 한다. 또한 그것을 계승하여 만든 클래스를 "서브 클래스"라고 한다. 서브 클래스는 슈퍼 클래스의 기능을 모두 사용할 수 있게 된다.

실제로 상속을 사용해 보자.

import Cocoa

class Helo {
    var name:String = "Taro";
    
    func say(){
        print("Hello, " + name + "!");
    }
}

class Hello:Helo {
    var name2:String = "YAMADA";
    
    func say2(){
        print("Hi," + name + "-" + name2 + "!");
    }
}

var obj:Hello = Hello();
obj.say();

obj.name = "Hanako";
obj.name2 = "TANAKA";
obj.say2();

위에 예제은 Helo 클래스를 상속한 'Hello'클래스를 만들어 사용하고 있다. Hello 클래스의 인스턴스에는 Hello에 추가한 name2 속성과 say2 메소드를 사용할 뿐만 아니라, 슈퍼 클래스의 Helo의 name이나 say도 사용할 수 있다.

Hello 클래스에 추가한 say2 메소드를 보면, 그 속에서 슈퍼 클래스에 있는 name 속성을 사용하는 것을 알았다. 이런 식으로 슈퍼 클래스에 있는 것은 모두 그 클래스에 있는 것으로 간주하고 사용할 수 있다.

슈퍼 클래스의 메소드 호출

아래에 또 다른 상속 샘플을 보도록 하자.

class Friend {
    var name:String
    
    init(name:String) {
        self.name = name
    }
}

class BestFriend : Friend {
    var age:Int
    
    init(name:String, age:Int) {
        self.age = age
        super.init(name:name)
    }
    
}

var you = BestFriend(name: "Taro", age: 30)
print("\(you.name) (\(you.age))")

여기에서는 Friend 클래스를 상속해서 BestFriend 클래스를 만들고 있다. 보면 알 수 있듯이, BestFriend에는 name 속성은 없지만 문제없이 동작한다. 슈퍼 클래스의 Friend에 name이 있기 때문에 BestFriend에도 작동하는 것이다.

BestFriend의 이니셜라이저에는 "super.init(name:name)"라는 문장이 있는데, 이 "super"는 슈퍼 클래스를 나타내는 키워드이다. 이렇게 하여 슈퍼 클래스인 Friend의 이니셜라이저를 호출한다. 슈퍼 클래스의 메소드는 모두 이처럼 'super.〇〇'라는 형태로 호출 할 수 있다.



메소드 오버라이드(override)

상속을 이용하면 이미 있는 클래스에 새로운 기능을 추가할 수 있는 것은 알았다. 하지만 그것만으로는 부족한다. 이미 있는 클래스의 기능을 변경할 수 있어야 한다.

여기에는 '오버라이드(override)'라는 기능을 사용한다. 오버라이드는 슈퍼 클래스에 있는 메소드를 재정의하는 기술이다. 이것은 메소드의 정의를 다음과 같이 작성하면 된다.

override func 메소드명() {...}

처음에 override를 붙이는 것으로, 슈퍼 클래스의 메소드를 새롭게 정의된 메소드로 덧쓰기를 한다. 이렇게 하면, 그 메소드를 호출했을 때의 동작을 변경할 수 있다.

오버라이드를 사용하는 경우, 메소드명뿐만 아니라 인수와 반환 값도 정확하게 슈퍼 클래스의 메소드와 동일해야 한다.

아래에 실제 사용 예제를 보도록 하자.

import Cocoa

class Helo {
    var name:String = "Taro";
    
    func say(){
        print("Hello, " + name + "!");
    }
}

class Hello:Helo {
    var name2:String = "YAMADA";
    
    override func say(){
        print("Hi," + name + "-" + name2 + "!");
    }
}

var obj:Hello = Hello();
obj.say();

obj.name = "Hanako";
obj.name2 = "TANAKA";
obj.say();

이것은 이전에 언급한 예제를 이용하여 say 메서드를 재정의하는 형태로 변경한 것이다. say 메소드를 호출하면 Helo 클래스에있는 say 대신에 Hello에서 재정의되는 say가 호출되는 것을 확인할 수 있다.

오버라이드 상세

상속을 이용하면 슈퍼 클래스의 속성과 메소드를 그대로 사용할 수 있다. 예를 들어 슈퍼 클래스의 기능을 서브 클래스에서 변경하려는 생각도 있을 것이다. 그럴 때 사용되는 것이 '오버라이드'이다.

오버라이드는 슈퍼 클래스에 있는 기능을 서브 클래스에서 덮어 변경하는 것이다. 예를 들어 슈퍼 클래스 'A'라는 메소드가 있었을 때, 그 서브 클래스에서 "A"를 호출하면 슈퍼 클래스에 있는 A라는 메소드가 그대로 실행된다. 상속을 했기 때문이다.

그럼, 서브 클래스에 똑같은 'A'라는 메소드가 있다면 어떻게 될까? 그러면 서브 클래스에서 "A"를 호출하면, 서브 클래스에 추가한 A 메소드가 호출하고, 슈퍼 클래스에 있는 A는 더 이상 호출되지 않을 것이다. 이것이 오버라이드 개념이다.

오버라이드하는 메서드는 메서드의 시작 부분에 'override'라는 수식자를 지정한다. 또한 메소드명, 인수 , 반환 값은 모두 동일해야 한다. 어떤 거라도 다르면, 그것은 "다른 메소드"라고 판단되어서 오버라이드가 되지 않는다.

아래에 또 다른 오버라이드 예제를 보도록 하자.

class Friend {
    var name:String
     
    init(name:String) {
        self.name = name
    }
     
    func printData() {
        println("\(self.name)")
    }
}
 
class BestFriend : Friend {
    var age:Int
     
    init(name:String, age:Int) {
        self.age = age
        super.init(name:name)
    }
     
    override func printData() {
        println("\(self.name) (\(you.age))")
    }
}
 
var you = BestFriend(name: "Taro", age: 30)
you.printData()

여기에서는 printData라는 메소드를 하위 클래스에서 오버라이드 한다.


이니셜라이저

여기까지 인스턴스 생성은 모든 Helo();와 같이 인수없이 만들었다. 이 인스턴스를 만들 때 필요한 값 등을 인수로 전달할 수 있다면 매우 편리할 것이다.

이런 때에 이용되는 것이 "이니셜라이저(initialize)"이다. 이것은 인스턴스를 만들 때 자동으로 호출되는 초기화 처리 전용의 메소드이다. 이 이니셜라이저는 다음과 같이 정의한다.

init (인수) {
    ...... 초기화 처리 ......
}

func init로 쓸 필요는 없다. 단, init만 작성하면 된다. 이니셜라이저는 특별한 방법이므로 func 정의할 필요가 없는 거다.

이 init의 ()에 인수를 선언하면 인스턴스 생성시에 값을 전달할 수 있게된다. 아래에 간단한 예제를 보도록 하자.

import Cocoa

class Helo {
    var name:String;
    
    init(name:String){
        self.name = name;
    }
    
    func say(){
        print("Hello, " + name + "!");
    }
}

var obj:Helo = Helo(name:"Taro");
obj.say();

여기에서는 Helo 클래스에서 init(#name:String) 형태로 이니셜라이저가 작성되어 있다. 이렇게 하면 인스턴스 생성시에는 Helo(name:"Taro");와 같이 인수를 지정하여 쓰면 된다.

또한, 이 이니셜라이저에서 인수로 전달된 변수 name을 Helo 클래스의 name 속성에 할당하기 위해, "self.name"라고 작성을 하고 있다. 이 "self"는 인스턴스 자신을 나타내는 특별한 값이다. 이와 같이 작성하는 것으로, "이 인스턴스 자신의 name 속성"을 지정하고 있다. 클래스 정의에서 자주 사용하는 것이므로 모두 기억하도록 하자.



Computed 속성

상속 외에도 이것은 기억 두지 않으면 안되는 기능이 몇 가지 있다. 그것에 대해서도 설명하겠다. 먼저 "Computed 속성"을 설명하겠다.

속성이라는 것은 클래스에 값을 보관할 변수이다. 이것은 그대로 값을 교체할 수 있기 때문에, 어떤 값이 될지 모른다. 여기서 값의 입출력을 프로그래밍으로 제어할 수 있도록 한 것이 Computed 속성이다.

Computed 속성은 다음과 같이 작성한다.

var 속성 : 유형 {
    get {
        ...... 처리 ......
        return 값
    }
    set {
        ...... 처리 ......
    }
}

속성의 선언 뒤에 {}가 있고, 그 안에 get 및 set이라는 것이 작성하고, 여기에서 값을 읽고 쓰기 위한 처리를 작성한다. get만 작성하고 set를 작성하지 않으면, 값을 얻기만 가능하고 수정할 수 없는 속성이 된다. 반대로 set만 작성하면, 값 변경만 가능하고 얻을 수 없는 속성이 된다.

아래 Computed 속성의 사용 예제를 보도록 하자.

class Friend {
    var name:String
    
    var old:Int
    
    var age:Int {
        get {
            return old
        }
        set {
            if newValue > 0 {
                old = newValue
            }
        }
    }
    
    init(name:String, age:Int) {
        self.name = name
        self.old = age
    }
    
    func printData() {
        print("\(self.name) (\(age))")
    }
}

var you = Friend(name: "Taro", age: 30)
you.printData()

여기에서는 age라는 Computed 속성을 사용하고 있다. 실제 값은 old라는 속성에 보관하고 있다. Computed 속성은 이 처럼 실제 값을 보관해 두기 위한 장소를 따로 마련할 필요가 있는 것이다.



유형 속성 및 유형 메소드

일반적 속성과 메서드는 인스턴스를 생성해야만 사용할 수 있다만, 클래스에서 바로 직접 이용할 수 있는 속성과 메소드도 제공할 수 있다. 이는 "유형 속성(type property)", "유형 메소드(type method)"라고 한다.

유형 속성과 유형 메소드를 선언할 때는 앞에 "class"라는 수식자를 붙여 클래스에 배치된다. 그러면 클래스에서 직접 호출될 수 있다.

유형 속성 및 유형 메소드를 사용할 때 몇 가지 주의해야 할 점이 있다.

  • 유형 속성으로 만들 수 있는 것은 "Computed 속성"이다. 보통의 변수에 할당하는 속성은 제공되지 않는다. 또한 클래스에 저장할 변수를 제공되지 않기 때문 때문에, Computed 속성의 set도 거의 사용할 수 없다고 생각해도 좋을 것이다.

  • 유형 메소드 내에서 이용할 수 있는 것은 유형 메소드 및 유형 속성뿐이다. 일반 (인스턴스에서 사용하는) 속성과 메소드는 사용할 수 없다.

아래에 실제 사용 예를 보도록 하자.

class Exchange {
    class var rate:Double {
        return 1005.0
    }
    
    class func DollarToWon(d:Double)->Int {
        return Int(d * rate)
    }
    
    class func WonToDollar(y:Int)->Double {
        return Double((y * 100) / Int(rate)) / 100
    }
}

print(Exchange.DollarToWon(1.5))
print(Exchange.WonToDollar(1500))

여기에서는 원화에서 달러로, 달러에서 원화로 환산하면 Exchange 클래스를 준비했다. rate 유형 속성과 계산을 할 두 가지 유형 메소드를 제공하고 있다. 이러한 계산을 중심으로한 클래스는 일일이 인스턴스를 만들 필요가 없기 때문에, 클래스 메소드로 제공하는 편이 효율적으로 사용할 수 있는 것이다.

'Swift' 카테고리의 다른 글

[Switf] 구조체, 열거형, 튜플  (0) 2017.12.09
[Swift] 배열과 사전  (0) 2017.12.09
[Swift] 클래스 기본  (0) 2017.12.09
[Swift] 함수  (0) 2017.12.09
[Swift] 제어 구문  (0) 2017.12.09
[Swift] 값, 변수, 연산  (0) 2017.12.09

+ Recent posts