일급 객체로서의 함수
일급 함수의 특성
객체가 다음의 조건을 만족하는 경우 이 객체를 일급 객체로 간주한다.
- 객체가 런타임에도 생성이 가능해야 한다.
- 인자값으로 객체를 전달할 수 있어야 한다.
- 반환값으로 객체를 사용할 수 있어야 한다.
- 변수나 데이터 구조 안에 저장할 수 있어야 한다.
- 할당에 사용된 이름과 관계없이 고유한 구별이 가능해야 한다.
🚀 함수가 이런 조건을 만족하면 이를 일급 함수라고 하고, 그 언어를 함수형 언어로 분류한다. 즉, 함수형 언어에서는 함수가 일급 객체로 대우받는다.
🚀 함수가 일급 객체로 대우받는다면 런타임에도 함수의 생성이 가능하고, 매개변수나 반환값으로 함수를 전달할 수 있으며, 함수를 변수나 데이터 구조 안에 저장할 수 있을 뿐 아니라 함수의 이름과 관계없이 고유한 구별이 가능하다.
🚀 일급 함수의 특성 (1) - 변수나 상수에 함수를 대입할 수 있음
func foo(base: Int) -> String {
return "결과값은 \(base + 1)입니다.")
}
let fn1 = foo(base: 5)
🚀 함수의 결과값을 변수나 상수에 대입하는 것이 아니라 "함수 자체를 대입"하는 것.
[예시 1] - 변수나 상수에 함수를 대입할 떄에는 함수가 실행되는 것이 아니라 함수라는 객체 자체만 대입된다.
func foo(base: Int) -> String { // 정수 인수를 받고 문자열 타입을 반환하는 함수
print("함수 foo가 실행됩니다.")
return "결과값은 \(base + 1)입니다.")
}
let fn2 = foo(base: 5)
// 실행 결과 >>
"함수 foo가 실행됩니다."
[예시 2] - 함수 자체를 대입하는 구문
let fn4 = foo
// 출력 결과 없음
fn4(7)
// 함수 foo가 실행됩니다!
🚀 함수 타입 작성
(인자 타입1, 인자 타입2, ...) -> 반환 타입
위의 foo 함수를 함수 타입 형태로 표현하면 아래와 같다.
(Int) -> String
foo : 함수의 이름
foo(base:) : 함수의 식별자
🚀 함수 타입에 대한 타입 어노테이션을 누락하는 경우 에러가 발생한다.
func boo(age: Int) -> String {
return "\(age)"
}
func boo(age: Int, name: String) -> String {
return "\(name)의 나이는 \(age)세 입니다.")
}
let t = boo // (X) 위의 두 함수 중 어느 것인지 모호하기 때문
// 해결 방법 1 : 타입 어노테이션을 통해 입력받을 함수의 타입을 지정
let t1: (Int, String) -> String = boo
// 해결 방법 2 : 함수의 식별값을 통해 입력받을 정확한 함수를 지정
let t2 = boo(age:name:)
🚀 타입 어노테이션을 사용하는 경우에는 입력받을 값의 타입이 명확하기 때문에 컴파일러가 알아서 함수를 찾아서 대입할 수 있다.
🚀 그러나 타입 어노테이션과 함수 이름의 조합으로 대입 구문을 구성하면 안되는 경우도 있다. 동일한 함수 타입을 사용하지만 매개변수명이 서로 다른 함수의 경우가 그러하다.
func boo(age: Int, name: String) -> String {
return "\(name)의 나이는 \(age)세 입니다."
}
func boo(height: Int, nick: String) -> String {
return "\(nick)의 키는 \(height)입니다.")
}
let fn03: (Int, String) -> String = boo // (X)
let fn04: (Int, String) -> String = boo // (X)
함수의 식별자를 사용해 명확하게 구분해주어야 한다.
let fn03: (Int, String) -> String = boo(age:name:)
let fn04: (Int, String) -> String = boo(height:nick:)
or
let fn03 = boo(age:name:)
let fn04 = boo(height:nick:)
🚀 다양한 함수 타입
// 인자값이 없는 경우
func foo() -> String {
return "Empty values"
}
// 인자값이 없는 경우의 함수 타입
() -> String
// 반환값이 없는 경우
func boo(base: Int) {
print("param = \(base)")
}
// 반환값이 없는 경우 함수 타입
(Int) -> ()
// 인자값, 반환값 모두 없는 경우
func too() {
print("empty values")
}
// 인자값 반환값 모두 없는 경우의 함수 타입
() -> ()
🚀 함수 타입을 표현할 때 반환값이 없는 경우 빈 괄호 대신 Void를 사용하여 몀시적으로 값이 없음을 표시하기도 함
public typealias Void = ()
(Int) -> Void
() -> Void
🚀 일급 함수의 특성 (2) - 함수의 반환 타입으로 함수를 사용할 수 있음
func desc() -> String {
return "this is desc()"
}
func pass() -> String {
return desc
}
let p = pass()
p()
// "this is desc()"
🚀 일급 함수의 특성 (3) - 함수의 인자값으로 함수를 사용할 수 있음
일급 함수는 반환값으로 함수를 사용할 수 있을 뿐만 아니라 다른 함수의 인자값으로 함수를 전달할 수 있는 특성도 가지고 있다.
func incr(paramL Int) -> Int {
return param + 1
}
// 중개 역할을 하는 함수는 '브로커'라고 부른다.
func broker(base: Int, function fn: (Int) -> Int) -> Int {
return fn(base)
}
broker(base: 3, function: incr) // 4
🚀 콜백 함수를 사용하는 예
func successThrough() {
print("연산 처리 성공")
}
func failThrough() {
print("처리 과정 오류")
}
func divide(base: Int, success sCallBack: () -> Void, fail fCallBack: () -> Void) -> Int {
guard base != 0 else {
fCallBack() // 실패 함수 실행
return 0
}
defer {
sCallBack() // 성공 함수 실행
}
return 100 / base
}
divide(base: 30, success: successThrough, fail: failThrough)
// 실행 결과 >>
연산 처리 성공.
🚀 defer 블록 : 함수나 메소드에서 코드의 흐름과 상관없이 가장 마지막에 실행되는 블록이다. 종료 시점에 맞추어 처리해야 할 구문이 있다면 우리는 어디에 작성해야 할지 고민하지 않고 defer 블록에 넣어두기만 하면 된다.
이 블록은 함수에서 사용된 각종 리소스의 처리나 해제, 연결 종료 등의 구문을 처리하는 용도로 유용하게 사용된다. defer 블록의 특성은 아래와 같다.
- defer 블록은 작성된 위치와 순서에 관계없이 함수가 종료되기 직전에 실행된다.
- defer 블록을 읽기 전에 함수의 실행이 종료될 경우 defer 블록은 실행되지 않는다.
- 하나의 함수나 메소드 내에서 defer 블록을 여러 번 사용할 수 있다. 이때에는 가장 마지막에 작성된 defer 블록부터 역순으로 실행된다.
- defer 블록을 중첩해서 사용할 수 있다. 이때에는 바깥쪽 defer 블록부터 실행되며 가장 안쪽에 있는 defer 블록은 가장 마지막에 실행된다.
defer는 주로 함수가 연산을 처리하는 과정에 영향을 끼치지 않으면서 실행해야 할 다른 내용이 있을 때 사용하거나, 함수를 종료하기 직전에 정리해야 하는 변수나 상수값들을 처리하는 용도로 사용된다. 다시 말해, 함수를 종료하기 전에 처리해야 하는 변수나 상수들 중에서 처리 시점이 모두 달라서 적절한 처리 시점을 잡기 어려울 때 defer 구문을 통해 처리하면 된다는 뜻이다.
🚀 익명 함수 : 함수의 형태를 가지지만 이름이 부여되지 않으며, 일회용이기 때문에 여러 가지 간편한 작성 형식을 따른다는 특성이 있다. 스위프트에서는 클로저(Closure)라고 부른다.
divide(base: 30,
success: {
() -> Void in
print("연산 처리 성공")
},
fail: {
() -> Void in
print("처리 과정 오류")
}
}
함수의 중첩
🚀 스위프트에서 함수는 중첩해서 사용할 수 있다. 함수 내에 다른 함수를 작성해서 사용할 수 있다는 것이다. 이렇게 작성된 함수를 중첩 함수라고 한다. 함수 내에 작성된 함수는 내부 함수, 내부 함수를 포함하는 바깥쪽 함수는 외부 함수로 구분한다.
🚀 함수를 중첩해서 정의하면 내부 함수는 외부 함수가 실행되는 순간 생성되고 종료되는 순간 소멸한다. 외부 함수는 프로그램이 실행될 때 생성되고 프로그램이 종료될 때 소멸하지만, 내부 함수는 외부 함수의 실행과 종료 사이에서 생겼다가 소멸한다. 이것이 내부 함수의 생명주기 이다.
// 외부 함수
func outer(base: Int) -> String {
// 내부 함수
func inner(inc: Int) -> String {
return "\(inc)를 반환한다."
}
let result = inner(inc: base + 1)
return result
}
outer(base: 3)
let fn1 = outer(param: 3) // outer() 가 실행되고, 그 결과로 inner가 대입된다.
// 위의 경우 내부함수인 inner는 결과값으로 반환되어 상수에 참조되었으므로 참조 카운트가 존재한다.
// 이로 인해 함수의 종료에도 혼자 생명을 유지할 수 있다.