스위프트는 기본적인 매개변수 호출 방법이 다른 언어와 다르기도 하지만, 이외에도 특별한 기능을 많이 가지고 있다.
내부 매개변수명, 외부 매개변수명
스위프트에서는 함수를 정의할 때 매개변수를 용도에 따라 두 가지로 분리할 수 있다. 내부 매개변수와 외부 매개변수이다. 외부 매개변수는 함수를 호출할 때 인자값에 대한 레이블 역할을 하고, 동시에 식별자 일부로 사용되기도 한다. 내부 매개변수는 입력된 인자값를 함수 내부에서 참조하기 위해 사용하는 변수이다.
별도로 외부 매개변수는 나누지 않을 경우 일반 매개변수가 인자 레이블 역할까지 겸하지만, 외부 매개변수를 명시적으로 정의하면 이때부터는 외부 매개변수가 인자 레이블이 된다. 외부 매개변수를 지정하는 방법은 아래와 같다.
func 함수명(<외부 매개변수명><내부 매개변수명>: <타입>, <외부 매개변수명><내부 매개변수명> : <타입>..) {
// 함수 내용
}
함수를 정의할 때 내부 매개변수명 앞에 외부 매개변수명을 넣어주기만 하면 된다. 아래는 사용 예시이다.
// 내부 매개변수명, 외부 매개변수명
func printHello(to name: String, welcomeMessage msg: String) {
print("\(name)님, \(msg)")
}
printHello(to: "홍길동", welcomeMessage: "안녕하세요")
// 결과값
// 홍길동님, 안녕하세요
이전과 호출방식이 달라졌다. 매개변수명 name 대신 to를, msg 대신 welcomeMessag라는 외부 매개변수명을 넣어주고있다. 함수 내부에서는 내부 매개변수명인 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 i in score { // 배열로 입력된 값들을 순회 탐색하면서 점수를 합산
total += i
}
return (Double(total) / Double(score.count)) // 평균을 구해서 반환
}
print(avg(score: 10, 20, 30, 40)) // 결과 : 25.0
위의 평균을 구하는 예시처럼 가변 인자값은 입력 개수를 특정할 수 없는 형태의 매개변수에서 사용된다.
기본값을 갖는 매개변수
함수의 매개변수에는 유용한 기능이 있는데 바로 기본값을 지정할 수 있다는 것이다. 반드시 직접 입력받아야 하는 값이 아니라면 인자값을 생략할 수 있도록, 함수 정의 시 매개변수에 기본값을 지정할 수 있는 문법을 제공한다.
func 함수명(매개변수: 매개변수 타입 = 기본값) {
실행할 내용
}
매개변수 타입 뒤에 기본값을 대입하면 된다. 아래는 그 예시이다.
// 기본값이 지정된 함수
func echo(message: String, newline: Bool = true) {
if newline {
print(message, true)
}else {
print(message, false)
}
}
echo(message: "hello")
echo(message: "hello", newline: true)
위 두 구문은 모두 if 조건문이 true가 되어 같은 결과를 출력한다. newline 파라미터를 넣지 않아도 자동으로 true가 대입되어 있기 때문이다. 만약 newline에 false를 대입하고 싶다면 반드시 파라미터를 사용하여 false를 넣어야 한다.
매개변수의 기본값을 제공하도록 함수를 정의하면 함수는 두 가지 형식으로 모두 생성된다고 생각하는게 좋다. 아래와 같이 두 가지 형식으로 제공된다.
echo(message: String)
echo(message: String, newline: Bool)
매개 변수의 수정
지금까지 매개변수라는 단어를 사용했지만 사실 문제가 하나 있다. 아래 예제를 보면 나온다.
// 입력받은 값을 +1 하여 리턴해주는 함수
func incrementBy(base: Int) -> Int {
base += 1
return base
}
이 함수를 작성하면 아래와 같은 에러가 발생한다.
Left side of mutating operator isn't mutable: 'base' is a 'let' constant
오류 메세지를 확인해보면 base는 상수로 정의되었으므로 값을 변경할 수 없다는 뜻이다. 따라서 매개변수라는 이름보다는 매개상수라는 표현이 맞다. 하지만 일반적으로 매개상수라는 표현은 사용되지 않고 영어로는 그냥 Parameter이다. 즉, 영어적 표현으로는 상수나 변수의 개념이 없다. (현업에서도 실제로 파라미터라고 많이한다...)
그렇다면 파라미터의 값을 변경하여 활용하여야 할 경우에는 어떤 식으로 사용하냐면 파라미터를 변수화 시켜서 사용하면 된다. 아래는 예제 코드이다.
func incrementBy(base: Int) -> Int {
var base = base
base += 1
return 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
외부에서 입력한 인자값의 참조값이 직접 함수 내부로 전달되는 것이 아니라 그 값이 복사된 다음 전달되기 때문이다. 즉 인자값으로 전달된 cnt와 value는 서로 다른 변수이다.
하지만 함수에서도 내부에서 수정된 인자값이 외부까지 영향을 미칠 수 있는 방법이 존재한다. 바로 inout 키워드이다.
func foo(paramCount: inout Int) -> Int{
paramCount += 1
return paramCount
}
var count = 30
print(foo(paramCount: &count)) // 함수 내부의 paramCount 변수값 : 31
print(count) // 외부에서 정의된 count 변수값 : 31
함수를 호출하는 부분에서 count가 인자값으로 사용된 부분에 주소 추출 연산자인 &연산자가 붙어있다. 이는 값이 아닌 주소값을 전달하며 이 주소를 읽어들이기 위해 함수에서는 매개변수에 inout 키워드가 추가된다고 보면 된다. 이처럼 주소를 전달하는 것을프로그래밍 용어로 "참조에 의한 전달" 이라고 하고, 기존처럼 값을 복사하여 전달하는 것을 "값에 의한 전달"이라고 한다.
이처럼 주소를 직접 전달하는 참조에 의한 전달은 함수에서 inout 키워드를 사용했을 때만 적용되지만, 예외적으로 클래스(Class)로 구현된 인스턴스는 inout 키워드를 사용하지 않아도 항상 참조에 의해 전달된다. 따라서 함수 내부에서 값이 수정되면 원본 객체에도 영향을 미치므로 주의해야 한다.
inout 키워드가 붙은 매개변수는 함수 내부에서 직접 값을 수정할 수 있으므로 상수는 전달 대상이 될 수 없다. 같은 이유로 리터럴 역시 불가능하며 오직 변수만 가능하다.
// 상수는 inout 매개변수에 인자값으로 전달할 수 없음
let count1 = 30
foo(paramCount: &count1) // x
// 리터럴은 inout 매개변수에 인자값으로 전달할 수 없음
foo(paramCount: &30) // x
// 변수는 inout 매개변수에 인자값으로 전달할 수 있음
var value = 30
foo(paramCount: &value) // o
변수의 생존 범위와 생명 주기
변수는 정의된 위치에 따라 사용할 수 있고 생존할 수 있는 일정 영역을 부여받는다. 이를 변수의 생존 범위 또는 스코프라고 한다. 영역을 기준으로 보면 전역 변수와 지역 변수로 나눌 수 있다.
전역 변수는 글로벌(Global) 변수라고도 하는데, 프로그램의 최상위 레벨에서 작성된 변수를 의미한다. 일반적으로 프로그램 내 모든 위치에서 참조할 수 있으며, 특별한 경우를 제외하면 프로그램이 종료되기 전까지 삭제되지 않는다.
반면 로컬(Local) 변수라고도 하는 지역 변수는 특정 범위 내에서만 참조하거나 사용할 수 있는 변수를 의미한다. 지역 변수는 선언된 블록이 실행되면서 생겨났다가 실행 블록이 끝나면 제거된다. 이를 변수의 생명주기라고 한다. 다음 예제를 보면 알 수 있다.
do { // 상위 블록
do { // 하위 블록
var ccnt = 3
ccnt += 1
print(ccnt) // 4
}
ccnt += 1
print(ccnt) // 오류 - "Use of unresolved identifier 'ccnt'"
}
do 블록은 일반적으로 do ~ catch 구문으로 사용되지만, 단독으로 사용되면 단지 실행 블록을 구분하는 역할을 한다. do 블록은 중첩해서 사용할 수 있는데, 이 때 내부에 중첩된 do 블록을 기준으로 실행 블록은 단계화 된다. 내부에 많이 중첩되어 있을수록 하위 블록이다.
위 코드의 아래 부분에서 오류가 난 이유는 ccnt 라는 변수는 하위 블록에서 선언되었고 해당 코드가 종료된 이후 메모리에서 해제되고 존재하지 않는 변수가 되기 때문이다. 만약 해당 위치에서 ccnt를 출력하기 위해서는 수정이 필요하다. 아래는 수정을 한 코드이다.
do { // 상위 블록
var ccnt = 0 // <- 옮긴 위치
do { // 하위 블록
ccnt = 3
print(ccnt) // 3
}
ccnt += 1
print(ccnt) // 4
}
ccnt는 더 넓은 범위의 블록에서 정의되었으며, 하위 블록에서는 상위 블록의 변수를 얼마든지 가져다 사용할 수 있기 때문에 오류가 발생하지 않는다.
이 때 주목할 점은 0으로 초기화를 했다는 점이다. 변수가 생성된 블록이 아닌 다른 블록에서 사용하려면 반드시 초기화가 되어있어야 한다. 하나의 블록에서 다른 블록으로 참조에 의한 전달 과정이 일어날 때 변수의 주소값이 필요하다. 만약 초기화되지 않았다면 메모리에 할당받지 못한 상태이므로 주소값도 존재하지 않아 오류가 발생한다.
전역변수는 최상위 블록에서 선언된 변수이므로 그보다 하위 블록인 함수 내부에서도 얼마든지 접근할 수 있다. 아래 코드는 그 예시이다.
var count = 30
func foo() -> Int {
count += 1
return count
}
foo() // 31
이 함수는 내부에서 count 변수를 1만큼 증가시키는 처리를 한다. count는 상위 블록인 전역 범위에서 선언된 변수이다. 함수 내에서는 전역 변수의 값에 접근할 수도 있고, 수정할 수도 있다.
이번에는 전역 변수와 지역 변수가 겹치는 예제 코드이다.
var count = 30
func foo(count: Int) -> Int {
var count = count
count += 1
return count
}
print(foo(count: count)) // 함수 내부의 count 변수값 : 31
print(count) // 외부에서 정의된 count 변수값 : 30
함수 외부에서 count 변수는 전역 범위로 선언되어 있고 함수 내부에서도 count가 매개변수로, 그리고 지역 변수로 선언되어 있다.
스위프트에서 함수의 외부와 내부에 각각 같은 이름의 변수가 존재하면 내부에서 선언된 변수는 외부와 상관없이 새롭게 생성된다는 것을 알 수 있다. 그렇지 않다면 외부 변수의 값도 함께 변경되었을 것이다.
위의 예에서 내부 영역에 정의된 count 변수는 지역 변수(Local Variable)로써, 외부에서 정의된 count 변수와는 엄연히 다른 객체이다. 또한, 외부와 내부에서 같은 이름의 변수가 선언되면 변수 사용의 우선순위에 따라 외부 변수가 아닌 내부 변수를 사용하게 된다.
함수처럼 블록 내부에서 변수나 상수가 사용될 경우 컴파일러는 이 변수가 정의된 위치를 다음의 순서에 따라 검색한다.
- 함수 내부에서 정의된 변수를 찾음
- 함수 외부에서 정의된 변수를 찾음
- 글로버 범위에서 정의된 변수를 찾음
- import 된 라이브러리 범위에서 정의된 변수를 찾음