kotlin은 아래와 같은 형식으로 함수를 작성한다.
fun 함수이름([변수 이름: 자료형, 변수 이름: 자료형..]): [반환값의 자료형]{
표현식...
[return 반환값]
}
여기서 인자와 매개변수에 대한 개념을 다시 확인하고 가자.
매개변수와 인자는 같은 역할을 하는 것처럼 보이기 때문에 같은 것으로 착각하기 쉽다.
하지만 이 둘은 명확하게 구분할 수 있는 개념이다.
함수를 선언할 때는 매개변수라고 부르고 함수를 호출할 때는 인자라고 부른다.
즉, sum() 함수의 선언 부분의 a: Int, b: Int는 매개변수이고 main() 함수에서 sum() 함수를 호출할 때 sum(3, 2)에서 3, 2는 인자입니다. 이 인자는 함수 선언 부분에 있는 a와 b에 복사되어 전달됩니다.
함수의 스택 프레임
함수의 각 정보는 프레임(frame)이라는 정보로 스택(stack)메모리의 높은 주소부터 거꾸로 채워져간다.
함수의 지역 변수는 함수가 종료되면 스택 프레임과 함께 사라지는 임시 변수이다.
함수 또한 마찬가지이다.
스택의 내용이 최대 영역을 초과하면 스택 오버플로가 발생한다.
프레임은 스택에 생성되고 스택은 메모리의 높은 주소에서 낮은 주소 방향으로 생성이되는데, 힙 영역은 동적으로 생성된 객체의 정보가 담겨져 있다. 힙ㅇ베서는 보통 낮은 주소에서 높은 주소로 자라가며 정보를 저장하는데, 그래서 두 영역이 만날 수가 있어서 메모리 관리가 중요하다. 관리를 잘못하거나 무한하게 함수를 호출하면 스택 오버플로가 발생한다.
반환값이 없는 함수인 경우에는 아래와 같이 void가 아닌 Unit(자바의 void형)을 사용한다.
fun printSum(a: Int, b: Int): Unit{
println("sum of $a and $b is ${a+b}")
}
매개변수를 활용한 코드이다.
코틀린에서 신기했던 점은 함수의 매개변수를 선언했지만, 인자에 따라서 값이 달라진다는 점이 너무 신기했다.
아래 코드에서 add함수에서 email값이 고정이 되고, defaultArgs에서도 값이 고정이 될지 알았는데, 아니었다. 매개변수가 아닌 인자의 값에 따라 달라진다는게 핵심이다.
package chap03.section1
fun main(){
val name = "홍길동"
val email = "hong@example.kr"
add(name)
add(name, email)
add("둘리", "dooly@example.kr")
defaultArgs()
defaultArgs()
}
fun add(name: String, email: String = "default"){
val output = "${name}님의 이메일은 ${email}입니다."
println(output)
}
fun defaultArgs(x: Int = 100, y: Int = 200){
println(x + y)
}
인자의 갯수를 다양하게 전달받는 경우가 있다.
그럴때는 vararg 를 사용하자.
package chap03.section1
fun main(){
normalVarargs(1, 2, 3, 4)
normalVarargs(4, 5, 6)
}
fun normalVarargs(vararg counts: Int){
for (num in counts){
print("$num ")
}
print("\n")
}
본 책을 통해서 처음 들어보는 여러 함수와 식을 알게 되었다.
순수함수
람다식
일급 객체
고차 함수
순수 함수
부작용이 없는 함수라고 말하는데, 이런 함수를 스레드에 사용해야한다.
순수 함수의 조건은 크게 두가지가 있다.
1. 같은 인자에 대하여 항상 같은 값을 반환한다.
2. 함수 외부의 어떤 상태도 바꾸지 않는다.
순수 함수를 처음 들어본 나는 기준이 애매했는데 아래와 같은 예시를 설명해주었다.
fun check() {
val test = User.grade() // check() 함수에 없는 외부의 User 객체를 사용
if (test != null) process(test) // 변수 test는 User.grade()의 실행 결과에 따라 달라짐
}
check() 함수는 함수 안에서 함수 안에서 함수 외부에 있는 User 객체의 함수인 grade()함수를 실행하고 있다. 또 grade() 함수의 결괏값을 test에 저장하여 조건문 if(test!=null)에 사용한다. 심지어 process()함수는 조건을 만족하지 못하면 실행되지 않는다. check() 함수만 보면 User가 어떤 객체인지, grade() 함수는 어떤 값을 반환하는지, process() 함수는 대체 무엇을 하는지 알 수 없다. 쉽게 말해 check() 함수의 실행 결과를 예측하기 어렵다는 것이다. 바로 이런 함수가 순수 함수의 조건을 만족하지 못하는 함수이다.
람다식
{x, y -> x + y}를 람다식이라 한다.
화살표를 사용하고, 이름이 없는 함수로 2개 이상의 입력을 1개의 출력으로 단순화 하는 개념이다.
람다식 호출 방법에는 값에 의한 호출(Call by value), 이름에 의한 람다식 호출(Call by name)등이 있지만, 그중에서 신기했던 것이 바로
인자와 반환값이 없는 람다식 함수다.
package chap03.section3
fun main() {
val out: () -> Unit = {println("Hello World!")} // 인자와 반환값이 없는 람다식의 선언
// 자료형 추론이 가능하므로 val out = { println("Hello World!") } 와 같이 생략 가능
out() // 함수처럼 사용 가능
val new = out // 람다식이 들어 있는 변수를 다른 변수에 할당
new()
}
그리고 참조에 의한 함수 호출도 있다.
처음보는 2개의 콜론기호(::)에 당황했지만 이것은 sum()과 funcParam()의 매개변수 c의 선언부 구조를 보면 인자 수와 자료형의 개수가 동알한데, 이럴때 사용하는것이다.
package chap03.section3
fun main() {
// 인자와 반환값이 있는 함수
val res1 = funcParam(3, 2, ::sum)
println(res1)
// 인자가 없는 함수
hello(::text) // 반환값이 없음
// 일반 변수에 값처럼 할당
val likeLambda = ::sum
println(likeLambda(6, 6))
}
fun sum(a: Int, b: Int) = a + b
fun text(a: String, b: String) = "Hi! $a $b"
fun funcParam(a: Int, b: Int, c: (Int, Int) -> Int): Int{
return c(a, b)
}
fun hello(body:(String, String)->String): Unit{
println(body("Hello", "World"))
}
이외에도 0개, 1개, 2개의 매개변수를 가지는 람다식이 있지만, 이것을 자세히 알아야할까 라는 의문이 든다.
일급 객체
일급 객체는 아래와 같은 특징을 가진다.
1. 함수의 인자로 전달
2. 함수의 반환값에 사용할 수 있다.
3. 변수에 담을 수 있다.
고차 함수
다른 함수를 인자로 사용하거나 함수를 결괏값으로 반환하는 함수를 말한다.
fun main() {
println(highFunc({x, y -> x + y}, 10, 20)) // 람다식 함수를 인자로 넘김
}
fun highFunc(sum: (Int, Int) -> Int, a: Int, b: Int): Int = sum(a, b) // sum 매개변수는 함수
고차 함수와 람다식을 복잡하게 알아봤는데, 이것을 어디에 쓰기에 내용이 길었을까?
동기화를 위한 코드 구현에서도 볼 수 있다.
Lock lock = new ReentrantLock();
lock.lock();
try{
// 보호할 임계 영역의 코드
// 수행할 작업
} finally{
lock.unlock(); // 해제
}
이 코드에서는 Lock을 활용해 임계 영역을 보호하고 있다.
특정 공유 자원에 접근한다고 했을 때 공유 자원이 여러 요소에 접근해서 망가지는 것을 막기 위해 임계영역의 코드를 잠가 두었다가 사용한 후 풀어줘야한다. 위 코드에서 특정함수를 보호하기 위한 고차함수를 만들고 활용해야한다.
fun <T> lock(reLock: ReentrantLock, body: ()->T): T{
reLock.lock()
try{
return body()
} finally{
reLock.unlock()
}
}
잠금을 위한 lock() 함수를 fun <T> lock() 형태인 제네릭 함수로 설계하고 있다.
T는 제네릭의 형식 매개변수라고 하며 임의의 참조 자료형을 의미한다. 형식 매개변수는 다양한 자료형을 처리하는데 클래스뿐만 아니라 메서드 매개변수나 반환값으로도 사용할 수 있다.
package chap03.section4
import java.util.concurrent.locks.ReentrantLock
var sharable = 1
fun main() {
val reLock = ReentrantLock()
// 아래 lock함수 표현식은 모두 동일
lock(reLock, { criticalFunc() })
lock(reLock) {criticalFunc()}
lock(reLock, ::criticalFunc)
println(sharable)
}
fun criticalFunc(){
// 공유 자원 접근 코드 사용
sharable += 1
}
fun <T> lock(reLock: ReentrantLock, body: () -> T):T{
reLock.lock()
try{
return body()
}
finally{
reLock.unlock()
}
}
콜백함수의 개념도 알 수 있었다.
콜백(Callback)함수란 특정 이벤트가 발생하기까지 처리되지 않다가 이벤트가 발생하면 즉시 호출되어 처리되는 함수를 말한다. 즉 사용자가 아닌 시스템이나 이벤트에 따라 호출 시점을 결정한다.
코틀린에서는 앞에서 나온 함수 뿐만 아니라 다양한 형태의 함수가 있다.
1. 익명함수
일반 함수이지만 이름이 없는 것이다. 람다 함수도 이름 없이 구성할 수 있지만 일반 함수의 이름을 생략하고 사용하는 것이다.
val add: (Int, Int) -> Int = fun(x, y) = x + y // 익명 함수를 사용한 add 선언
val result = add(10, 2) // add의 사용
위와 같은 코드는 간단해보이기에 직관적일 수 있지만, 코드가 길다면 가독성이 좋을까? 라는 의문이든다.
2. 인라인 함수
전 회사에서는 #define과 Inline함수를 사용해서 속도측면에서 개설한 경험이 있다.
코틀린에서도 마찬가지로, 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해 넣어 함수의 분기 없이 처리되기 때문에 코드의 성능을 높일 수 있다.
하지만 코드의 길이는 3줄이하로 짤것을 권장했다.
package chap03.section5
fun main() {
// 인라인 함수 shortFunc()의 내용이 복사되어 shortFunc으로 들어감
shortFunc(3) { println("First call: $it")}
shortFunc(5) { println("First call: $it")}
}
inline fun shortFunc(a: Int, out:(Int) -> Unit){
// inline fun shortFunc(a: Int, noinline out:(Int) -> Unit){ 역컴파일 X
println("Before calling out()")
out(a)
println("After calling out()")
}
3. 확장 함수
클래스에 함수를 확장하고 싶은 경우가 있다.
아래 String 클래스에 함수를 추가한 예시를 통해 익히자.
package chap03.section5
fun main(){
val source = "Hello World!"
val target = "Kotlin"
println(source.getLongString(target))
}
fun String.getLongString(target: String): String =
if(this.length > target.length) this else target
4. 중위 함수
fun 앞에 infix를 붙여서 중위함수를 만들 수 있는데, 이것을 사용하는 가장 큰 이유는
클래스의 멤버를 호출할 때 사용하는 점(.)을 생략하고 함수 이름 뒤에 소괄호를 붙이지 않아 직관적인 이름을 사용할 수 있다.
중위 함수의 조건은 아래와 같다.
package chap03.section5
fun main() {
// 일반 표현법
// val multi = 3.multiply(10)
// 중위 표현법
val multi = 3 multiply 10
println("multi: $multi")
}
// Int를 확장해서 multiply() 함수를 하나 더 추가함
infix fun Int.multiply(x: Int): Int{ // infix로 선언되므로 중위 함수
return this * x
}
5. 꼬리 재귀 함수
파이썬에서는 재귀함수의 제한이 있지만, 그것을 바꿀 수 있었다.
코틀린에서는 tailrec라는 키워드를 줘서 2만번 이상 반복되는 재귀함수를 실행할 수 있다.
package chap03.section5
import java.math.BigInteger
fun main(){
val n = 100
val first = BigInteger("0")
val second = BigInteger("1")
println(fibonacci(n, first, second))
}
tailrec fun fibonacci(n: Int, a: BigInteger, b: BigInteger): BigInteger {
return if (n == 0) a else fibonacci(n-1, b, a+b)
}
6주차 - 프로퍼티와 초기화 (3) | 2024.01.09 |
---|---|
5주차 - 클래스와 객체의 정의 (1) | 2024.01.06 |
4주차 - 프로그램의 흐름 제어 (0) | 2024.01.04 |
2일차 - 패키지, 변수와 자료형, 연산자 (1) | 2023.12.26 |
1일차 - 코틀린 시작하기 (1) | 2023.12.22 |
댓글 영역