Swift Tour ​

A Swift Tour

이 글은 애플의 The Swift Programming Language의 서두에 해당하는 챕터를 간략히 요약했다. 이 책의 웹 버전은은 이 곳에서 확인할 수 있다.

전통에 따라 Hello World를 찍어보자.

println("Hello, world")

스위프트에서 이 코드 한 줄은 그 자체로 완전한 프로그램이다. 별도의 헤더 반입이나 보일러 플레이트가 없다. 전역적으로 작성된 코드는 곧장 프로그램의 진입점(entry point)이 된다. 따라서 별도의 main 함수를 작성할 필요는 없다. 또한, 한 라인에 한 구문씩 작성하는 경우 라인의 끝에는 세미콜론을 따로 붙이지 않아도 된다.

간단한 값

간단한 값은 상수와 변수로 나뉜다. 변수는 var를 통해, 상수는 let을 통해 선언하고 정의한다.

var myVariable = 42
myVariable = 50
let myConstant = 42

변수나 상수는 대입되는 값과 동일한 타입을 가져야 한다. 하지만 이를 항상 명시적으로 쓸 필요는 없다. 컴파일러가 적절한 타입을 추론해준다. 아마도 위 코드에서 컴파일러는 myVariable에 대해 정수형으로 추론할 것이다.

만약 값을 추론할 초기값이 없다면 이는 콜론(:)을 사용하여 타입을 명시할 수 있다.

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70

명시적인 Float타입의 4의 값을 갖는 상수는 다음과 같이 정의한다.

let explicitFloat: Float = 4

값들은 결코 다른 타입으로 암시적으로 변환되지 않는다. 만약, 값의 타입을 변환해야 하면 연산에 앞서 이를 명시적으로 변경해야 한다.

let label = "The width is"
let width = 94
let widthLable = label + String(width)

특별히 문자열 내에 특정한 값을 변환해서 삽입하는 것은 다음과 같이 역슬래시 뒤에 괄호로 감싼 표현식을 쓰는 것으로 좀 더 간단한 코딩을 할 수 있다.

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

이 부분은 아마도 NSString의 stringWithFormat으로 대체되지 않을까 싶다.

대괄호(Bracket)을 사용하여 배열과 사전을 만들 수 있다. (동일한 기호를 사용한다. 중괄호가 아니다.)

var shoppingList = ["catfish", "water", "tulips"]
shoppingList[1] = "bottle of water"

var occupations = [
    "Malcolm": "Captain",
    "Keylee": "Mechanic",
    ]
occupations["Jayne"] = "Pubilc Relations"

빈 배열이나 사전을 만드려면 초기화 메소드를 사용한다.

let emptyArray = String[]()
let emptyDictionary = Dictionary<String, Float>()

만약 타입 정보가 추론 가능하다면, 단순히 [][:]으로도 빈 배열, 사전을 만들 수 있다.

shoppingList = []

흐름 제어

분기를 위해 if, switch 문을 쓸 수 있고, 반복을 위해 for..in, for, while 문과 do-while 문을 쓸 수 있다. 조건식을 괄호로 감싸는 것은 선택이지만, 구문 전체를 중괄호로 감싸는 것은 필수이다.

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
teamScore

암시적인 형 변환이 없기 때문에 if score { ... }라고 쓰는 것은 에러가 된다. (즉 정수타입이 자동으로 Bool 타입으로 변환되어 평가되지 않는다.) 반드시 온전한 표현식으로 평가해야 한다.

iflet을 함께 써서 선택적 타입값을 만들 수 있다. 이 타입의 값은 값을 포함할 수도 있고 nil일 수도 있다. 그리고 해당 타입은 물음표가 덧붙은 형태가 된다.

var optionalString: String? = "Hello"
optionalString == nil

var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
    gretting = "Hello, \(name)"
}

만약 optionalNamenil이었다면 위 조건식은 거짓으로 평가된다. 하지만 선택적 타입의 변수에 값이 있었다면 이는 추출되어 let 구문에서 처리되고 조건식은 참으로 평가된다.

switch 구문은 보다 많은 비교에 적합하다. 이는 단순히 정수뿐만 아니라 여러 가지 테스트를 할 수 있다.

let vagetable = "red pepper"
switch vagetable {
    case "celery":
        let vegetableComment = "Add some raisins and make ants on a log."
    case "cucumber", "watercress":
        let vegetableComment = "That would make a good tea snadwich"
    case let x where x.hasSuffix("pepper"):
        let vegetableComment = "Is it a spicy \(x)?"
    default:
        let vegetableComment = "Everything tests good in soup."
}

for .. in 문은 사전에 대해서도 이터레이션을 할 수 있게 해준다. 다음은 숫자의 배열을 값으로 가지는 어떤 사전에 대해서 가장 큰 숫자 값을 찾는 코드이다.

파이썬과 비슷하다!

let interestingNumbers = [
    "Prime": [2, 3, 5, 7, 11, 13],
    "Fibonacci": [1, 1, 2, 3, 5, 8],
    "Square": [1, 4, 9, 16, 25]
]

var largest = 0
for (kind, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}

largest

while은 설명 생략

var n = 2
while n < 100 {
    n = n * 2
}
n

for 문은 전통적인 C 방식의 구문과 ..을 사용한 범위값 이터레이션을 모두 사용할 수 있다.

var firstForLoop = 0
for i in 0..3 {
    firstForLoop += i
}
firstForLoop

var secondForLoop = 0
for var i = 0; i < 3; i++ {
    secondForLoop += i
}
secondForLoop

함수와 클로저

함수는 func 문으로 선언한다. 리턴타입은 ->을 사용해서 명시해줄 수 있다.

func greet(name: String, day: String) -> String {
    return "Hello \(name), today is \(day)."
}
greet("Bob", "Tuesday")

복수의 값을 리턴하는 함수는 튜플타입을 사용한다.

func getGasPrices() -> (Double, Double, Double) {
    return (3.59, 3.69, 3.79)
}
getGasPrices()

함수는 복수 인자를 받을 수 있는데, 이는 배열로 취급된다.

func sumOf(numbers: Int...) -> Int {
    var sum = 0
    for num in numbers {
        sum += num
    }
    return sum
}

sumOf()
sumOf(42, 587, 12)

함수는 1급 객체(1st Class Citizen)이고, 함수 내부에서 중첩될 수 있다. 중첩된 함수는 부모 함수의 로컬 변수에 접근할 수 있다. 뿐만 아니라 함수는 함수를 리턴할 수도 있다. (데..데코레이터?)

func returnFifteen() -> Int {
    var y = 10
    func add(){
        y += 5
    }
    add()
    return y
}

returnFifteen()

func makeIncrementer() -> (Int -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}

var increment = makeIncrementer()
increment(7)

물론 함수의 인자로, 함수를 넘길 수 있다.

func hasAnyMathces(list: Int[], conditiono:Int->Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}

func lessThanTen(number: Int) -> Bool {
    return number < 10
}

var numbers = [20, 19, 7, 12]
hasAnyMathces(numbers, lessThanTen)

사실 함수는 특별한 형태의 클로저이다. 클로저는 중괄호로 둘러싸인 코드 블럭으로, 파라미터와 리턴타입을 알리는 첫 줄과 in을 사용한다.

numbers.map({
    (number: Int) -> Int in
    let result = 3 * number
    return result
    })

만약 클로저 내에서 파라미터와 리턴값의 타입 추론이 가능하다면 그 마저도 생략하고 다음과 같이 간결하게 쓸 수 있다.

numbers.map({number in return 3 * number})

정렬 predicator 등에서 사용하는 클로저는 좀 더 특별하다. 다음 코드에서는 클로저가 함수의 두 번째 파라미터로 넘겨진다. 이 때 클로저의 파라미터는 이름이 아닌 번호로 참조된다.

sort([1, 5, 3, 12, 2]){$0 > $1}

객체와 클래스

class를 사용하여 클래스를 정의할 수 있다. 클래스 내의 프로퍼티 선언은 상수나 변수 선언과 동일한 방식으로 할 수 있다. 또한 메소드 역시 함수 정의와 같은 식으로 하게된다.

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

위 예제 클래스는 사실 중요한 것을 하나 빼먹었다. 바로 초기화 메소드이다. 초기화 메소드는 기본적으로 init()으로 정해져있다.

class NamedShape {
    var numberOfSides: Int = 0
    var name: String

    init(name: String) {
        self.name = name
    }

    func simpleDescription() -> String {
        return "A Shape with \(numberOfSides) sides."
    }
}

초기화 메소드에서 객체 자신의 name 속성과 초기화 메소드의 파라미터명 name을 구분하기 위해서 self를 사용했음을 주목하자. 모든 프로퍼티는 선언시에 곧장 초기화되거나 초기화 메소드에서 초기값을 받아야 한다.

deinit은 객체가 소멸(deallocate)되기 직전에 호출된다. 만약 자원을 정리해야 한다면 이 메소드도 정의해주자.

서브클래스는 콜론으로 구분하여 수퍼클래스명을 붙여주면 된다. 스위프트에서는 표준 루트 객체 클래스가 없다. 따라서 얼마든지 수퍼클래스를 새롭게 만들어도 좋다.

서브클래스를 작성할 떄, 수퍼클래스의 메소드를 오버라이딩하는 경우에는 override를 명시해주어야 한다.(그렇지 않으면 컴파일 에러)

class Square: NamedShape {
    var sideLength: Double

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 4
    }

    func area() -> Double {
        return sideLength * sideLength
    }

    override func simpleDescription() -> String {
        return "A square with side of length \(sideLength)"
    }
}

let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

프로퍼티의 getter, setter를 다음과 같이 따로 정의해줄 수 있다.

class EquilateralTriangle: NamedShape {
    var sideLength: Double  = 0.0

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }

    var perimeter: Double {
        get {
            return 3.0 * sideLength
        }

        set {
            sideLength = newValue / 3.0
        }
    }

    override func simpleDescription() -> String {
        return "An equilateral triangle with side of length \(sideLength)."
    }
}

var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
triangle.perimeter
triangle.perimeter = 9.9
triangle.sideLength

setter 메소드에서 새로운 값의 기본 이름은 newValue이며 이를 변경하고 싶을 때는 괄호에 써서 정의해주면 된다.

위의 예와 달리 특정 프로퍼티 값을 계산할 필요는 없지만, 프로퍼티를 변경할 때 변경이전, 변경 이후에 특정한 동작을 해야한다면 willSet, didSet을 작성할 수 있다. 예를 들어 아래 클래스는 삼각형의 변과 사각형의 변의 길이가 동시에 변경되도록 하고 있다.

class TriangleAndSquare {
    var triangle: EquilateralTriangle {
        willSet {
            square.sideLength = newValue.sideLength
        }
    }

    var square: Square {
        willSet {
            triangle.sideLength = newValue.sideLength
        }
    }

    init(size: Double, name: String) {
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size, name: name)
    }
}

var triangleAndSquare = TriangleAndSquare(size: 10, name: "Another test shape")
triangleAndSquare.square.sideLength
triangleAndSquare.triangle.sideLength
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
triangleAndSquare.triangle.sideLength

클래스의 메소드는 일반 함수와 큰 차이점이 하나 있다. 함수의 파라미터 이름은 오직 함수내부에서만 쓰인다. 하지만 메소드의 파라미터 이름은 이를 호출할 때도 사용한다. 파라미터 이름에는 두번째 이름을 사용할 수 있다. 두 번째 이름은 메소드 내부에서만 쓰인다.

class Counter {
    var count: Int = 0
    func incrementBy(amount: Int, numberOfTimes times: Int) {
        count += amount * times
    }
}

var counter = Counter()
counter.incrementBy(2, numberOfTimes: 7)

선택적 값 타입을 사용할 때는 ?를 메소드, 프로퍼티, 인덱스등의 앞에 쓸 수 있다. ?의 앞쪽의 값이 nil이면 ?이후의 전체 표현식이 nil이 된다. 만약 선택적 값 내부의 값이 있는 경우에는 이 값으로 표현식이 평가된다.

let optionSquare: Square? = Square(sideLength: 2.5, name: "optional square")

let sideLength = optionSquare?.sideLength

열거와 구조체

enum 키워드를 통해 열거를 만들 수 있다. C와 달리 열거는 메소드를 가질 수 있다.

enum Rank: Int {
    case Ace = 1
    case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
    case Jack, Queen, King

    func simpleDescription() -> String {
        switch self {
            case .Ace:
                return "ace"
            case .Jack:
                return "jack"
            case .Queen:
                return "queen"
            case .King:
                return "king"
            default:
                return String(self.toRaw())
        }
    }
}

let ace = Rank.Ace
let aceRawValue = ace.toRaw()

이 예제에서 각 값은 정수값이다. 그리고 첫번째 값만 정의해주면 나머지는 순차적으로 증가한다. raw type의 열거형에서는 float이나 문자열도 사용할 수 있다.

toRaw, fromRaw를 사용하면 열거 값과 원값을 서로 변환할 수 있다.

if let convertedRank = Rank.fromRaw(3) {
    let threeDescription = convertedRank.simpleDescription()
}

C에서는 열거가 단순한 매크로와 비슷했다고 하면, 여기서는 객체의 개념인 듯

사실 열거는 구분자를 위해 사용되므로, 반드시 이런 변환을 해야할 필요는 없다. (그럼 가이드에 왜 이렇게 많이 설명했냐;;;)

이번에는 카드의 종류를 열거형으로 나타내자

enum Suit {
    case Spades, Hearts, Diamonds, Clubs
    func simpleDescription() -> String {
        switch self {
            case .Spades:
                return "spades"
            case .Hearts:
                return "hearts"
            case .Diamonds:
                return "diamonds"
            case .Clubs:
                return "clubs"
        }
    }
}

let hearts = Suit.Hearts
let heartsDescription = hearts.simpleDescription()

구조체는 여러 모로 클래스와 비슷하다. 멤버 변수, 함수, 초기화 메소드까지 닮았다. 클래스와 구조체의 가장 큰 차이점은 클래스는 언제나 참조로 넘겨지지만, 구조체는 항상 값이 복사되어 넘어간다는 점이다.

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}

let threeOfSpades = Card(rank: .Three, suit: .Spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()

열거의 인스턴스는 그와 관련된 값을 가지고 있다. 따라서 동일한 열거의 다른 인스턴스들은 각자의 값을 가지고 있다. 이 값들은 인스턴스를 만들 때 지정해줄 수 있다. 연관된 값은 raw 값과는 다르다. 열거의 특정 멤버의 인스턴스들은 모두 동일한 raw 값을 가지며, 이 값은 열거의 정의에서 결정된다.

예를 들어 일출, 일몰 시각을 서버에 요청하는 코드를 생각해보자. 서버가 오류정보와 함께 응답을 할 것이다.

enum ServerResponse {
    case Result(String, Stirng)
    case Error(String)
}

let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Error("Out of cheese.")

switch success {
    case let .Result(sunrise, sunset):
        let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
    case let .Error(error):
        let serverResponse = "Failure...\(error)"
}

그 값을 열거의 각 케이스에 매칭되는 포맷에 따라 결정될 수 있다.

왠지 Haskell의 패턴매칭을 응용하는 느낌이다.

프로토콜과 확장

프로토콜을 정의할 때는 protocol 예약어를 사용한다.

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust ()
}

클래스 뿐만 아니라 열거(체), 구조체도 프로토콜을 적용할 수 있다. 이제 적용 예시를 보자.

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}

var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription


struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}

var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription

여기서 눈여겨 볼 점은 구조체 정의부분에서 mutating 키워드가 사용된 점이다. 이는 void와 유사하지만 자신이 속한 클래스(혹은 패밀리)의 객체를 변경한다는 의미를 갖는다. 하지만 클래스에서는 명시적으로 mutating을 쓰지 않는데, 왜냐하면 클래스의 메소드들은 항상 클래스를 변경하기 때문이다.

이제 기존 타입에 새로운 메소드나 계산된 프로퍼티를 추가하여 확장하는 방법을 보자. 카테고리하고 비슷하다.

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }

    mutating func adjust() {
        self += 42
    }
}

7.simpleDescription

프로토콜 이름은 마치 다른 타입 이름처럼 사용할 수 있다. 예를 들면 서로 타입은 다르지만 동일한 프로포톨을 따르는 객체들의 집합을 만들 수 있다. 만약 프로토콜 이름을 타입처럼 사용한다면, 프로토콜 외부에서 정의된 메소드는 참조할 수 없다.

let protocolValue: ExampleProtocol = a
// a <- SimpleClass: ExampleProtocol
protocolValue.anotherProperty  // ERROR!!!

protocolValueSimpleClass의 런타임 타입을 갖지만, 컴파일러는 ExampleProtocol의 타입으로 간주한다. 따라서 클래스 내에 정의된 메소드라도 프로토콜에서 선언되지 않았으면 참조할 수 없다.

제네릭

제네릭은 형이 명시되지 않은 타입을 처리할 수 있는 함수, 클래스를 의미한다. 제네릭은 꺾인 괄호에 타임을 둘러싼다.

func repeat<ItemType>(item: ItemType, times: Int) -> ItemType[] {
    var result = ItemType[]()
    for i in 0..times {
        result += item
    }
    return result
}

repeat("knock", 4)
// ["knock", "knock", "knock", "knock"]

다음은 이를 활용한 선택적 타입의 정의이다.

enum OptionalValue<T> {
    case None
    case Some(T)
}

// Haskell의 Maybe와 비슷하다?

제네릭 표기에는 여러 타입이 명시될 수 있고, 그 중 where 절을 통해서 특정 타입이 특정 프로토콜을 만족해야 한다거나, 두 타입이 같은 타입이어야 한다거나하는 제약을 걸 수 있다.

func anyCommonElements<T, U where T: Sequence, U: Sequence, T.GeneratorType.Element: Equatable, T.GeneratorType.Element == U.GeneratorType.Element>(lhs: T, rhs: U) --> Bool {
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false;
}

anyCommonElements([1, 2, 3], [3, 4])

where 절은 단순히 타입 (타입에는 raw type외에도 클래스, 프로토콜 등을 포함)인지 검사하는 부분을 넣을 수 있다. 즉,

<T where T: Equatable>

<T: Equatable> 

로 줄여 쓸 수 있다.