Swift 정리 - 7. 함수


Swift에서 함수를 어떻게 만들고, 사용하는지 알아보자.


함수는 특정 작업을 수행하는 단위다. 함수를 무엇을 하는지 식별하는 이름을 부여하고, 이 이름은 작업을 수행하기 위해 “호출”된다. Swift에서 함수는 일급 객체이기 때문에 하나의 값으로도 사용할 수 있다.

Swift에서 함수는 다른 언어보다 다양한 모습으로 존재한다.

함수와 메서드

함수와 메서드는 기본적으로 같다. 하지만 상황, 위치에 따라 다른 용어로 부른다.

  • 메서드 : 구조체, 클래스, 열거형 등 특정 타입에 연관되어 사용되는 함수
  • 모듈 전체에서 전역적으로 사용할 수 있는 함수

함수의 정의와 호출

함수를 정의할 때, parameter라는 하나 이상의 타입이 정해진 값들을 입력 값으로 받게 옵션을 줄 수 있다. 또한 함수가 실행이 완료됐을 때 출력할 결과의 타입을 설정할 수도 있는데, 이를 return type이라고 한다.

모든 함수는 함수 이름이 있고, 이 함수 이름은 함수의 동작을 설명한다. 함수를 사용하기 위해 이 이름을 호출하고, argument라고 알려진 입력 값들을 전달한다. 이 argument의 타입은 함수의 parameter의 타입과 일치해야 한다.

함수 정의의 뼈대는 아래와 같다.

func 함수이름(매개변수...) -> 리턴 타입 {
  실행 구문
  
  return 리턴 
}

사용 예시는 아래와 같다.

func greetAgain(person: String) -> String {
    return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// Prints "Hello again, Anna!"

만약 함수 내부 코드가 단 한 줄의 표현이고, 해당 표현의 결과 값의 타입이 함수의 반환 타입과 일치한다면 return 키워드를 생략할 수 있다.

func greetAgain(person: String) -> String {
    "Hello again, " + person + "!"
}

Parameter

매개변수가 없는 경우

함수에 매개변수가 필요 없다면 매개변수 위치를 비워둔다. 매개변수가 없어도 함수 이름 다음에 소괄호는 써야 한다.

func sayHelloWorld() -> String {
    return "hello, world"
}
print(sayHelloWorld())
// Prints "hello, world"

매개변수가 여러 개인 경우

매개변수가 여러 개 필요하다면 쉽표로 이를 구분한다.

func greet(person: String, alreadyGreeted: Bool) -> String {
    if alreadyGreeted {
        return greetAgain(person: person)
    } else {
        return greet(person: person)
    }
}
print(greet(person: "Tim", alreadyGreeted: true))
// Prints "Hello again, Tim!"

리턴 값이 없는 경우

함수는 리턴 값이 없어도 된다.

func greet(person: String) {
    print("Hello, \(person)!")
}
greet(person: "Dave")
// Prints "Hello, Dave!"

위 코드에서는 함수는 리턴 값이 없기 때문에, 함수 정의에서 -> 화살표를 쓰거나 리턴 타입을 쓸 필요가 없다.

엄격하게 얘기하면 위 예시의 greet(person:) 함수도 리턴 값을 가지고 있다. 함수에서 리턴 값이 정의가 되지 않았지만, 이렇게 리턴 값이 정의되지 않은 함수들은 Void라는 없음을 나타내는 타입을 반환한다. 이는 단순히 빈 튜플로, ()로 표현할 수 있다.

함수의 리턴 값을 무시할 수도 있다.

func printAndCount(string: String) -> Int {
    print(string)
    return string.count
}
func printWithoutCounting(string: String) {
    let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
// prints "hello, world" and returns a value of 12
printWithoutCounting(string: "hello, world")
// prints "hello, world" but doesn't return a value

printWithoutCounting(string:) 함수에서는 첫 번째 함수의 리턴 값을 무시하고 있다.

함수가 리턴하는 값을 무시할 수 있지만, 호출되는 함수는 내가 무시하든 말든 항상 값을 반환한다.

리턴 값이 여러 개인 경우

여러 값을 리턴할 때 이를 튜플로 묶어 하나의 통합된 값을 리턴할 수도 있다.

func minMax(array: [Int]) -> (min: Int, max: Int) {
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)

위 함수에서는 두 개의 Int 값을 포함한 튜플을 리턴하고 있고, 값들은 각각 min, max로 이름이 붙여져 있기 때문에 함수의 리턴 값에서 이를 통해 접근할 수 있다.

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
// Prints "min is -6 and max is 109"

이때 튜플의 멤버들은 함수에서 리턴되었을 때 명명될 필요가 없다는 것에 유의하자. 이미 이름은 함수의 리턴 타입에서 정의되어 있기 때문에 굳이 다시 이름을 붙일 필요가 없는 것이다.

Optional Tuple Return Types

만약 함수가 리턴해야 하는 튜플 타입이 튜플 자체가 “값이 없을” 가능성이 있다면, 튜플이 nil이 될 수 있음을 나타내기 위해 optional 튜플을 리턴 타입으로 사용할 수 있다. 튜플 다음에 물음표를 붙여서 표시한다.

옵셔널 튜플 타입 (Int, Int)?는 옵셔널 타입을 포함한 튜플 (Int?, Int?)와는 다르다. 전자의 경우 전체 튜플 자체가 값이 없거나, 튜플의 값이 있다면 튜플은 무조건 두 개의 옵셔널이 아닌 Int값을 가지고 있다. 반면 후자의 경우 튜플 자체는 무조건 값이 있고(튜플은 항상 존재하고) 내부의 Int 두 개는 각각 값이 있을 수도 있고 없을 수도 있다는 뜻이다.

func minMax(array: [Int]) -> (min: Int, max: Int)? {
    if array.isEmpty { return nil }
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)

이렇게 옵셔널 튜플이 리턴 값일 경우 함수가 리턴한게 실제 튜플 값인지 아니면 nil인지 판단하기 위해 옵셔널 바인딩을 해야 한다. 옵셔널 바인딩은 뒤에서 자세히 볼 것이므로 여기에서는 그냥 이런게 있구나 하고 넘어가면 된다.

if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
    print("min is \(bounds.min) and max is \(bounds.max)")
}
// Prints "min is -6 and max is 109"

Implicit Return

만약 함수 내부의 코드가 하나의 표현이고, 이게 함수의 리턴 타입과 일치한다면 return 키워드를 생략할 수 있다.

func greeting(for person: String) -> String {
    "Hello, " + person + "!"
}
print(greeting(for: "Dave"))
// Prints "Hello, Dave!"

func anotherGreeting(for person: String) -> String {
    return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Dave"))
// Prints "Hello, Dave!"

Function Argument Labels and Parameter Names

함수의 파라미터들은 argument labelparameter name을 가지고 있다. Argument label은 함수를 호출할 때 사용되고, 함수를 호출할 때 인자 전에 이 전달인자 레이블을 쓴다. Parameter name은 함수 내부에서 사용된다. 기본적으로 파라미터들은 그들의 파라미터 이름을 전달인자 레이블처럼 사용한다.

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(firstParameterName: 1, secondParameterName: 2)

파라미터들은 같은 이름을 가질 수 없다. 하지만 여러 파리미터들이 같은 전달인자 레이블을 갖는 건 가능하다. 하지만이게 가능해도 다 다른 전달인자 레이블을 설정하는 것이 코드를 더 가독성 있게 만든다.

Specifying Argument Labels

func someFunction(argumentLabel parameterName: Int) {
    // In the function body, parameterName refers to the argument value
    // for that parameter.
}

func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// Prints "Hello Bill!  Glad you could visit from Cupertino."

Omitting Argument Labels

만약 전달인자 레이블을 사용하고 싶지 않으면 와일드카드 식별자를 사용할 수 있다.

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)

또한 전달인자 레이블을 바꾸면 함수의 이름 자체가 바뀐다. 따라서 전달인자 레이블만 다르게 써도 오버로드로 동작한다.

func someFunction(a firstParameterName: Int, b secondParameterName: Int) {
    // In the function body, firstParameterName and secondParameterName
    // refer to the argument values for the first and second parameters.
}

func someFunction(c firstParameterName: Int, d secondParameterName: Int) {
    // In the function body, firstParameterName and secondParameterName
    // refer to the argument values for the first and second parameters.
}

someFunction(a: 1, b: 2)
someFunction(c: 1, d: 2)

Default Parameter Values

Swift의 함수에서는 매개변수마다 기본값을 지정할 수 있다. 만약 매개변수가 전달되지 않으면 기본값을 사용한다. 매개변수 기본값이 있는 함수 또한 오버로드 한 것처럼 사용할 수 있다.

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

기본값이 없는 매개변수를 기본값이 있는 매개변수 앞에 사용한다. 기본값이 없는 매개변수는 함수에서 중요한 값을 전달할 가능성이 높기 때문이다.

Variadic Parameters

매개변수로 몇 개의 값이 들어올지 모를 때, 가변 매개변수를 사용할 수 있다. 가변 매개변수는 0개 이상의 값을 받아올 수 있고, 가변 매개변수로 들어온 인자 값은 배열처럼 사용할 수 있다. 파라미터 타입 이름 다음에 세 개의 점을 붙여(...) 가변 매개변수를 사용할 수 있다.

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 Parameters

함수 파라미터는 기본적으로 상수다. 함수 파라미터의 값을 함수 내부에서 변경하려고 하면 컴파일 에러가 발생한다. 이는 파리미터의 값을 실수로라도 바꿀 수 없게 해준다. 만약 파라미터의 값을 바꾸고 싶고 이 변화를 함수가 종료된 이후에도 유지하고 싶다면 in-out 파라미터를 선언하면 된다. 즉 in-out 파라미터, 입출력 파라미터는 값이 아닌 참조를 전달하게 된다. 이 방법은 외부의 값에 어떤 영향을 줄지 모르기 때문에 함수형 프로그래밍 패러다임에서는 지양하는 패턴이긴 하다.

입출력 파라미터를 사용하려면 파라미터 타입 전에 inout 키워드를 붙인다. 이말은 함수 으로 전달된 값이 원래의 값을 대체하기 위해 함수의 으로 나왔다고 해석할 수 있다. in-out 매개변수의 전달 순서는 아래와 같다.

  1. 함수를 호출할 때, 전달인자의 값을 복사한다.
  2. 해당 전달인자의 값을 변경하면 앞에서 복사한 값을 내부에서 변경한다.
  3. 함수를 반환하는 시점에 내부에서 변경된 값을 원래의 매개변수에 할당한다.

연산 프로퍼티, 또는 감시자가 있는 프로퍼티가 입출력 매개변수로 전달되면 함수 호출 시점에 프로퍼티의 접근자(getter)가 호출되고 함수의 반환 시점에 프로퍼티의 설정자(setter)가 호출된다.

in-out 파라미터에는 변수만 전달할 수 있다. 상수나 리터럴 값을 인자로 전달할 수 없는 이유는 이들은 수정될 수 없는 값이기 때문이다. 변수 이름 바로 전에 &를 붙여서 함수에서 수정될 수 있다고 표시할 수 있다.

In-out 파라미터는 기본 값을 가질 수 없고, 가변 파리미터는 inout으로 마킹될 수 없다.

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"

In-out 파라미터는 함수에서 값을 리턴하는 거랑 같지 않다. 위 예시에서도 무언가를 리턴하지 않았지만, 변수의 값을 수정했다. In-out 파라미터는 함수 내부가 밖에도 영향을 미칠 수 있게 한다.

Function Types

모든 함수는 function type이라는 게 있다. 이 function type은 파라미터 타입과 함수의 리턴 타입으로 이루어진다.

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라고 할 수 있다. 이는 “함수는 두 개의 Int 타입인 파라미터를 받고, 하나의 Int 를 반환한다” 라는 뜻이다.

그렇다면 아래의 예제에서 function type은 어떻게 될까?

func printHelloWorld() {
    print("hello, world")
}

() -> Void가 된다. 이는 “파라미터가 없고, Void를 리턴하는 함수”임을 의미한다.

Using Function Types

Swift에서는 function type을 다른 타입과 똑같이 사용할 수 있다. 예를 들어 function type을 상수나 변수에 할당하고, 해당 상수나 변수에 적절한 함수를 할당할 수도 있다. 쉽게 생각해서 함수를 Int 값과 같이 취급하는 것이다. 이게 가능한 이유는 Swift의 함수는 일급 객체이기 때문이다.

(매개변수 타입의 나열) -> 리턴 타입

과 같이 쓴다.

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

이러고 위에서 할당한 addTwoInts를 아래와 같이 mathFunction 으로 호출할 수도 있고, 같은 타입의 또 다른 함수를 할당하는데 사용할 수도 있다.

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

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

Function Types as Parameter Types

함수 타입을 일반적인 다른 타입과 똑같이 사용할 수 있다면 함수의 인자 타입으로도 함수 타입을 사용할 수 있을 것이다.

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

Function Types as Return Types

파라미터 타입 뿐만 아니라 리턴 타입으로도 함수 타입을 사용할 수 있다. 리턴 화살표 -> 바로 다음에 함수 타입을 쓰면 된다.

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

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

Argument Label and Function Type 전달인자 레이블은 함수 타입의 구성 요소가 아니기 때문에 함수 타입을 작성할 때는 전달 인자 레이블을 쓸 수 없다.

let someFunction: (lhs: Int, rhs: Int) -> Int // error
let someFunction: (_ lhs: Int, _ rhs: Int) -> Int // ok
let someFunction: (Int, Int) -> Int // ok

Nested Functions

지금까지 위에서 본 함수들은 모두 global function으로, 전역인 영역에서 정의된 함수들이다. Swift는 데이터 타입의 중첩이 자유롭다. 열거형 안에 또 열거형이 들어갈 수도 있도 클래스 안에 또다른 클래스가 들어갈 수도 있다. 함수도 마찬가지로 다른 함수 내에서 정의될 수 있는데, 이를 nested function이라고 한다.

중첩 함수는 기본적으로 밖에서는 접근이 안되지만, 중첩 함수가 반환되면 밖에서도 사용할 수 있다.

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!

종료되지 않는 함수

Swift에서는 종료되지 않는(return하지 않는) 함수가 있다.

종료되지 않는다는 것은 정상적으로 끝나지 않는다는 것을 의미한다. 이를 Nonreturning 함수, Nonreturning 메서드라고 한다. 이런 함수/메서드는 정상적으로 끝날 수 없는 함수로, 이 함수/메서드를 실행하면 프로세스 동작은 끝났다고 할 수 있다.

정상적으로 종료되지 않는다는 게 무슨 의미일까? 즉 함수 내에서 오류를 던진다던가, 중대한 시스템 오류를 보고하고 프로세스를 종료하기 때문이다. 이런 Nonreturning 함수는 어디서든 호출이 가능하고 guard-else 문의 else 블록 내에서도 사용할 수 있다.

Nonreturning 함수/메서드는 반환 타입을 Never라고 명시한다.

func crashOccured() -> Never {
  fatalError("Something very bad happend")
}

crashOccured() // 프로세스 종료 후 에러 보고

func someFunction(allGood: Bool) {
  guard allGood else {
    crashOccured()
  }
}

반환 값을 무시할 수 있는 함수

함수의 반환 값이 필요없는 경우도 있다. 프로그래머가 의도적으로 함수의 반환 값을 사용하지 않을 경우 컴파일러가 함수의 결과 값을 사용하지 않았다는 경고를 보낼 때가 있다. 이때 함수의 반환 값을 무시해도 된다는 @discardableResult 선언 속성을 사용하면 된다.

func ignoreReturnValue() -> String {
  "Hi"
}

ignoreReturnValue() // Compiler can warn

@discardableResult func ignoreAnotherReturnValue() {
  "Hi"
}

ignoreAnotherReturnValue() // 리턴 값을 무시할 수 있다고 미리 선언했기 때문에 컴파일러가 경고하지 않는다.