Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Functions 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
함수는 특정 task를 수행하는 자기완결성(self-contained)을 가진 코드들의 집합이다. Swift에서의 함수 문법은 C 스타일의 함수부터 Objective-C 스타일의 메서드까지 모두 표현할 수 있을만큼 유연하다. 함수 파라미터에 디폴트 값을 설정할 수도 있고, 변수를 in-out 파라미터로 넘겨서 함수 실행 후 해당 변수의 값이 변하게 만들 수도 있다.
Swift의 모든 함수는 타입을 가진다. 이 타입이라는 것은 함수의 파라미터 타입과 리턴 타입으로 구성되어 있으며, Swift에서 쓰이는 다른 타입들과 마찬가지로 다룰 수 있다. 따라서 함수의 파라미터로 다른 함수를 넘기거나 함수에서 함수를 리턴하는 것이 용이해진다. 또한 기능 캡슐화를 위해 함수는 다른 함수의 내부에 쓰여질 수도 있다.
Defining and Calling Functions
함수를 정의할 때는, 함수가 input으로 받을 값의 타입과 이름, 즉 parameters를 하나 또는 그 이상으로 정의할 수 있으며, 함수의 실행이 끝났을 때 돌려줄 값의 타입, 즉 return type을 정의할 수 있다.
모든 함수는 이름을 가진다. 함수의 이름은 해당 함수가 수행하는 일을 잘 설명할 수 있어야 한다.
함수는 arguments를 넘기며 함수의 이름을 call 하는 것으로 사용할 수 있다. argument는 함수의 parameter와 타입이 일치해야 하며 순서도 일치해야 한다.
예제를 보자. greet(person:)라는 함수다. 함수의 이름이 함수의 역할을 설명해주고 있다. 이 함수는 personName이라는 이름의 String 타입 파라미터를 가지며, String 리턴 타입을 가진다. 리턴 타입을 정의할 때는 함수 이름 옆에 -> 을 쓴다.
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
print(greet(person: "Anna"))
// prints "Hello, Anna!"
print(greet(person: "Brian"))
// prints "Hello, Brian!"
// 한 줄로 처리할 수 있다
func greetAgain(person: String) -> String {
return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// prints "Hello again, Anna!"
함수 내에서 return 을 하면 그 즉시 함수의 실행이 종료되고 리턴값이 반환된다.
<note> print(_:separator:terminator:) 함수는 첫번째 argument에 대한 label이 없다. 또한 나머지 argument들은 default 값을 가지고 있기 때문에 optional argument이다. 따라서 print 함수를 호출할 때 단순히 출력할 상수/변수만 넣을 수 있는 것이다.
Function Parameters and Return Values
Swift에서 함수 파라미터와 리턴값은 극도로 유연하다. 이름없는 파라미터를 사용하는 심플한 기능성 함수에서부터, 명시적인 파라미터 이름을 사용하고 각각 파라미터 옵션이 다른 복잡한 함수까지 전부 정의할 수 있다. 하나씩 살펴보자.
<Functions Without Parameters>
함수에 항상 파라미터를 정의할 필요는 없다. 파라미터가 필요없는 함수의 예제를 보자.
func sayHelloWorld() -> String {
return "hello, world"
}
print(sayHelloWorld())
// prints "hello, world"
// <참고> 파라미터가 없더라도 함수 call 시에는 괄호를 써줘야 한다
<Functions With Multiple Parameters>
함수에 여러 개의 파라미터를 정의할 수 있다. 다음의 예제를 보자.
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!"
// <참고> 파라미터 이름을 명시하더라도 순서가 바뀌면 컴파일 에러가 난다
<Functions Without Return Values>
함수에 항상 리턴값이 필요한 것은 아니다. 다음의 예제를 보자.
func greet(person: String) {
print("Hello, \(person)!")
}
greet(person: "Dave")
// prints "Hello, Dave!"
func printAndCount(string: String) -> Int {
print(string)
return string.characters.count
}
func printWithoutCounting(string: String) {
let _ = printAndCount(string: string)
// printAndCount에서 리턴된 값을 사용하지 않음
}
printAndCount(string: "hello, world")
// prints "hello, world" and returns a value of 12
// 리턴된 값을 사용하지 않음
printWithoutCounting(string: "hello, world")
// prints "hello, world" but does not return a value"
<note> 리턴값이 정의되지 않은 함수도 사실은 Void 라는 리턴값을 반환한다. Void는 ( ) 라고 쓸 수 있는 empty tuple이다.
<note> 리턴값이 있다고 명시한 함수는 반드시 리턴을 해야한다. (함수를 호출한 곳에서 그 리턴값을 사용하지 않더라도)
<Functions with Multiple Return Values>
함수는 여러 개의 값을 리턴할 수 있다. 이때 여러 개의 값은 하나로 합성이 되어있어야 한다. 예제를 보자. tuple 타입의 값을 리턴하는 함수이다.
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)
// 리턴 시점에서 tuple의 요소들의 이름을 지을 필요는 없다.
// 이미 함수 파라미터에 명시가 되어있기 때문에
// min과 max라는 이름이 이미 붙어있다.
}
let bounds = minMax([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>
만약 함수의 반환값이 tuple 타입이고 이 값이 nil일 가능성이 있다면 Optional Tuple 타입을 리턴타입으로 정의해야 한다. (Int, Int)? 혹은 (String, Int, Bool)? 과 같이 뒤에 ? 마크를 붙인다.
<note> (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)
}
if let minMaxTuple = minMax([]) {
print(minMaxTuple.min)
} else {
print("Empty!")
}
Function Argument Labels and Parameter Names
함수의 파라미터는 argument label 과 parameter name 을 가진다.
argument label은 함수를 호출할 때 arguments에 이름을 지정하기 위해 사용된다.
parameter name 은 함수 내부 구현 안에서 사용이 된다.
따로 정의하지 않는 경우 parameter name과 argument label은 같은 이름을 사용하게 된다.
func someFunction(firstParameterName: Int,
secondParameterName: Int) {
// function body 시작
// firstParameterName 과 secondParameterName 은
// 함수로 넘어오는 첫 번째와 두 번째 argument 값을 각각 가리킨다
}
someFunction(firstParameterName: 1, secondParameterName: 2)
함수 하나에서 쓰이는 parameter name들은 반드시 서로 구분되는 이름(unique name)을 가져야 한다. argument label의 경우에는 여러 개가 동일한 이름을 가져도 컴파일 에러는 나지 않지만, 코드의 가독성을 위해 당연히 서로 구분되게 정의하는 것이 좋다.
<Specifying Argument Labels>
argument label을 정의하는 방법은 parameter name의 왼쪽에 스페이스바로 구분하여 명시하는 것이다. argument label이 따로 정의되어 있는 파라미터는 함수를 호출할 때 꼭 argument label을 명시해주어야 한다.
func someFunction(argumentLabel parameterName: Int) {
// parameterName refers to the argument value
// 밖에서 function call을 할 때는
// argumentLabel이라는 이름에 argument를 넘기고
// function body 에서는
// parameterName이라는 이름을 사용하여 구현을 한다
}
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."
// 아래 라인은 Compile Error
print(greet(person: "Bill", hometown: "Cupertino"))
이렇게 함수 밖과 안에서 각각 다른 이름을 지을 수 있게 만든 목적은? argument label은 함수를 명시적이고 말이 되게끔(sentence-like) 호출할 수 있게 만들어준다. parameter name은 함수 내부 구현을 가독성있고 의도한 바를 명확하게 (clear in intent) 보일 수 있게 해준다.
<Omitting Argument Labels>
함수를 호출할 때 argument label을 명시하지 않게 하고 싶다면 언더바( _ )로 argument label을 정의하자.
func someFunction(_ firstParameterName: Int,
secondParameterName: Int) {
// function body안에서는
// firstParameterName과 secondParameterName 사용
}
someFunction(1, secondParameterName: 2)
<Default Parameter Values>
모든 함수 파라미터는 디폴트 값을 설정할 수 있다. 디폴트 값이 정의된 파라미터는 함수 호출 시 생략할 수 있다.
func someFunction(parameterWithoutDefault: Int,
parameterWithDefault: Int = 12) {
// 두 번째 argument가 넘어오지 않으면
// parameterWithDefault 는 자동으로 12 가 된다
}
someFunction(parameterWithoutDefault: 3,
parameterWithDefault: 6)
// parameterWithDefault is 6
someFunction(parameterWithoutDefault: 4)
// parameterWithDefault is 12
<note> 디폴트 값이 없는 파라미터들을 디폴트 값이 있는 파라미터들보다 앞에 위치시켜라. 그 편이 가독성에 좋다. (디폴트 값이 없는 파라미터들이 보통 함수에서 더 중요한 역할을 한다)
<Variadic Parameters>
variadic parameter 는 가변 개수의 파라미터를 뜻한다. 특정 타입의 값을 0개 이상 받을 수 있다. 파라미터의 타입 이름 옆에 마침표 세 개 (...)를 써서 사용할 수 있다. variadic parameter 는 함수 내부에서 적합한 타입의 배열로 만들어져서 사용된다. 예제를 보자.
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
arithmeticMean(3, 8.25, 18.75)
// returns 10.0
<note> 함수는 최대 1개의 variadic parameter를 가질 수 있다.
<In-Out Parameters>
함수의 파라미터는 디폴트가 상수이다. 따라서 함수 내부에서 파라미터값을 변경하려고 하면 컴파일 에러가 난다. 이것은 코딩 실수를 줄여주지만, 어떤 경우에는 파라미터의 값을 함수 내부에서 조작하고 이 값이 원본에도 적용되는 것을 원할 수도 있다. 이럴 때 사용하는 것이 in-out parameter이다.
inout 키워드를 붙이면 in-out 파라미터가 된다. 그러면 함수로 넘겨받은 (in) 값이 함수 내부에서 변경되고 이것이 함수 밖으로 나가서 (out) 원본 값을 대체하게 된다. in-out 파라미터의 자세한 원리는 나중에 다룬다.
in-out 파라미터 argument로 넘길 수 있는 것은 오직 변수 뿐이다. 상수나 문자열 리터럴 등은 나중에 변경할 수 없기 때문이다. in-out 파라미터로 넘길 변수는 & 를 변수 이름에 붙여 함수를 호출하면 된다.
<note> In-out 파라미터는 디폴트 값을 가질 수 없으며 variadic parameters도 될 수 없다.
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
모든 함수는 function type을 가진다. fuction type은 함수의 파라미터들 타입들과 리턴 타입으로 구성되어 있다.
// function type : (Int, Int) -> Int
// (두 개의 int 타입의 파라미터를 가지고 int 타입의 값을 리턴한다)
func addTwoInts(_ a: Int, _ b: Int) -> Int {
return a + b
}
// function type : (Int, Int) -> Int
// function type 이 addTwoInts와 같다
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
return a * b
}
// function type : () -> Void
// (파라미터가 없고 Void를 리턴한다)
func printHelloWorld() {
print("hello, world")
}
<Using Function Types>
Swift에서 함수 타입은 다른 타입들이 사용되는 것처럼 똑같이 사용할 수 있다.
// 함수 타입의 상수나 변수를 만들어서 적절한 함수를 할당
var mathFunction: (Int, Int) -> Int = addTwoInts
// 할당한 함수를 호출해서 사용할 수 있다
print("Result: \(mathFunction(2, 3))")
// prints "Result: 5"
// 다른 상수나 변수에 할당하기. 다른 변수와 마찬가지임
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// prints "Result: 6"
// 함수 타입을 할당받은 상수/변수의 타입은 컴파일러에 의해 유추된다
let anotherMathFunction = addTwoInts
// anotherMathFunction 은 (Int, Int) -> Int 타입이 된다
<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"
// printMathResult 함수는 mathFunction의 내부구현이 무엇인지 몰라도 됨
// 단지 함수 타입이 일치하는지 체크만 한다
// 부가설명
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
func mathFuntion(mainFunction: (inout Int, inout Int) -> (),
_ a: inout Int, _ b: inout Int) {
mainFunction(&a, &b)
}
var a = 4
var b = 5
mathFuntion(mainFunction: swapTwoInts, &a, &b)
print("value of a is \(a), value of b is \(b)")
// prints "value of a is 5, value of b is 4"
<Function Types as Return Types>
함수 타입을 다른 함수의 리턴 타입으로 사용할 수도 있다. 함수의 파라미터 정의 옆의 -> 옆에 바로 함수 타입을 쓰면 된다.
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
// parameter : backwards: Bool
// return type : (Int) -> (Int)
return backwards ? stepBackward : stepForward
}
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backwards: currentValue > 0)
// moveNearerToZero : stepBackward() function
print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// 3...
// 2...
// 1...
// zero!
Nested Functions
지금까지 예제로 본 함수들은 전부 global scope 를 가지는 global function들이었다. 함수의 정의가 global 영역에 되었기 때문이다. 이와 다르게 함수 안에 다른 함수를 정의할 수도 있는데, 이런 함수를 nested function 이라고 부른다. nested function은 scope가 그 함수를 감싸고 있는 함수(enclosing function)이며, 바깥에서는 보이지 않도록 감추어져 있는 것이 기본적이다.
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backwards ? stepBackward : stepForward
}
// stepForward, stepBackward 는 이 함수 바깥에서 부를 수 없다
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 공식 가이드 > Swift 3' 카테고리의 다른 글
Enumerations (0) | 2017.02.27 |
---|---|
Closures (0) | 2017.02.21 |
Control Flow (0) | 2017.02.15 |
Collection Types - Dictionary (0) | 2017.02.11 |
Collection Types - Set (0) | 2017.02.11 |