프로그래밍/iOS

[Swift/Ch6] 함수: Functions

장장꾸 2023. 2. 14. 13:46

함수는 특정 기능을 수행하는 코드의 묶음이다. 함수가 어떤 역할을 하는지 이름을 붙여주면, 필요할 때 함수를 call하여 그 기능을 수행할 수 있다.

 

Swift의 통일된 함수 문법은 매개변수 이름이 없는 단순한 C 스타일의 함수부터 각 매개변수의 이름과 인자 레이블이 있는 Objective C 스타일의 복잡한 함수까지 표현할 수 있을 만큼 충분히 유연하다. 매개변수는 함수 호출을 단순화하기 위해서 기본 값을 제공하고, 함수 실행이 완료되면 전달된 변수를 수정하는 인아웃 매개변수로 전달될 수 있다.

 

Swift의 모든 함수는 함수의 매개변수 타입과 리턴 타입을 포함하여, 타입을 가지고 있다. 함수를 다른 함수에 매개변수로 전달하기 쉽도록, 함수에서 함수를 리턴하기 쉽도록 이러한 타입을 사용할 수 있다. 또한 함수는 중첩된 함수 범위 안에서 유용한 기능을 캡슐화하기 위해 다른 함수 안에 쓰일 수 있다.

 

함수를 정의하고 호출하기(Defining and Calling Functions)

함수를 정의할 때, 이름과 타입이 있는 하나 이상의 값, 즉 함수가 input으로 갖는 매개변수를 선택적으로 정의할 수 있다. 또한 함수가 완료되면 output으로 전달하는 값의 타입인 리턴 타입을 선택적으로 정의할 수 있다.

 

모든 함수는 함수가 수행하는 기능을 설명하는 함수 이름을 가진다. 함수를 사용하기 위해서는 함수의 이름을 input 값들(인자)을 매개변수의 타입과 맞추어서 함께 call(호출)해야 한다. 함수의 인자는 항상 매개변수 리스트의 순서와 같은 순서로 주어져야 한다.

 

아래 예시의 함수는 사람의 이름을 input으로 받아 그 사람에게 인사를 리턴하는 greet(person: )이라는 함수이다.

 

func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

 

모든 정보는 func 키워드로 시작되어 모든 정보가 함수의 정의로 롤업된다. -> 로 리턴 타입을 명시한다. 함수의 정의는 함수가 뭘 하는지, 어떤 값을 받을 것인지, 어떤 값을 리턴할 것인지를 설명한다. 정의는 함수가 코드의 어디에서든 모호하지 않게 호출되도록 한다.

 

print(greet(person: "Anna"))
// 출력 결과: Hello, Anna!
print(greet(person:"Brian"))
// 출력 결과: Hello, Brian!

 

greet(person: ) 함수가 String 값을 리턴하기 때문에, print() 함수 안에 넣어서 리턴한 결과를 확인할 수 있다. 위의 함수를 더 줄여서 아래와 같이 작성할 수 있다.

 

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

print(greetAgain(person: "Anna"))
// 출력 결과: Hello again, Anna!

 

함수 매개변수와 리턴 값(Function Parameters and Return Values)

- 매개변수가 없는 함수

함수는 매개변수가 꼭 있어야 하는 것은 아니다. 아래 예시는 매개변수가 없는 함수이고, 호출될 때마다 String 메시지를 리턴한다. 정의될 때나 호출될 때, 매개변수가 없는 경우에도 함수 이름 옆에 ( ) 괄호는 필요하다.

 

func sayHelloWorld() -> String {
    return "hello, world"
}
print(sayHelloWorld())
// 출력 결과: 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))
// 출력 결과: Hello again, Tim!

 

- 리턴 값이 없는 함수

아래의 함수는 String 값을 리턴하지 않고 출력한다. 값을 리턴할 필요가 없기 때문에 함수 정의 부분에서 -> 또는 리턴 타입을 명시하지 않는다.

 

func greet(person: String) {
    print("Hello, \(person)!")
}
greet(person: "Dave")
// 출력 결과: Hello, Dave!

 

함수의 리턴 값이 무시될 수도 있다. 아래 첫번째 함수는 문자열을 출력하고, 문자의 개수를 정수 타입으로 리턴한다. 두 번째 함수는 첫 번째 함수를 호출하지만 리턴 값을 무시한다. 두 번째 함수가 호출되었을 때, 첫 번째 함수에 의해 메시지는 출력되지만, 리턴된 값은 사용되지 않는다. 

 

func printAndCount(string: String) -> Int {
    print(string)
    return string.count
}

func printWithoutCounting(string: String) {
    let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
// 출력 결과: hello, world
// 12를 리턴
printWithoutCounting(string: "hello, world")
// 출력 결과: hello, world
// 값을 리턴하지 않음

 

- 리턴 값이 여러 개인 함수

아래의 minMax(array: ) 함수는 주어진 정수 배열 중에서 최솟값과 최댓값을 찾아낸다. 이 함수는 두 정수 값을 포함한 하나의 튜플을 리턴한다. 이 값들은 min, max라고 이름 붙여졌기 때문에 함수의 리턴 값을 찾을 때 이 이름으로 접근한다.

 

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)
}

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

 

함수의 리턴 타입의 일부로 튜플 멤버의 이름이 이미 명시되었기 때문에, 함수로부터 리턴되는 시점에는 튜플 멤버의 이름은 쓸 필요가 없다.

 

- 옵셔널 튜플 리턴 타입

함수로부터 튜플이 리턴될 때 그 튜플 전체가 값이 없을 수도 있는 가능성이 있다면, 튜플 전체가 nil이 될 수 있다는 것을 반영하기 위해 옵셔널 튜플 리턴 타입을 사용할 수 있다. (Int, Int)? , (String, Int, Bool)? 과 같이 ?를 추가하여 옵셔널 튜플 리턴 타입이라는 것을 표현할 수 있다.

 

(Int, Int)? != (Int?, Int?)이다. 옵셔널 튜플 리턴 타입은 전체 튜플이 옵셔널이라는 의미이지, 튜플 안의 각 값이 옵셔널이라는 뜻이 아니다.

 

위에서 봤던 minMax(array: ) 함수는 전달받은 배열에 대한 안전성 검사를 수행하지 않았다. 만약 array 인자가 빈 배열이라면 array[0]에 접근하는 과정에서 런타임 에러가 발생할 것이다. 안전하게 빈 배열을 처리하기 위해서는 옵셔널 튜플 리턴 타입으로 작성하여 배열이 비어있을 때 nil을 리턴하도록 작성하면 된다.

 

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)
}

 

minMax(array: ) 함수가 실제 튜플을 리턴하는지 nil을 리턴하는지 확인하기 위하여 옵셔널 바인딩을 사용할 수도 있다.

 

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

 

- 암시적 리턴을 하는 함수(Functions With an Implicit Return)

함수의 body가 하나의 표현으로 되어있다면, 함수는 그 표현을 암시적으로 리턴한다. 다시 말해서, 함수의 내용이 return을 포함하여 한 줄뿐이라면 return을 생략할 수 있다. 아래의 함수들은 같은 동작을 한다.

 

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

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

 

인자 레이블과 매개변수명(Function Argument Labels and Parameter Names)

각 매개변수는 인자 레이블과 매개변수명을 가진다. 인자 레이블은 함수를 호출할 때 쓰인다. 매개변수명은 함수를 구현할 때 쓰인다. 기본 값으로, 매개변수는 매개변수명을 인자 레이블로 쓴다.

 

모든 매개변수는 고유의 이름을 가져야 한다. 여러 개의 매개변수가 같은 인자 레이블을 가질 수 있으나, 고유한 인자 레이블은 코드의 가독성을 높여줄 것이다.

 

- 인자 레이블 명시(Specifying Argument Labels)

매개변수명 앞에 띄어쓰기로 구분한 인자 레이블을 쓴다.

 

func someFunc ( argLabel paramName: Int ) {

    // 함수 body 안에서 paramName(매개변수명)은 argLabel(인자 레이블)을 가리킨다.

}

 

앞에서 보았던 greet(person: ) 함수를 변형하였다. 인자 레이블은 함수의 body가 의도가 분명하고 가독성이 좋게 함과 동시에, 함수를 표현적이고, 문장스럽도록 만든다.

 

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

 

- 인자 레이블 제거(Omitting Argument Labels)

인자 레이블을 붙이고 싶지 않다면, _ (underscore)를 사용하면 된다. 매개변수가 인자 레이블을 가지고 있다면, 함수를 호출할 때 인자는 꼭 레이블이 있어야 한다. 

 

func someFunction(_ firstParamName: Int, secondParamName: Int) {
    // firstParamName, secondParamName은 함수의 body에서 첫번째, 두번째 매개변수로서 인자의 값을 가리킴
}
someFunction(1, secondParamName: 2)

 

- 매개변수 기본 값(Default Parameter Values)

매개변수의 타입 옆에 값을 할당함으로써 함수의 어떤 매개변수에도 디폴트 값을 정의할 수 있다. 디폴트 값이 정의되면, 함수를 호출할 때 그 매개변수는 생략할 수 있다.

 

func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
    // 함수를 호출할 때 두번째 인자를 생략하면, 함수 body에서 parameterWithDefault는 12
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) 
// parameterWithDefault == 6
someFunction(parameterWithoutDefault: 4) 
// parameterWithDefault == 12

 

디폴트 값을 갖지 않는 매개변수를 매개변수의 앞쪽에 놓아라. 디폴트 값이 없는 매개변수들은 보통 함수의 의미에 더 중요하다.

 

- 가변 매개변수(Variadic Parameters)

가변 매개변수는 정해진 타입의 0개 이상의 값을 허용한다. 함수가 호출되었을 때 다양한 개수의 input 값을 매개변수로 전달받는 경우에 가변 매개변수를 사용한다. 매개변수의 타입명 뒤에 점 세 개 ( ... )를 붙여서 가변 매개변수를 표현한다.

 

가변 매개변수로 전달된 값들은 함수의 body에서 적절한 타입의 배열로 사용할 수 있도록 만들어진다. 예를 들어, Double 타입의 numbers라는 가변 매개변수는 함수의 body에서 상수 배열 [Double] 타입의 numbers로 사용 가능해진다.

 

아래의 예시는 개수가 정해지지 않은 여러 개의 숫자가 주어졌을 때, 산술 평균을 구하는 예시이다.

 

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)
// 3.0을 리턴(1,2,3,4,5의 평균)
arithmeticMean(3, 8.25, 18.75)
// 10.0 리턴(이 세 개 숫자의 평균)

 

가변 매개변수 이후에 들어오는 첫번째 매개변수는 반드시 인자 레이블이 있어야 한다. 인자 레이블이 있어야 어떤 것이 가변 매개변수이고, 가변 매개변수 이후에 들어온 매개변수인지를 알 수 있기 때문이다. 

 

- 인아웃 매개변수(In-Out Parameters)

함수의 매개변수는 기본적으로 상수이다. 매개변수의 값을 함수의 body에서 바꾸려고 하면 컴파일 에러가 발생한다. 이것은 실수로라도 매개변수의 값을 바꿀 수 없다는 의미이다. 매개변수의 값을 바꾸고, 바꾼 값이 함수 호출 이후에도 유지되게 하고 싶다면 매개변수를 인아웃 매개변수로 정의해야 한다.

 

inout 키워드를 매개변수 타입 바로 앞에 써서 인아웃 매개변수를 사용한다. 인아웃 매개변수는 함수에 전달되는(in) 값을 가지고, 함수에 의해 변경되며, 함수 밖으로 전달되어(out) 원래의 값을 대체한다.

 

인아웃 매개변수에는 오로지 변수만을 인자로 전달할 수 있다. 상수나 리터럴 값은 변경이 불가능하기 때문에 인자로 전달할 수 없다. 변수를 인자로 전달할 때는 변수명 앞에 앰퍼샌트(&)를 붙여서 함수에 의해 값이 변경될 수 있음을 표시한다. 인아웃 매개변수는 디폴트값을 가질 수 없고 가변 매개변수는 inout으로 표시될 수 없다.

 

아래의 함수는 두 인아웃 정수 매개변수 a, b를 가진다. 이 함수는 단순히 두 값을 서로 바꾼다. a의 값을 임시 변수 tempA에 넣어두고, b를 a에 할당한 다음 b에 tempA를 할당함으로써 스왑을 수행한다.

 

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let tempA = a
    a = b
    b = tempA
}

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 출력 결과: someInt is now 107, and anotherInt is now 3

 

두 정수를 넣어서 위 함수를 호출할 수 있다. someInt, anotherInt 두 변수는 앰퍼샌트 기호 &를 앞에 붙여서 함수에 전달해야 한다. 두 변수는 함수의 바깥에서 정의되었지만, 함수에 의해서 두 변수의 원래 값이 변경되었다.

 

인아웃 매개변수는 함수가 값을 리턴하는 것과는 다르다. 위 예시에서 swapTwoInts는 타입을 정의하지도 않고 값을 리턴하지도 않는다. 그러나 someInt, anotherInt의 값을 변경한다. 인아웃 매개변수는 함수의 body 영역 밖에도 영향을 줄 수 있는 또 다른 방법이다.

 

함수 타입(Function Types)

모든 함수는 매개변수 타입과 리턴 타입으로 구성된 함수 타입을 가지고 있다. 위 두 함수는 두 정수를 받아서 연산을 수행한 후 하나의 정수를 리턴한다. 두 함수의 타입은 (Int, Int) -> Int 이다.

 

func addTwoInt(_ a: Int, _ b: Int) -> Int {
    return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b
}

 

다음 함수는 매개변수나 리턴 값이 없는 함수이다. 이 함수의 타입은 () -> Void 이다.

 

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

 

- 함수 타입 사용(Using Function Types)

함수 타입으로 상수/변수를 정의할 수 있고, 그 변수에 적절한 함수를 할당할 수 있다. 

"mathFunction이라는 변수를 정의하라. 이 변수는 '두 정수를 받아서 하나의 정수를 리턴하는 함수'의 타입을 갖는다. 이 변수는 addTwoInts라는 함수를 참조한다."를 다음과 같이 표현할 수 있다.

 

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

print("Result: \(mathFunction(2, 3))")
// 출력 결과: Result: 5

 

addTwoInts(_:_: ) 함수는 mathFunction 변수와 같은 타입을 갖기 때문에 Swift의 type-checker에게 허용된 할당이다.

 

함수 타입이 아닌 변수와 같은 방식으로, 같은 변수에 타입이 매칭되는 다른 함수를 할당할 수 있다. ---- (1)

다른 타입과 같이, 함수를 상수나 변수에 할당하면 Swift가 자동으로 함수 타입으로 추론한다. ---- (2)

 

mathFunction = multiplyTwoInts           // ----(1)
print("Result: \(mathFunction(2, 3))")
// 출력 결과: Result: 6

let anotherMathFunction = addTwoInts     // ----(2)
// (Int, Int) -> Int 로 타입을 추론함

 

- 매개변수 타입으로서 함수 타입(Function Types as Parameter Types)

(Int, Int) -> Int와 같은 함수 타입을 다른 함수의 매개변수 타입으로 사용할 수 있다. 이를 통해 함수가 호출될 때 함수 호출자가 제공할 함수 구현의 일부를 남길 수 있다.

 

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

 

위 printMathResult 함수는 세 개의 매개변수를 갖는다. 첫 번째 매개변수는 mathFunction이라 불리며 (Int, Int) -> Int 타입이다. 첫번째 매개변수의 인자로 이러한 타입을 갖는 어떤 함수든 전달할 수 있다. 두번째, 세번째 정수 타입의 a, b 매개변수는 제공된 수학 함수의 input 값으로 쓰인다.

 

이 함수가 호출되고, addTwoInts(_:_: ) 함수와 3, 5가 전달되었다. 3, 5로 제공된 함수를 호출하고 결과인 8을 출력한다.

 

printMathResult(_:_:_: ) 함수의 역할은 적절한 타입의 수학 함수를 호출하여 그 결과를 출력하는 것이다. 함수의 구현이 실제로 무엇을 하는지는 중요하지 않다. 오로지 올바른 타입의 함수가 주어졌는지가 중요하다. 이는 printMathResult(_:_:_: ) 함수가 type-safe한 방식으로 그것의 기능을 함수의 호출자에게 넘길 수 있게 한다.

 

- 리턴 타입으로서 함수 타입(Function Types as Return Types)

함수 타입을 다른 함수의 리턴 타입으로 사용할 수 있다. 리턴하는 함수의 -> 뒤에 완전한 함수의 타입을 적으면 된다.

 

stepForward(_: ) 함수는 input 값에 +1된 값을 리턴하고, stepBackward(_:) 함수는 input 값에 -1된 값을 리턴한다. 두 함수는 (Int) -> Int 타입을 갖는다.

 

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

 

chooseStepFunction(backward: ) 함수의 리턴 타입은 (Int) -> Int 이다. 이 함수는 backward라는 불리언 매개변수를 기반으로 stepForward(_:) 또는 stepBackward(_:)를 리턴한다. 이 함수는 currentValue가 점차 0에 가까워지도록 하기 위해서 양의 방향으로 이동해야하는지, 음의 방향으로 이동해야하는지 결정한다.

 

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

var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero는 이제 stepBackward() 함수를 참조함

 

currentValue의 초깃값은 3이므로 이때 currentValue > 0 은 true이다. 따라서 stepBackward 함수가 리턴된다. 리턴된 함수의 참조 결과는 moveNearerToZero 상수에 저장된다.

 

print("Counting to zero:")
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")

// 출력 결과:
// 3...
// 2...
// 1...
// zero!

 

중첩된 함수(Nested Functions)

이번 챕터에서 봤던 모든 함수는 전역 범위에서 정의되었던 전역 함수의 예시였다. 다른 함수의 body 안에서도 함수를 정의할 수 있다. 이를 중첩된 함수(nested function)라 부른다.

 

중첩 함수는 기본적으로 외부 영역으로부터 숨겨져있지만, 그 함수를 감싸고 있는 함수 내에서는 여전히 호출 가능하고 사용 가능하다. 그를 감싸고 있는 함수도 그 안의 중첩 함수를 리턴할 수 있고, 중첩 함수를 다른 영역에서 사용 가능하도록 할 수 있다.

 

위에서 보았던 chooseStepFunction(backward: ) 함수를 아래와 같이 다시 작성할 수 있다.

 

func chooseStepFunction(backward: Bool) -> (Int) -> Int {     // 외부 함수
    func stepForward(input: Int) -> Int { return input + 1 }  // 내부 함수 1
    func stepBackward(input: Int) -> Int { return input - 1 } // 내부 함수 2
    return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero는 이제 중첩된 stepForward() 함수를 참조

while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")

// 출력 결과:
// -4...
// -3...
// -2...
// -1...
// zero!

 

 

 

 

 


* 원문:

https://docs.swift.org/swift-book/LanguageGuide/Functions.html