[Swift] 기초 문법 정리 - 함수 (2)

Posted by WhiteHyun on March 31, 2022

Goal

  • Swift의 함수에 대한 정의를 올바르게 이해한다.
  • 함수를 직접 사용할 수 있다.

들어가기에 앞서

  1. 본 글은 Swift의 언어가이드를 따릅니다.[1]
  2. 이전에 작성했던 함수 - (1)과 이어집니다.
  3. 문법작성규칙은 아래와 같이 작성합니다.[2]
    • lower camel case: method, variable, constant
    • upper camel case: enum, struct, class, extension

전달인자 레이블과 파라미터 이름

지금까지 우리는 함수 파라미터 설정을 파라미터 이름: 타입으로 설정하였습니다. Swift는 함수 사용자에게 일관적이고 명시적인 파라미터명을 위해 전달인자 레이블을 별도로 지정해줄 수 있습니다.
지정 방식은 전달인자레이블 파라미터: 타입로 파라미터 이름 앞에 전달인자 레이블을 적어둡니다. 예시는 다음과 같습니다.

1
2
3
4
5
func greet(name person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(name: "승현", from: "Seoul"))
// Prints "Hello 승현!  Glad you could visit from Seoul."

이렇게 전달인자 레이블을 추가함으로써 읽기 쉽고 명확한 의도를 가진 함수를 사용할 수 있습니다.

전달인자 레이블 생략(Omit)

print(:) 함수와 같이 파라미터명을 생략하고 인자를 그대로 입력받게 하고싶다면 와일드카드 패턴(_)을 사용하여 생략하게 만들 수 있습니다.

1
2
3
4
5
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
    // In the function body, firstParameterName and secondParameterName
    // refer to the argument values for the first and second parameters.
}
someFunction(1, secondParameterName: 2)

파라미터 기본값

파라미터의 값을 입력받지 않아도 기본값으로 설정할 수 있도록 정의할 수 있습니다. 기본값을 설정할 파라미터는 관념적으로 뒷 부분에 작성합니다(앞 부분에 설정해도 오류는 발생하지 않습니다).

1
2
3
4
5
6
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
    // If you omit the second argument when calling this function, then
    // the value of parameterWithDefault is 12 inside the function body.
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault is 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault is 12

가변 파라미터

가변 파라미터는 지정된 타입의 값을 0개 이상 받습니다. 함수를 호출할 때 파라미터가 다양한 수의 입력 값을 전달할 수 있도록 지정하려면 가변 파라미터를 사용합니다. 파라미터 타입 이름 뒤에 세 개의 마침표 문자(...)를 삽입하여 가변 파라미터를 작성합니다.

가변 파라미터로 전달된 값은 함수 본문 내에서 지정된 타입의 배열로 사용할 수 있습니다. 예를 들어, 파라미터명인 numbersDouble...타입을 가진 가변 파라미터는 [Double] 타입에 numbers라는 상수 배열로 사용할 수 있습니다.

다음 예제에서는 임의의 길이의 숫자 목록에 대한 산술 평균을 계산합니다.

1
2
3
4
5
6
7
8
9
10
11
func arithmeticMean(_ numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8.25, 18.75)
// returns 10.0, which is the arithmetic mean of these three numbers

In-Out 파라미터

함수의 파라미터는 기본적으로 상수입니다. 그래서 함수 내에서 파라미터 값을 변경하려고 하면 컴파일 오류가 발생합니다. 즉, 실수로 파라미터 값을 변경할 수 없습니다. 하지만 전달된 파라미터의 값을 변경해야하는 상황이 존재하게 될 때, 이를 가능케 하는 것이 바로 in-out 파라미터 입니다.

in-out 파라미터로 설정하려면 파라미터 타입명 앞에 inout이라는 키워드를 작성하면 됩니다. 이렇게 하면 전달받은 파라미터 값을 함수 내에서 수정가능하며, 함수가 반환된 뒤 기존의 값을 변경합니다.

좀 더 알아보고 싶다면 In-Out Parameters[3]를 참고하세요.

in-out 파라미터를 전달할 때 상수값이나 리터럴은 수정불가능하므로 이를 전달할 시 컴파일 오류가 발생합니다. 또한 인자를 전달할 때 ampersand(&)기호를 사용하여야 합니다.

NOTE 🧑‍💻:

in-out 파라미터는 기본값과 가변 파라미터를 설정할 수 없습니다.

아래 예시에서는 swapTwoInts(_:_:)라는 함수에서 inout을 활용하여 두 파라미터의 값을 Swap하는 과정입니다.

1
2
3
4
5
6
7
8
9
10
11
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"

함수 타입(Function Types)

모든 함수에는 파라미터 타입과 반환 타입으로 구성된 특정 함수 타입을 갖습니다.

예를 들어, 두 함수가 있다고 가정합니다.

1
2
3
4
5
6
func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b
}

간단한 수학 함수로 정의되어있는 두 함수는 두 개의 Int 타입 파라미터를 갖고 Int를 반환하고 있습니다. 따라서 이 함수의 타입은 (Int, Int) → Int 으로 설명할 수 있습니다.

다음 예시는 파라미터와 반환값 둘 다 없는 함수입니다.

1
2
3
func printHelloWorld() {
    print("hello, world")
}

이 타입은 () → Void이며, Void는 ()와 같기 때문에 총 네 가지 방식으로 표현가능합니다.

함수 타입의 사용

Swift의 다른 타입과 마찬가지로 함수도 변수 또는 상수에 설정할 수 있습니다.

1
var mathFunction: (Int, Int) -> Int = addTwoInts

위 코드는, “두 Int타입의 파라미터와 Int를 반환값으로 가지는 함수타입인 mathFunction이라는 변수에 addTwoInts 함수를 참조한다.”라고 해석할 수 있습니다.

addTwoInts 함수는 mathFunction의 타입과 동일하기 때문에 할당가능하며, 이제 addTwoInts 뿐 아니라 mathFunction로도 호출할 수 있습니다.

1
2
print("Result: \(mathFunction(2, 3))")
// Prints "Result: 5"

물론 같은 타입의 함수가 또 있는 경우 기존의 참조를 변경하여 다른 함수를 참조하도록 할 수 있습니다.

1
2
3
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// Prints "Result: 6"

함수도 타입이기 때문에, 추론가능합니다.

1
2
let anotherMathFunction = addTwoInts
// anotherMathFunction is inferred to be of type (Int, Int) -> Int

파라미터에서의 함수 타입

함수의 파라미터로 함수 타입을 설정할 수 있습니다.

1
2
3
4
5
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// Prints "Result: 8"

반환값에서의 함수 타입

반환타입 역시 함수 타입으로 설정 가능합니다. 반환 화살표(→)바로 뒤에 함수 타입을 입력하면 끝입니다.

다음 예시를 보면, stepForward(_:)과 stepBackward(_:)라는 간단한 함수 두 개를 정의하고있습니다. stepForward(_:)은 파라미터로 받은 입력값보다 하나 더 큰 값을 반환하고, stepBackward(_:)는 하나 더 작은 값을 반환합니다.

1
2
3
4
5
6
func stepForward(_ input: Int) -> Int {
    return input + 1
}
func stepBackward(_ input: Int) -> Int {
    return input - 1
}

다음은 chooseStepFunction(backward:)이라는 함수로 반환 타입은 (Int) -> Int입니다. chooseStepFunction(backward:) 함수는 backward라는 bool 파라미터에 따라 stepForward(_:) 함수 또는 stepBackward(_:) 함수를 반환합니다.

1
2
3
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    return backward ? stepBackward : stepForward
}

이제 chooseStepFunction(backward:)을 사용하여 왼방향, 오른방향으로 이동하는 함수를 얻을 수 있습니다.

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

위 예시에서 currentValue라는 변수를 0에 가깝게 이동시키기 위해 앞으로 가야하는지 뒤로 가야하는지 chooseStepFunction으로 설정합니다. currentValue의 초기값은 3입니다. 즉, currentValue > 0이 true를 반환하고 chooseStepFunction(backward:)는 stepBackward(_:) 함수를 반환합니다. 그리고 반환된 함수에 대한 참조는 moveNeararerToZero라는 상수에 저장됩니다.

이제 moveNearerToZero는 다음과 같이 사용되게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// 3...
// 2...
// 1...
// zero!

중첩 함수(nested functions)

지금까지 설명했던 함수는 전부 광역으로 선언한 함수였습니다. 이 파트에서는 함수 내에 함수를 선언하는, 중첩 함수에 대해 설명합니다.

중첩 함수는 외부로부터 숨겨져있지만, 감싸둔 함수(enclosing function)가 중첩함수를 반환함으로써 다른 스코프에서 사용될 수 있습니다.

위의 chooseStepFunction(backward:) 예를 다시 작성하여 중첩 함수를 사용해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    func stepForward(input: Int) -> Int { return input + 1 }
    func stepBackward(input: Int) -> Int { return input - 1 }
    return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!

stepForward(:)stepBackward(:)는 외부로 나와있어도 상관없지만, chooseStepFunction 내에서만 사용되기 때문에 굳이 모듈 전역에서 사용할 필요가 없습니다. 그래서 사용 범위를 한정하고자 함수를 중첩 함수로 구현하고, 필요할 때만 외부에서 사용할 수 있도록 구현하는 것이 코드를 더 분명하게 만들 것입니다.

Reference

  1. The Basics — The Swift Programming Language (Swift 5.6)
  2. Camel Case - 위키피디아
  3. In-Out Parameters