들어가기 전에
본 포스팅은 개발자를 위한 코틀린 프로그래밍 의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다.
이전 포스팅 ⬇️
01 함수 알아보기
1.2 함수 몸체부 처리
단일 표현식으로 대체
함수의 로직은 블록 내부에 작성한다. 간단한 표현식만 있는 경우 블록을 생략하고 한 줄로 작성하는 단일표현식으로 작성할 수도 있다.
보통 단일 표현식으로 함수 코드 블록을 구성하면, 단일 표현식을 추론해 반환 자료형을 추론할 수 있으므로, 반환 자료형을 생략한다.
// 코드 블록 몸체부에 한 줄로 처리
fun add(x: Int, y: Int): Int {
return x + y
}
// 코드 블록 대신 = 와 표현식으로 표시, return 사용x
fun add1(x: Int, y: Int): Int = x + y;
// 반환 타입 생략
fun add2(x: Int, y: Int) = x + y;
복잡한 단일 표현식
조건문, object표현식 등은 하나의 표현식으로 작성 할 수 있다.
특히 object 표현식은 복잡하게 작성되지만 하나의 결과인 객체를 만들므로 단일 표현식으로 작성할 수 있다.
// if 표현식의 사용
fun singleExpression(x: Int, y: Int) = if (x > y) x else y
open class Person {
fun print() = println("Person 클래스의 print() 메서드")
}
// Person 클래스를 상속받은 익명 클래스
fun anonymousObject() = object : Person() {
}
val v = anonymousObject()
v.print() // Person 클래스의 print() 메서드
1.3 함수의 매개변수와 인자
함수는 동일 이름으로 여러개 함수를 정의할 수 있다.
(함수를 식별하는 기준이, 이름뿐 아니라 매개변수와 호출인자까지 포함되기 때문)
- 위치인자와 이름인자, 매개변수에 초기값 지정
fun add(x: Int = 0, y: Int, z: Int): Int = x + y + z
// 위치인자
add(1, 2, 3)
// 이름인자
add(x = 1, y = 2, z = 3)
// 혼합인자 : 이름인자와 위치인자를 혼합해서 사용할 수 있다.
add(1, z = 3, y = 2)
// 기본값이 전달되지 않으면, 초기값을 인자로 사용해 함수의 결과 반환
add(y = 2, z = 3)
- 가변인자 지정
배열로 정의된 것을 가변인자로 처리하려면 별표(*)를 붙여 모든 원소가 전달되어야 가변인자가 처리된다.
이 연산자를 스프레드 연산자(spread operator)라고 한다.
fun sum(vararg ints: Int) {
var sum = 0
for (i in ints) {
sum += i
}
println(sum)
}
fun main(args: Array<String>) {
val arr = intArrayOf(1, 2, 3, 4, 5)
sum(*arr) // 스프레드 처리할 때는 array 등 기본 배열 사용
sum(1, 2, 3, 4, 5)
sum(1, 2, 3)
sum(1, 2)
sum(1)
sum()
/*
15
6
3
1
0
*/
}
- 함수 인자를 전달 할 때 주의사항
코틀린은 가변 객체와 불변 객체가 있다. 가변 객체를 함수의 인자로 전달하면 가변 객체 내부의 값을 변경할 수 있다.
리스트의 경우 가변과 불변을 지정할 수 있다. 함수를 정의할 때 매개변수에 가변 매개변수를 지정하면, 가변 리스트를 인자로 전달할 경우 함수 밖에 지정한 리스트도 값이 변경된다.
함수 내부에서 외부 값을 변경하지 않으려면, 가변 리스트를 복사해서 함수의 인자로 전달해야 한다.
fun main(args: Array<String>) {
val intList = listOf(1, 2, 3, 4, 5)
fun addListElement(list: List<Int>): List<Int> {
return list + listOf(6, 7) // 리스트 원소 추가 및 반환
}
val intList2 = addListElement(intList)
println(intList2 == intList) // false
val mutableList = mutableListOf(1, 2, 3, 4, 5)
fun addMutableListElement(list: MutableList<Int>): MutableList<Int> {
list.add(6) // 리스트 원소 추가
list.add(7)
return list
}
val mutableList2 = addMutableListElement(mutableList)
println(mutableList2 == mutableList) // true
}
만일, 변경 가능한 인자를 복사해서 전달하고 싶다면, 복사해서 새로운 객체를 만들어 반환하는 것이 좋을 것이다.
02 익명함수와 람다표현식 알아보기
코틀린에서는 함수 정의 없이 바로 실행하는 함수를 만드는 법을 두 가지 제공한다.
2.1 익명함수
익명함수는 함수 정의와 동일하지만 함수 이름을 가지지 않으며, 일회성으로 처리하는 용도로 사용
println(fun(): String { return "익명함수를 즉시 시행" }())
val addFun = fun(): String = "익명함수를 변수에 할당"
println(addFun())
- 익명함수 내부에 지역 익명함수 정의
익명함수도 함수 코드 블록 내부에 지역변수, 매개변수, 반환 자료형을 지정해서 처리할 수 있다.
2.2 람다 표현식
람다는, 수학에서 상수를 의미한다.
코틀린에서 람다 표현식은 상수처럼 사용하는 함수를 의미한다. 익명함수도 있지만, 람다표현식을 사용하는 방식이 더 간결하게 함수를 정의하고 상수처럼 인자나 반환값 등으로 전달하기 편리하다.
- 예약어와 함수 이름이 없음
- {} 안에 직접 매개변수와 표현식을 작성
- 매개변수와 표현식을 기호 -> 로 구분.
- 람다 표현식에서 매개변수는 괄호처리를 하지 않는다.
- 매개변수가 없는 경우 매개변수를 생략하고 표현식만 작성한다.
- 바로 실행하려면 람다표현식 다음에 호출연산자()를 사용하면 된다.
- 기본적으로 return문을 사용할 수 없지만, 인라인 함수로 사용할 때는 return 문을 사용할 수 있다.
- 그 이유는 실제 람다표현식 내부 로직이 호출된 곳에 코드로 삽입되기 때문이다.
- 중간에 함수를 빠져나갈 경우에는 레이블된 return문을 사용 할 수 있다.
- 이때는 반환값이 아니라, 종료하는 조건으로만 사용한다.
- 매개변수가 하나만 있을 경우 별도의 매개변수를 지정하지 않고 하나의 매개변수라는 의미로 it를 제공한다.
{ println("아무 인자가 없다.") }()
println({ x: Int -> x + x }(10)) // 20 ; 인자가 하나 있는 경우
println({ x: Int, y: Int -> x * y }(10, 20)) // 200 ; 인자가 두 개 있는 경우
val f1 = { x: Int -> x + x } // 함수를 변수에 할당
println(f1(10)) // 20 ; 변수에 할당된 함수를 사용
val f2: (x:Int, y:Int, f:(Int, Int)->Int) -> Int = { x, y, f -> f(x, y) }
fun f3(x: Int, y: Int, f: (Int, Int) -> Int): Int = f(x, y)
// 함수를 인자로 받는 함수
2.3 클로저 이해하기
외부함수와 내부함수
- 외부함수(outer function): 지역함수를 가진 함수
- 내부함수(innner function): 외부함수 내에 정의된 지역함수
클로저(closure)
- 외부 함수 내에 내부함수를 정의, 단순히 내부함수를 실행하지 않고 반환
- 이때 내부함수는 외부 함수의 지역 변수를 사용할 수 있다.
반환된 내부함수가 실행될 동안 외부함수의 지역변수를 계속 사용.- 외부함수의 지역변수 -> 자유변수
- 이러한 환경을 클로저라고 함
fun outer(x: Int) { // 외부함수
fun inner(y: Int) = x + y //내부함수는 외부함수의 변수를 사용함
println(inner(x)) // 내부함수를 실행하면, 외부함수의 지역변수 인자를 제공한다.
}
fun outer1(x: Int): Int {
fun inner(y: Int) = x + y
return inner(x)
}
fun outer2(x: Int): Int {
return fun(y: Int): Int {
return x + y
}(5)// 내부 함수를 익명함수를 사용한 경우
}
fun outer3(x: Int): Int {
return {y:Int -> x + y}(5) // 람다식을 사용한 경우
}
fun main() {
outer(10) // 20
println(outer1(10)) // 20
println(outer2(10)) // 15
println(outer3(10)) // 15
}
내부 함수를 실행하지 않고 반환처리해서 클로저 환경을 구성할 수 있다.
fun outer(x: Int): (Int) -> Int {// 함수를 참조로 반환
fun inner(y: Int): Int = x * y
return ::inner
}
val inner1 = outer(10) // outer의 반환값을 inner1에 저장
fun outer2(x: Int): (Int) -> Int = { y -> x * y } // 람다식으로 표현
val inner2 = outer2(10) // outer2의 반환값을 inner2에 저장
fun outer3(x:Int): (Int) -> Int = fun(y: Int): Int = x * y // 익명함수로 표현
val inner3 = outer3(10) // outer3의 반환값을 inner3에 저장
fun main(){
println(inner1(20)) // 200
println(inner2(20)) // 200
println(inner3(20)) // 200
}
렉시컬 스코핑(lexical scoping)
외부함수와 내부함수는 각각의 스코프를 구성하는데, 내부함수는 외부함수의 스코프를 참조할 수 있으므로 스코프 계층이 생긴다.
이런 계층에서 변수를 검색하는 방법을 렉시컬 스코핑이라고 한다. (스코프는 함수를 호출할 때가 아니라, 어디에 선언하였는지에 따라 결정)
- 렉시컬 스코프의 처리 기준
- 내부(local) 조회
- 외부함수 조회
- 전역 패키지 변수 global 조회
- built-in 조회
내부함수를 반환해서 처리 할 때 스코프 처리 방식
내부함수가 반환되어 클로저 환경을 구성한 경우는 내부함수에서 사용하는 외부함수의 변수를 함수를 호출할 때마다 계속 사용한다.
fun strLenDeterminerGenerator(length: Int): (String) -> Boolean = { input: String -> input.length == length }
val is4LenString = strLenDeterminerGenerator(4)
fun main(args: Array<String>) {
println(is4LenString("abcd")) // true
println(is4LenString("abc")) // false
}
03 함수 자료형 알아보기
3.1 함수 자료형 정의
함수 자료형 표기법
함수 자료형은 함수 이름과 매개변수를 제외한 함수 시그니처로 작성한다. 그래서 함수 시그니처가 같으면 다양한 함수가 같은 함수 자료형에 포함되는 것을 알 수 있다.
- 매개변수 자료형 표시
- 괄호 안에 매개변수의 개수에 맞춰 자료형을 쉼표로 구분해서 정의한다.
- 반환 자료형 표시
- -> 다음에 자료형을 표시한다. 이때는 괄호를 사용하지 않는다.
- 보통 자료형이 함수일때도 함수 자료형을 지정한다.
- 변수에 함수가 할당될 때
- 익명함수, 람다표현식, 함수 참조에서 정의되는 것을 확인하고 타입 추론이 가능하다.
- 함수 자료형에 여러 개 묶어서 전달될 경우 ->을 기준으로 우측에 표기된 것을 괄호로 묶어서 처리
변수에 함수 자료형 처리
val a: () -> Unit = { println("Hello") }
val b: (Int) -> Int = { x -> x * 2 } // 하나의 매개변수
val c: (Int, Int) -> Int = { x, y -> x + y } // 두개의 매개변수
// 익명함수
val d: () -> Unit = fun() { println("Hello") }
val e: (Int) -> Int = fun(x: Int): Int = x * 2
val f: (Int, Int) -> Int = fun(x: Int, y: Int): Int = x + y
// 함수 정의 참조를 이용해 변수에 할당
fun hello(): Unit = println("Hello")
fun triple (x: Int): Int = x * 3
fun sum(x: Int, y: Int): Int = x + y
val g: () -> Unit = ::hello
val h: (Int) -> Int = ::triple
val i: (Int, Int) -> Int = ::sum
3.2 널이 가능한 함수 자료형 정의
함수 자료형 널러블 표기법
- 널러블 함수 자료형: (함수자료형)?
- 널러블 함수 자료형 함수 호출: 함수명?. invoke(인자)
매개변수에 널러블 함수 자료형
호출 메서드 invoke사용
// 함수 자료형도 Null 이 가능하다.
fun nullFunc(action: (() -> Unit)?): Long {
val start = System.nanoTime()
action?.invoke() // 널이 들어올 수 있으므로
return System.nanoTime() - start
}
fun main(){
println(nullFunc(null)) // 334
println(nullFunc { println("Hello") })
/*
Hello
626458
* */
}
클래스에 연산자 오버로딩
호출연산자도 하나의 연산자이다. 따라서 이 연산자에 해당하는 메서드가 invoke라서 클래스를 정의할 때 연산자를 재정의해서 사용할 수 있다.
class MyClass : () -> Unit { // 함수 자료형 상속
override operator fun invoke() { //실행연산자 오버라이딩
println("Hello World!")
}
}
val fun1: () -> Unit = MyClass()
class A : Function<Unit> { // 함수 자료형 인터페이스 상속
operator fun invoke() { // 실행연산자 오버라이딩
println("실행연산자 실행!!")
}
}
val fun2 = A()
fun main(args: Array<String>) {
fun1() // Hello World!
fun2() // 실행연산자 실행!!
}
object 정의와 표현식으로 호출연산자 처리
호출 연산자에 대한 연산자 오버로딩은 object에서도 사용할 수 있다.
val a = object : (Int, Int) -> Int { // object 표현식: 함수를 상속
override fun invoke(p1: Int, p2: Int): Int = p1 + p2 // 연산자 오버로딩
}
object Func: (Int, Int) -> Int{
override fun invoke(p1: Int, p2: Int): Int = p1 * p2
}
fun main(args: Array<String>) {
println(a(1, 2)) // 3
println(Func(1, 2)) //2
}
오버로딩이 무조건 좋지 않기 때문에, 함수를 정의할 때 초기값을 사용하거나, (여러 개의 매개변수가 같은 자료형일 경우)매개변수를 가변인자로 변경하여 작성하면 줄일 수 있다.
'코틀린 > 개발자를 위한 코틀린 프로그래밍' 카테고리의 다른 글
[chapter07] 클래스 관계 등 추가사항 알아보기 (0) | 2023.08.27 |
---|---|
[chapter06] 내장 자료형 알아보기 (0) | 2023.08.18 |
[chapter05] 클래스 알아보기 (0) | 2023.08.13 |
[chapter03] 문장 제어처리 알아보기 (0) | 2023.07.24 |
[chpater 02] 코틀린에서는 모든 것이 객체이다. (0) | 2023.07.21 |