Programming/Mac & iOS

[Swift] Swift Study 2주차 요약 (Closure, Collection, Property, Initialization, Observer ... etc)

MB Brad KWON 2014. 8. 18. 10:41
Function parameters
함수의 param들은 기본적으로 다 상수다. 그러므로 수정이 불가능하다. 함수 내에서 param을 수정하기 위해선 inout param으로 선언하면 되는데, 함수 선언부에서 param앞에 '&'를 표시해주면 된다.

Function type
function type을 다른 function의  param의 타입으로 사용할 수 있다. 함수의 타입이 들어가는 부분에  '(Int, Int) -> Int'와 같이 일반 함수 선언 부에서 함수명의 뒷부분을 적어주면 된다. function type을 사용해서 함수를 param으로 사용할 수 있다.

ex)
func addTwoInts(a: Int, b: Int) -> Int {
    return a + b
}

func printMathResult(mathFunction: (Int, Int) -> Int, a: Int, b: Int) {
    println("Result: \(mathFunction(a, b))")
}

printMathResult(addTwoInts, 3, 5)
// prints "Result: 8’

또, function type을 return  type으로 또한 사용이 가능하다. 함수 선언부에 return을 적어 주는 부분에 function type을 적으면 된다.

ex)
func stepForward(input: Int) -> Int {
    return input + 1
}
func stepBackward(input: Int) -> Int {
    return input - 1
}

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
    return backwards ? stepBackward : stepForward
}

var currentValue = 3
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function

Closures
closure는 코드 상에서 나뉘어 있고 사용되는 기능을 스스로 내포한 블록이다. C와 objective-C의 block과 다른 언어의 lambda와 비슷하다. 다만, block과 closure의 다른 점은 block는 단순한 instruction의 모음 단위이다. closure는 실제로 block을 value로 소유한 객체라고 생각하면 된다. 간단한 예로 value를 block에서 쓰려면 block에서 사용한다는 표시하기 위해 '__block' 키워드를 사용하지만 closure는 실체를 가진 reference이므로 정의할 때 당시의 주변 context의 value 자체에 대한 refence count를 증가시켜 capturing이 가능한 것이다.

다른 개발자의 표현을 빌리자면 'closure는 함수 포인터와 비슷하지만 함수 포인터는 함수 내부의 변수만 접근이 가능한 반면 closure는 정의할 당시의 주변의 context의 변수 들에 접근이 가능하다'라고 서술되어 있다.

전역 함수와 중첩 함수도 closure의 유형으로 아래와 같다.

    • 전역 함수는 함수명을 가지지만 value를 capture하지 않는다.
    • 중첩 함수는 함수명을 가지면서 자신의 함수 중첩 내의 value를 capture할 수 있다.
    • closure는 이름을 가지지 않으며 주변의 context의 value를 capture할 수 있다.


closure의 capturing value에 관해서는 뒤에서 자세히 설명할 것이다. 먼저, closure의 syntax는 아래와 같다.

ex)
{ (parameters) -> return type in
    statements
}

'in' 키워드는 closure에서 param과 return의 선언이 끝나고 body가 시작됨을 알려준다. closure의 body는 너무 짧아 한 줄로 끝날수도 있으므로 'in'이라는 키워드를 사용하여 명시적으로 표시한다.

Operator function
closure의 축약형이다. 애플의 swift 서적의 예로 설명하면 아래와 같다. 먼저, 아래와 같은 closure가 존재한다.

ex)
reversed = sort(names, { (s1: String, s2: String) -> Bool in return s1 > s2 })

위에서와 같이 실행 단위를 block으로 묶어서 사용된다. 단, 위의 경우는 전역함수나 중첩 함수와 다르게 anonymous 형태로 사용되었다. 위의 closure를 단순화 시켜 최종적으로 operator function의 형태로 만들것이다. 우선, Swift에서 제공하는 sort 함수는 두번 째 param의 (String, String) -> Bool 임을 예측한다. 그러므로 function type은 생략한다.

ex)
reversed = sort(names, { (s1, s2) in return s1 > s2 })

'in' 키워드 뒤에 statement가 1줄이기 때문에 return statement가 올 것이라는 것을 예측할 수 있기 때문에 'return' 키워드를 생략한다.

ex)
reversed = sort(names, { (s1, s2) in s1 > s2 })

swift는 위와 같은 inline closure에 default param name을 제공한다. 그러므로 param name을 $0, $1, $2 ... and so on으로 대체 가능하다. 그리고 default param name을 사용할 경우 param의 선언부도 생략한다.

ex)
reversed = sort(names, { $0 > $1 })

여기서 추가적으로 swift에서 제공하는 String에서는 '>' operator에 대해서 추가 구현이 되어 있으므로 아래와 같이 축약이 가능하다.

ex)
reversed = sort(names, >)

실제로 operator에 관한 내용은 더 뒤에서 설명이 나오지만 짐작하건데 C++에서의 operator overloading과 비슷할 것으로 생각된다.

func : map -> call closure once each value in array
swift의 array 객체는 map이라는 method를 가진다. map 메소드는 array의 각 value마다 사용자가 정의한 closure를 호출하는 역할을 한다. 사용법은 아래와 같다.

ex)
let strings = numbers.map {
    (var number) -> String in
    var output = ""
    while number > 0 {
        output = digitNames[number % 10]! + output
        number /= 10
    }
    return output
}
// strings is inferred to be of type String[]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]


capturing values in closure
closure는 자신이 정의된 곳의 context의 constant와 value들을 capture할 수 있다. 심지어 context의 scope를 벗어났을 때도 capture가 가능하다. 일반적인 중첩 함수로 makeIncrementor(forIncrement:)를 예로 들어보자. 지역 변수 'runningTotal'은 makeIncrementor가 가지는 scope 안에서만 유효하다.

ex)
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0

    func incrementor() -> Int {
        runningTotal += amount
        return runningTotal
    }

    return incrementor
}

중첩 함수는 closure의 한 형태이다. 아래와 같이 makeIncrementor(forIncrement:)를 상수에 집어넣어 사용해보자. 비록 makeIncrementor(forIncrement:)가 실행되어 scope안에서만 'runningTotal'의 value가 유효하지만 closure이기 때문에 scope를 벗어나서도 'runningTotal'의 value는 유효하다. 이를 value를 capture했다고 한다.

ex)
let incrementByTen = makeIncrementor(forIncrement: 10)

incrementByTen()
// returns a value of 10

incrementByTen()
// returns a value of 20

incrementByTen()
// returns a value of 30

Enumeration
swift의 enum은 C와 obejctive-C와 달리 default로 integer값을 사용하지 않는다. integer 대신에 각 member를 구별할 수 있는 value를 사용한다.  아래의 syntax로 사용한다.

ex)
enum CompassPoint {
    case North
    case South
    case East
    case West
}

enum Planet {
case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}

enum : Associated values
associated value는 연관되어 있는 값들을 서로 다른 타입(양식)으로표현할 때 사용한다. 사용자가 정의한 타입으로 enumeration을 할 수 있다. 아래와 같이 UPCA와 QRCode로 사용자가 직접 enumeration 안에서 타입을 정의할 수 있고 분기문에서 사용 가능하다. 사용된 enum의 associated value는 constant로 사용한다. 

ex)
enum Barcode {
    case UPCA(Int, Int, Int)
    case QRCode(String)
}

var productBarcode = .QRCode("ABCDEFGHIJKLMNOP")

switch productBarcode {
case let .UPCA(numberSystem, identifier, check):
    println("UPC-A with value of \(numberSystem), \(identifier), \(check).")

case let .QRCode(productCode):
    println("QR code with value of \(productCode).")
}
// prints "QR code with value of ABCDEFGHIJKLMNOP."

enum : Raw values
associated value는 서로 다른 타입의 값을 표할 때 사용된다. 하지만 raw value는 모두 같은 타입의 enum member들을 사용할 때 사용한다. raw value를 사용하려면 enum을 정의할 때, 구체적인 타입을 아래와 같이 정의해주면 된다.

ex)
enum ASCIIControlCharacter: Character {
    case Tab = "\t"
    case LineFeed = "\n"
    case CarriageReturn = "\r"
}

또한 기존의 C와 objective-C처럼 integer로 이루어진 enum 선언도 아래와 같이 할 수 있다. 'Mercury'를 '1'로 정의하고 뒤에는 자동으로 '2, 3...'으로 정의된다.

ex)
enum Planet: Int {
        case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}


Structures and Enum is value type
Classes and Closures is reference type

syntax)
struct SomeStructure {
    // structure definition goes here
}

class SomeClass {
    // class definition goes here
}

NSArray & Array
NSDictionary & Dictionary
NSArray & NSDictionary는 클래스로 구현되어 있다. 하지만 Array & Dictionary는 구조체로 이루어져 있어서 value type을 가진다. 여기서 특이점은 Dictionary는 value type의 특징 대로 무조건 copy된다. 하지만 Array의 경우는 length의 변화가 없을 경우, copy를 하더라도 공유된 sequence를 가지게 된다. length가 변할 때 실제로 물리적 copy를 수행하게 된다. 만약에 Array를 sequence를 공유하디 않고 애초에 물리적 copy를 하고 싶을 때는 아래와 같이 unshare() 혹은 copy()를 사용하면 된다.

ex)
var unsharedNames = names.unshare()
var copiedNames = names.copy()

unshare()는 호출 당시에는 물리적 copy를 수행하지 않지만 Array의 length 이외의 변화가 생길 때에도 즉시 물리적 copy를 수행한다. copy()는 호출과 동시에 물리적 copy를 수행하게 된다.


You can use (===, !==)
swift에서는 삼등연산자를 사용가능하다. 객체를 상대로 사용할 경우, 객체가 같은 클래스로 생성되었는지 아닌지를 구분할 수 있다. Array에 사용할 경우 Array 내부의 value들이 같은지 아닌지를 확인 할 수 있다.

Lazy property
lazy property는 실제로 property에 접근하여 read를 하기 전까지 불필요한 property의 초기화를 하지 않는다. 사용할 때는 아래와 같이 '@lazy'를 붙여주면 된다. 참고로 conatant property는 lazy property로 사용이 불가능하다.

ex)
class DataImporter {
    /*
    DataImporter is a class to import data from an external file.
    The class is assumed to take a non-trivial amount of time to initialize.
    */
    var fileName = "data.txt"
    // the DataImporter class would provide data importing functionality here
}
 
class DataManager {
    @lazy var importer = DataImporter()
    var data = String[]()
    // the DataManager class would provide data management functionality here
}
 
let manager = DataManager()
manager.data += "Some data"
manager.data += "Some more data"
// the DataImporter instance for the importer property has not yet been created

println(manager.importer.fileName)
// the DataImporter instance for the importer property has now been created
// prints "data.txt"

위의 예시를 통해서 볼 경우, 제일 하단의 'println(manager.impoter.fileName)'이 호출되어 read가 일어나기 전까지 lazy property인 manager.importer의 초기화는 이루어지지 않는다.

Computed property
computed property는 실제 물리적인 값을 가지진 않고 오로지 getter와 setter를 가진 property를 말한다. 아래의 예시를 보면 이해가 바르다.

ex)
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
    get {
        let centerX = origin.x + (size.width / 2)
        let centerY = origin.y + (size.height / 2)
        return Point(x: centerX, y: centerY)
    }
    set(newCenter) {
        origin.x = newCenter.x - (size.width / 2)
        origin.y = newCenter.y - (size.height / 2)
    }
    }
}

위와 같이 property center는 실제로 물리적인 value를 가지고 있지 않지만 getter와 setter를 통한 접근이 가능하다. 이를 computed property라고 한다. setter를 정의하지 않을 경우, read only property로 사용되며 아래와 같이 getter라고 정의할 필요없이 간단하게 사용 가능하다.

ex)
struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
    return width * height * depth
    }
}

Property observer
objective-C의 KVO와 비슷한 기능을 가진 친구이다. 'willSet()'과 'didSet'을 통하여 property의 value에 set이 발생할 때마다 수행할 로직을 정의할 수 있다. 심지어 같은 값을 set해도 호출된다. overrinding한 property에도 사용할 수 있다. 사용방법은 아래와 같다.

ex)
class StepCounter {
    var totalSteps: Int = 0 {
    willSet(newTotalSteps) {
        println("About to set totalSteps to \(newTotalSteps)")
    }
    didSet {
        if totalSteps > oldValue  {
            println("Added \(totalSteps - oldValue) steps")
        }
    }
    }
}

Type property & method
type property와 type method는 Java에서 static property와 method를 말한다. 실제의 객체가 없어도 사용 가능한 property와 method이다. 사용 법은 아래와 같다. struct와 enum에서 사용될 때는 'static'키워드를 사용하고 class에서 사용될 때는 'class'라는 키워드를 사용해주면 된다.

ex)
struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
    // return an Int value here
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
    // return an Int value here
    }
}
class SomeClass {
    class var computedTypeProperty: Int {
    // return an Int value here
    }
}

Mutating
구조체와 enum은 value type을 가지기 때문에 instance method를 통한(type method 말고) 내부 value의 변경이 불가능하다. 하지만 method 앞에 'mutating'이라는 키워드를 사용할 경우, 내부 value의 변경이 가능한 mutating method를 정의할 수 있다. 또한 내부의 value뿐만 아니라 self를 사용하여 구조체와 enum의 자신의 개체 마저도 변경이 가능하다. 

ex)
‘struct Point {
    var x = 0.0, y = 0.0
    mutating func moveByX(deltaX: Double, y deltaY: Double) {
        self = Point(x: x + deltaX, y: y + deltaY)
    }
}

enum TriStateSwitch {
        case Off, Low, High
        mutating func next() {
            switch self {
            case Off:
                self = Low
            case Low:
                self = High
            case High:
                self = Off
            }
    }
}

Subscripts
subscript는 class, struct, enum에서 정의 가능하다. subscript는 array나 dictionary와 같이 index나 key를 가지고 value를 querying하는 것을 가르킨다. subscript를 이용해서 class, struct, enum에서도 index를 통한 value의 querying이 가능하다. syntax는 computed property와 비슷하다. 2개의 예제 중 아래는 read-only subscript이다.

ex)
subscript(index: Int) -> Int {
    get {
        // return an appropriate subscript value here
    }
    set(newValue) {
        // perform a suitable setting action here
    }
}

subscript(index: Int) -> Int {
        // return an appropriate subscript value here
}

아래와 같이 실제 사용의 예를 보면 class, struct, enum도 array, dictionary와 같이 index를 통한 querying을 사용할 수 있다. 또한 index로 사용할 param을 variable param 혹은 variadic param으로도 사용 가능하나 in-out이나 defalut param으로는 사용 불가능하다.(기억이 안난다면 1주차 요약 참고) input param과 return type에는 제한이 없다.

ex)
struct TimesTable {
        let multiplier: Int
        subscript(index: Int) -> Int {
            return multiplier * index
        }
}

let threeTimesTable = TimesTable(multiplier: 3)
println("six times three is \(threeTimesTable[6])")
// prints "six times three is 18"


Preventing overrides
상속을 할 때, 상속을 허용하지 않을 property 혹은 method는 앞에 '@final'이라는 키워드를 사용하면 된다. Java를 사용해본 개발자라면 표현이 익숙할 것이다.

Initializer (init)
Initializer에서 모든 property를 초기화해야만 컴파일 가능하다. Initailizer내에서의 method 호출은 모든 property(Optional 제외, 엄밀히 말하면 optional은 nil로 초기화됨)를 초기화하고 나서 가능하다.

Designated & convenience initializer
Designated initializer는 모든 property 초기화하고 모든 클래스가 최소 1개 이상을 가진다. 반면, Convenience initializer는 보조 initializer이기 때문에 굳이 필요하지 않으면 만들지 않는 것이 좋다. 사용할 때는 'convenience' 키워드 사용한다. syntax는 아래와 같다.

ex)
Designated initializer
init(parameters) {
    statements
}

Convenience initializer
 convenience init(parameters) {
    statements
}

Initailizer chaining 
Initializer의 작은 규칙 몇 가지가 따른다. 구현에 앞서 이를 숙지하면 아~~주 좋다.
  1. Designated만 상위클래스의 Designated를 호출
  2. Convenience만 같은 클래스 내의 다른 생성자를 호출
  3. Convenience는 최종적으로 Designated를 호출하며 끝날 것


위의 이미지와 같이 designated는 오로지 슈퍼 클래스의 designated만을 호출할 수 있다. convenience만 다른 생성자들을 호출이 가능하다. 객체가 실제로 물리적 초기화가 되는 시점은 designated가 호출되는 시점이다. initial logic이 중첩되는 다양한 customized initializer를 사용할 경우엔 convenience를 이용하자. 굳이 필요하지 않으면 안 쓰는 것이 좋다.



참고 : Apple Inc. ‘The Swift Programming Language.’