함수는 대부분의 프로그래밍 언어에서 지원하는 개념으로, 프로그래밍 실행 과정 중에서 독립적으로 처리될 수 있는 부분을 분리하여 구조화한 객체를 의미한다.
🚀 함수 사용의 이점
- 동일한 코드가 여러 곳에서 사용될 때 이를 함수화하면 재작성할 필요 없이 함수 호출만으로 처리할 수 있다.
- 전체 프로세스를 하나의 소스 코드에서 연속적으로 작성하는 것보다 기능 단위로 함수화하면 가독성이 좋아지고 코드와 로직을 이해하기 쉽다.
- 비즈니스 로직을 변경해야 할 때 함수 내부만 수정하면 되므로 유지보수가 용이하다.
사용자 정의 함수
func 함수이름(매개변수1: 타입, 매개변수2: 타입, ...) -> 반환 타입 {
실행 내용
return 반환값
}
// 매개변수 생략 가능
func 함수이름() -> 반환 타입 {
실행 내용
return 반환값
}
// 반환값이 없는 void 타입 정의 가능
func 함수이름(매개변수1: 타입, 매개변수2: 타입, ...) {
실행 내용
}
func 함수이름() {
실행 내용
}
- 반환 타입과 반환값의 타입은 반드시 일치해야 한다.
함수의 호출
func printHello() {
print("안녕하세요")
}
func printHelloWithName(name: String) {
let returnValue = "\(name)님 안녕하세요"
return returnValue
}
위와 같이 함수가 정의되어 있을 때,
printHello()
// 실행 결과
안녕하세요
- 매개변수가 없는 함수는 그냥 빈 괄호만 붙여서 호출하면 되지만, 매개변수를 가지는 함수는 호출할 때 괄호 안에 인자값을 넣어 호출해주어야 한다. 이때 인자값은 앞에서 정의된 매개변수 타입과 일치해야 한다.
let inputName = "수빈"
printHelloWithName(name: inputName)
// 실행 결과
수빈님 안녕하세요
- 간혹 불필요한 코드를 줄이기 위해 인자값으로 변수나 상수 대신 실제 값 자체 (리터럴) 을 넣어주기도 한다.
printHelloWithName(name: "수빈")
🚀 다른 언어로 프로그래밍해본 경험이 있다면, 호출 구문에서 인자값을 입력하는 방식이 특이하다는 것을 알 수 있다. 인자값 안에 넣어주는 "레이블"이 그것이다. 레이블을 빼고 함수를 호출한다면 에러가 발생한다.
🚀 일반적으로 스위프트에서 인자 레이블은 매개변수명과 동일하다. 다시 말해 함수를 정의할 때 사용했던 매개변수명을 함수 호출 시에도 붙여주어야 한다는 뜻이다.
🚀 함수를 호출할 때 이처럼 인자 레이블을 붙여주는 것은 인자값의 목적을 확실하게 드러내줄 뿐 아니라 비슷하지만 서로 다른 의미로 사용되는 함수를 구분하기 위함이다.
함수의 반환값과 튜플
함수는 반드시 하나의 값을 반환해야 한다. 여러 개의 값을 반환해야 한다면, 이 값들을 집단 자료형에 담아 반환해야 한다. 이때 사용할 수 있는 집단 자료형에는 딕셔너리나 배열, 튜플, 또는 구조체와 클래스가 있다. 이 중에서 활용도가 높고 다른 자료형과는 조금 차이가 있는 튜플을 사용하는 예시를 살펴본다.
🚀 튜플을 사용하여 값을 반환할 때에는 함수의 반환 타입을 튜플 형태로 정의해야 한다. 튜플에는 여러 종류의 자료형이 다양하게 섞여 정의될 수 있으므로 이를 빠짐없이 표시해야 한다. 아래 예는 (Int, String) 조합으로 구성된 튜플을 반환하는 함수에 대한 선언이다.
func getIndvInfo() -> (Int, String) {
let height = 160
let name = "수빈"
return (height, name)
}
var uInfo = getIndvInfo()
uInfo.0 // 160
uInfo.1 // 수빈
- 위와 같이 튜플을 반환하는 함수의 반환값을 대입받은 변수나 상수는 튜플의 인덱스를 이용하여 튜플 내부 요소를 사용할 수 있다.
- 인덱스 대신 튜플 요소 각각을 변수로 직접 받을 수도 있다.
var (a, b) = getIndvInfo()
a // 160
b // 수빈
- 일부 필요하지 않은 튜플 항목은 언더바를 이용하여 변수 할당 없이 건너뛸 수도 있다.
var (height, _) = getIndvInfo()
- 특정 튜플 타입이 여러 곳에서 사용될 경우는 타입 알리어스를 통해 새로운 축약형 타입을 정의하는 것이 좋다. 타입 알리어스는 이름이 길거나 사용하기 복잡한 타입 표현을 새로운 타입명으로 정의해주는 문법으로,
typealias
키워드를 사용하여 정의한다.
typealias <새로운 타입 이름> = <타입 표현>
typealias infoResult = (Int, Character, String)
func getUserInfo() -> infoResult {
let gender: Character = "F"
let height = 160
let name = "수빈"
return (height, gender, name)
}
내부 매개변수명 , 외부 매개변수명
🚀 내부 매개변수 : 입력된 인자값을 함수 내부에서 참조하기 위해 사용하는 변수
🚀 외부 매개변수 : 함수를 호출할 때 인자값에 대한 레이블 역할을 하며, 동시에 함수의 식별자 일부로 사용되기도 한다.
func 함수이름( <외부 매개변수 명> <내부 매개변수 명> : <타입> .. ) {
// 함수 내용
}
func func printHello(to name: String, welcomeMessage msg: String) {
print("\(name)님, \(msg)")
}
printHello(to: "수빈", welcomeMessage: "안녕")
// 외부 매개변수를 사용하고 싶지 않을 떄
func printHello(_ name: String, _ msg: String) {
print("\(name)님, \(msg)")
}
printHello("수빈", "안녕")
// 한 쪽만 생략하는 것도 가능
func printHello(to name: String, _ msg: String) {
print("\(name)님, \(msg)")
}
printHello(to: "수빈", "안녕")
가변 인자
일반적으로 함수는 미리 정의된 형식과 개수에 맞는 인자값만 처리하지만, 때에 따라서는 가변적인 개수의 인자값을 입력받아야 할 때도 있다. 스위프트 역시 인자값의 입력 개수를 제한하지 않도록 하는 함수 정의 형식을 제공하는데, 아래와 같이 정의하면 된다.
func 함수 이름 (매개변수명 : 매개변수 타입 ...)
- 위와 같이 정의된 매개변수는 가변 인자로 인식되어 개수를 제한하지 않고 인자값을 입력받으며 입력된 인자값을 배열로 처리한다.
- 함수의 실행 블록 내에서 for ~ in 구문을 사용하면 입력된 모든 인자값을 순서대로 읽어들일 수 있다.
func avg(score: Int...) -> Double {
var total = 0 // 점수 합계
for r in score { // 배열로 입력된 값들을 순회 탐색하면서 점수 합산
total += r
}
reurn (Double(total) / Double(score.count)) // 평균값을 구해서 반환
}
print(avg(score: 10, 20, 30, 40, 50))
- 이처럼 가변 인자값은 입력 개수를 특정할 수 없는 형태의 매개변수에서 사용된다.
기본값을 갖는 매개변수
func 함수 이름(매개변수: 매개변수 타입 = 기본값) {
실행할 내용
}
func echo(message: String, newline: Bool = true) {
if newline == true {
print(message, true)
} else {
print(message, false)
}
}
echo(message: "안녕하세요")
echo(message: "안녕하세요", newline: true)
// 위의 두 구문은 실행 결과가 동일하다.
echo(message: "안녕하세요")
echo(message: "안녕하세요", newline: false)
// 위의 두 구문은 실행 결과가 동일하지 않다. 윗줄은 newline = true이기 때문이다.
매개변수의 수정
func incrementBy(base: Int) -> Int {
base += 1
return base
}
- 위의 함수를 보면 매개변수로 입력받은 값에 +1을 하여 리턴해주는 함수라고 생각할 수 있다.
- 그러나 위의 함수는 에러가 발생한다. 왜냐하면 함수에 입력된 인자값은 함수 내부에서 항상 상수로 정의되기 때문이다. 인자값을 변경할 수는 없다.
- 그러나! 아래의 방법으로 내부의 인자값을 수정할 수 있다.
func incrementBy(base: Int) -> Int {
var base = base
base += 1
return base
}
- 매개변수와 동일한 변수 base를 선언하고 여기에 매개변수를 대입하는 구문이다. 이렇게 동일한 이름의 변수를 작성해서 값을 대입하고 나면, 이후로 base 라는 이름의 호출은 모두 매개변수 base가 아니라 변수 base를 가리키게 된다.
InOut 매개변수
함수 내부에서 발생하는 사건은 함수 외부에 영향을 미칠 수 없다. 함수 내부와 외부에 동일한 인자값이 존재하지만 함수 내부에서 변경된 인자값은 함수 외부의 인자값에는 전혀 영향을 끼칠 수 없다. 단순히 같은 값을 가질 뿐, 둘은 단절된 서로 다른 객체이기 때문이다.
var cnt = 30
func autoIncrement(value: Int) -> Int {
var value = value
value += 1
return value
}
print(autoIncrement(value: cnt)) // 함수 내부의 value 변수값 : 31
print(cnt) // 외부에서 정의된 cnt 변수값 : 30
- 그러나 함수에서도 내부에서 수정된 인자값이 외부까지 영향을 미칠 수 있는 방법이 존재한다. 물론 반환값을 이용하지 않고 말이다. 이를 위해 사용되는 키워드가
inout
이다.
func foo(paramCount: inout Int) -> Int {
paramCount += 1
return paramCount
}
- inout 키워드의 명확한 의미는 값 자체를 전달하는 것이 아니라 값이 저장된 메모리 주소를 전달한다는 의미이다. 인자값에 할당된 데이터가 저장되어 있는 메모리 주소를 함수에 전달하는 것이다.
- C언어의 포인터와 비슷한 개념이다. 따라서 inout 키워드가 사용된 함수는 호출할 때 주의가 필요하다. 인자값을 전달할 때 값이 아니라 주소를 전달해주어야 하기 때문이다.
var count = 30
print(foo(paramCount: &count) // 함수 내부의 paramCount 변수값 : 31
print(count) // 함수 외부에 정의된 count 변수값 : 31
- 이처럼 주소를 전달하는 것을 프로그래밍 용어로 '참조에 의한 전달' 이라고 하며, 기존처럼 값을 복사하여 전달하는 것을 '값에 의한 전달' 이라고 한다.
변수의 생존 범위와 생명 주기
🚀 변수의 생존 범위 : 스코프 (Scope)
🚀 변수의 생명 주기 : 변수가 생겨났다가 실행 블록이 끝나면 제거되는 것
🚀 전역 변수는 어떤 블록에서든 접근이 가능하지만, 지역 변수는 코드 블록 내에서만 접근이 가능하다.
🚀 컴파일러가 변수가 정의된 위치를 검색하는 순서
- 함수 내부에서 정의된 변수 찾기
- 함수 외부에 정의된 변수 찾기
- 글로벌 범위에서 정의된 변수(전역 변수)를 찾음
- import된 라이브러리 범위