xtring.dev

[Swift] Swift 기본 문법 정리(2) 👽 - 함수와 클로저, 클래스와 구조체 본문

Mobile/iOS

[Swift] Swift 기본 문법 정리(2) 👽 - 함수와 클로저, 클래스와 구조체

xtring 2021. 3. 22. 17:23

 

Swift 기본 문법 정리(2)

전수열님의 GitBook을 기반으로 정리합니다.

 

 

함수

Swift에서 함수는 func 키워드를 사용해서 정의합니다. 그리고 ->를 사용해서 함수의 반환(return) 타입을 지정합니다.

func hello(name: String, time: Int) -> String {     // hello 함수는 name(string), time(int) parameter들을 받으며 String 타입을 반환합니다.
    var string = ""
    for _ in 0..<time {
        string += "\(name)님 안녕하세요!\n"
    }
    return string
}

Swift에서는 함수를 호출할 때 파라미터 이름을 함께 써주어야 합니다.

hello(name: "Mark", time: 3)    // 함수 호출 시 파라미터에 각 파라미터 명을 써주어야 함

함수 호출 시 사용하는 파라미터 이름과 함수 내부에서 사용하는 파라미터 이름을 다르게 사용할 수 있습니다.

func hello(to name: String, numberOfTimes time: Int) {
    // 함수 내부에서는 `name`, `time`을 사용합니다.
    for _ in 0..<time {
        print(name)
    }
}

hello(to: "Mark", numberOfTimes: 3)     // 함수를 호출 할 때는 `to`, `numberOfTimes`를 사용합니다.

파라미터 이름을 _로 정의하면 함수를 호출할 때 파라미터 이름을 생략할 수 있습니다.

func hello(_ name: String, time: Int) {
    // ...
}

hello("Mark", time: 3)      // 'name: '이 생략됨

파라미터에 기본값을 지정할 수도 있습니다. 기본값이 지정되어 있다면 함수 호출 시 생략이 가능합니다.

func hello(name: String, time: Int = 1) {
    // ...
}

hello("Mark")       // time은 기본값이 지정되어 있으므로 생략이 가능

...을 사용하면 개수가 정해지지 않은 파라미터(Variadic Parameters)를 받을 수 있습니다.

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

sum(1, 2)
sum(3, 4, 5)
// * number parameter를 정하지 않고 받을 수 있음

함수 안에 함수를 다시 작성하는 것이 가능합니다.

func hello(name: String, time: Int) {
    func message(name: String) {
        return "\(name)님 안녕하세요!"
    }

    for _ in 0..<time {
        print(message(name: name))
    }
}

함수 안에 정의한 함수를 반환 할 수 있습니다.

func helloGenerator(message: String) -> (String) -> String {
    func hello(name: String) -> String {
        return name + message
    }

    return hello
}

let hello = helloGenerator(message: "님 안녕하세요!")
hello("Mark")   // Mark님 안녕하세요!

여기서 핵심은 helloGenerator() 함수의 반환 타입이 (String) -> String라는 것입니다. 즉, helloGenerator()는 '문자열을 받아서 문자열을 반환하는 함수'를 반환하는 함수라는 것입니다.
helloGenerator() 안에 정의한 hello() 함수가 여러개의 파라미터를 받는다면 아래와 같이 사용합니다.

func helloGenerator(message: String) -> (String, String) -> String {
    func hello(firstName: String, lastName: String) -> String {
        return lastName + firstName + message
    }
    return hello
}

let hello = helloGenerator(message: "님 안녕하세요!")
hello("Mark", "Hwang")

클로저(Closure)

클로저(Closure)를 사용하면 바로 위에 작성한 코드를 조금 더 간결하게 만들 수 있습니다. 클로저는 중괄호({})로 감싸진 '실행 가능한 코드 블럭'입니다.

func helloGenerator(message: String) -> (String, String) -> String {
    return { (firstName: String, lastName: String) -> String in
        return lastName + firstName + message
    }
}

일반적인 함수와 다르게 함수 이름을 정의하지 않습니다. 하지만 파라미터를 받을 수 있고 반환 값이 존재할 수 있습니다. 이렇게 생긴 형태를 클로저(Closure)라고 합니다.

클로저는 중괄호({})로 감싸져있습니다. 그리고 파라미터를 괄호로 감싸서 정의합니다. 함수와 마찬가지로 ->를 사용해서 반환 타입을 명시합니다. 조금 다른 점은 in 키워드를 사용해서 파라미터, 반환 타입 영역과 실제 클로저의 코드를 분리하고 있습니다.

{ (firstName: String, lastName: String) -> in
    return lastName + firstName + message
}

클로저의 장점은 간결함유연함에 있습니다.

Swift 컴파일러의 타입 추론 덕분에, helloGenerator()함수에서 반환하는 타입을 가지고 클로저에서 어떤 파라미터를 받고 어떤 타입을 반환하는지를 알 수 있습니다. >> 파라미터 타입을 생략 가능

func helloGenerator(message: String) -> (String, String) -> String {
    return { firstName, lastName in
        return lastName + firstName + message
    }
}

파라미터에 임의의 변수를 통해 접근이 가능합니다.

func helloGenerator(message: String) -> (String, String) -> String {
    return {
        return $1 + $0 + message
    }
}

클로저 내부의 코드가 한 줄이라면, return까지도 생략할 수 있습니다.

func helloGenerator(message: String) -> (String, String) -> String {
    return { $1 + $0 + message }
}

코드를 최대한으로 생략하면 아래의 코드가 나옵니다.

let hello: (String, String) -> String = { $1 + $0 + "님 안녕하세요!" }
hello("Mark", "Hwang")

여기에는 옵셔널과 옵셔널 체이닝도 사용가능합니다.

let hello: ((String, String) -> String)?
hello?("Mark", "Hwang")

클로저를 변수로 정의하고 함수에서 반환할 수도 있는 것처럼, 파라미터로도 받을 수 있습니다.

func manipulate(number: Int, using block: Int -> Int) -> Int {
    return block(number)
}

manipulate(number: 10, using: { (number: Int) -> Int in
    return number * 2
})

이것도 생략이 가능합니다.

manipulate(number: 10, using: {
    $0 * 2
})

만약 함수의 마지막 파라미터가 클로저라면, 괄호와 파라미터 이름마저 생략할 수 있습니다.

manipulate(number: 10) {
    $0 * 2
}

이런 구조로 만들어진 예시가 sort()filter()입니다. 함수가 클로저 하나만을 파라미터로 받는다면, 괄호를 아예 쓰지 않아도 됩니다.

let numbers = [1, 3, 2, 6, 7, 5, 8, 4]
let sortedNumbers = numbers.sort { $0 < $1 }
print(sortedNembers)    // [1, 2, 3, 4, 5, 6, 7, 8]

let evens = numbers.filter { $0 % 2 == 0 }
print(evens)    // [2, 6, 8, 4]

클로저 활용하기

클로저는 Swift의 많은 곳에서 사용됩니다. sort(), filter()와 같은 배열에 많이 사용됩니다. 대표적인 메서드로는 sort(), filter(), map(), reduce()가 있습니다.

map()은 파라미터로 받은 클로저를 모든 요소에 실행하고, 그 결과를 반환합니다.

let arr1 = [1, 3, 6, 2, 7, 9]
let arr2 = arr1.map { $0 * 2 }  // [2, 6, 12, 4, 14, 18]

reduce()는 초깃값이 주어지고, 초깃값과 첫 번째 요소의 클로저 실행 결과, 그리고 그 결과와 두 번째 요소의 클로저 실행 결과, 그리고 그 결과의 세 번째 요소의 클로저 실행 결과, ... 를 끝까지 실행한 후의 값을 반환합니다.

arr1.reduce(0) { $0 + $1 }  // 28

Swift에서는 연산자도 함수입니다. 따라서 이와 같은 문법도 가능합니다. arr1.reduce(0, +) // 28

클래스와 구조체

클래스(Class)는 class로 정의하고, 구조체(Structure)는 struct로 정의합니다.

class Dog {
    var name: String?
    var age: Int?

    func simpleDescription() -> String {
        if let name = self.name {
            return "\(name)"
        } else {
            return "No name"
        }
    }
}

struct Coffee {
    var name: String?
    var size: String?

    func simpleDescription() -> String {
        if let name = self.name {
            return "\(name)"
        } else {
            return "No name"
        }
    }
}

var myDog = Dog()
myDog.name = "초코"
myDog.age = 3
print(simpleDescription())  // 초코

var myCoffee = Coffee()
myCoffee.name = "카페라떼"
myCoffee.size = "Venti"
print(myCoffee.simpleDescription()) // 카페라떼

클래스는 상속이 가능하며 구조체는 불가능합니다.

class Animal {
    let numberOfLegs = 4
}

class Dog: Animal {
    var name: String?
    var age: Int?
}

var myDog = Dog()
print(myDog.numberOfLegs)   // Animal 클래스로부터 상속받은 값 4

클래스는 참조(Reference)하고, 구조체는 복사(Copy)합니다.

var dog1 = Dog()    // dog1은 새로 만들어진 Dog()를 참조합니다.
var dog2 = dog1     // dog2는 dog1이 참조하는 Dog()를 똑같이 참조합니다.
dog1.name = "초코"   // dog1의 이름을 바꾸면 Dog()의 이름이 바뀌기 때문에,
print(dog2.name)    // dog2의 이름을 가져와도 바뀐 이름("초코")이 출력됩니다.

var coffee1 = Coffee()  // coffee1은 새로 만들어진 Coffee() 그 자체입니다.
var coffee2 = coffee1   // coffee2는 coffee1을 복사한 값 자체입니다.
coffee1.name = "카페라떼" // coffee1의 이름을 바꿔도
coffee2.name            // coffee2는 완전히 별개이기 때문에 이름이 바뀌지 않습니다.(nil)

생성자

클래스와 구조체 모두 생성자(Initializer)를 가지고 있습니다. 생성자에서는 속성의 초깃값을 지정할 수 있습니다.

class Dog {
    var name: String?
    var age: Int?

    init() {
        self.age = 0
    }
}

struct Coffee {
    var name: String?
    var size: String?

    init() {
        self.size = "Tall"
    }
}

만약 속성(name, age, ...)이 옵셔널이 아니라면 항상 초깃값을 가져야 합니다. 만약 옵셔널이 아닌 속성이 초깃값을 가지고 있지 않으면 컴파일 에러가 발생합니다.

class Dog {
    var name: String?
    var age: Int    // 초기값이 없기 때문에 컴파일 에러!
}

stored property 'age' without initial value prevents synthesized initializers

속성을 정의할 때 (1)초깃값을 지정해 주는 방법과, (2)생성자에게 초깃값을 지정해주는 방법이 있습니다.

(1) 초깃값을 지정하는 방법

class Dog {
    var name: String?
    var age: Int = 0    // 속성을 정의할 때 초깃값을 지정하는 방법
}

(2) 생성자에게 초깃값을 지정해주는 방법

class Dog {
    var name: String?
    var age: Int

    init() {
        self.age = 0    // 생성자에서 초깃값 지정
    }
}

생성자도 함수와 마찬가지로 파라미터를 받을 수 있습니다.

class Dog {
    var name: String?
    var age: Int

    init(name: String?, age: Int) {
        self.name = name
        self.age = age
    }
}

var myDog = Dog(name: "초코", age: 3)

만약 상속받은 클래스라면 생성자에서 상위 클래스의 생성자를 호출해주어야 합니다. 만약 생성자의 파라미터가 상위 클래스의 파라미터와 같다면, override 키워드를 붙여주어야 합니다. super.init()은 클래스 속성들의 초깃값이 모두 설정된 후에 불러줍니다. 그리고 나서부터 자기 자신에 대한 self 키워드를 사용할 수 있습니다.

class Dog: Animal {
    var name: String?
    var age: Int

    override init() {
        self.age = 0    // 초깃값 설정
        super.init()    // 상위 클래스 생성자 호출
        print(self.simpleDescription()) // 여기서부터 `self` 접근 가능
    }

    func simpleDescription() -> String {
        if let name = self.name {
            return "\(name)"
        } else {
            return "No name"
        }
    }
}

만약, 위 예시 코드를 아래처럼 바꿔서 super.init()을 하기 전에 self에 접근한다면 컴파일 에러가 발생합니다.

override init() {
    self.age = 0
    print(self.simpleDescription())     // 컴파일 에러
    super.init()
}

error: use of 'self' in method call 'simpleDescription' before super.init initializes self

deinit은 메모리에서 해제된 직후에 호출됩니다.

class Dog {
    // ...
    deinit {
        print("메모리에서 해제됨")
    }
}

속성

속성은 크게 '값을 가지는 속성(Stored Property)'과 '계산되는 속성(Computed Property)'로 구분됩니다. 쉽게 말하자면 속성이 값 자체를 가지고 있는지, 혹은 어떠한 연산을 수행한 뒤 그 결과를 반환하는지의 차이입니다.
우리가 지금까지 정의하고 사용한 name, age와 같은 속성들은 모두 Stored Property입니다. Computed Property는 get, set을 사용해서 정의할 수 있습니다. set에서는 새로 설정될 값을 newValue라는 예약어를 통해 접근할 수 있습니다.

struct Hex {
    var decimal: Int?
    var hexString: String? {
        get {
            if let decimal = self.decimal {
                return String(decimal, radix: 16)
            } else {
                return nil
            }
        }
        set {
            if let newValue = newValue {
                self.decimal = Int(newValue, radix: 16)
            } else {
                self.decimal = nil
            }
        }
    }
}

var hex = Hex()
hex.decimal = 10
hex.hexString   // "a"

hex.hexString = "b"
hex.decimal // 11

위 코드의 hexString은 실제 값을 가지고 있지 않지만, decimal로부터 값을 받아와 16진수 문자열로 만들어서 반환합니다. decimal은 Stored Property, hexString은 Computed Property입니다.

참고로, get만 정의할 경우에는 get 키워드를 생략할 수 있습니다. 이러한 속성을 읽기 전용(Read Only)이라고 합니다.

class Hex {
    // ...
    var hexCode: String? {
        if let hex = self.hexString {
            return "0x" + hex
        }
        return nil
    }
}

get, set과 비슷한 willSet, didSet을 이용하면 속성에 값이 지정되기 직전과 원하는 코드를 실행할 수 있습니다.

struct Hex {
    var decimal: Int? {
        willSet {
            print("\(self.decimal)에서 \(newValue)로 바뀔 예정입니다.")
        }
        didSet {
            print("\(oldValue)에서 \(self.decimal)로 값이 바뀌었습니다.")
        }
    }
}

마찬가지로, willSet에서 새로운 값을 newValue로 얻어올 수 있고, didSet에서는 예전 값을 oldValue라는 예약어를 통해 얻어올 수 있습니다.
willSetdidSet은 일반적으로 어떤 속성의 값이 바뀌었을 때 UI를 업데이트하거나 특정 메서드를 호출하는 등의 역할을 할때 사용됩니다.

반응형
Comments