1. 람다 함수
람다 함수
람다 함수는 코틀린뿐만 아니라 많은 프로그래밍 언어에서 제공하는 익명 함수 정의 기법입니다. 람다 함수는 주로 함수를 간단하게 정의할 때 이용하며 람다식이라고도 합니다. 코틀린으로 프로그램을 작성하다 보면 람다 함수를 많이 쓰는데, 이는 코틀린에서 고차함수를 지원하기 때문입니다.
고차함수는 매개변수나 반환값으로 함수를 이용하는데, 람다 함수는 주고받을 함수를 간단하게 정의할 때 사용합니다. 즉, 람다 함수는 그 자체도 가치가 있지만 고차 함수를 이해하고 사용하려면 꼭 알아야 합니다.
일반적으로 함수는 fun 키워드로 선언합니다. 그런데 람다함수는 fun 키워드를 이용하지 않으며 함수 이름이 없습니다. 람다 함수는 중괄호 { } 로 표현합니다.
{ 매개변수 -> 함수 본문 }
- 람다 함수는 { } 로 표현합니다.
- { } 안에 화살표(->)가 있으며 화살표 왼쪽은 매개변수, 오른쪽은 함수 본문입니다.
- 함수의 반환값은 함수 본문의 마지막 표현식입니다.
val sum = {no1: Int, no2: Int -> no1 + no2}
람다 함수는 이름이 없으므로 함수명으로 호출할 수 없습니다. 그래서 보통은 람다 함수를 변수에 대입해 사용합니다. 위 코드에서는 sum이라는 변수에 대입했습니다. 따라서 아래처럼 호출할 수 있습니다.
sum(10, 20)
만약 람다 함수를 선언하자마자 함수를 호출하고자 한다면 오른쪽처럼 작성하면 됩니다.
{no1: Int, no2: Int -> no1 + no2} (10, 20)
중괄호 { } 부분이 람다 함수입니다. 함수는 어디선가 소괄호 ( )를 이용해 호출해 주어야 실행됩니다. 그러므로 중괄호 뒤에 소괄호를 이용해 함수를 선언하자마자 호출한 구문입니다. 소괄호 안은 람다 함수에 선언한 매개변수에 맞추어 인자를 전달한 것입니다.
함수에 매개변수가 항상 있어야 하는 것은 아닙니다. 람다 함수에서 화살표 왼쪽이 매개변수를 정의하는 부분인데 매개변수가 없을 경우 비워 두면 됩니다. 그런데 이처럼 매개변수가 없을 때는 화살표까지 생략해도 됩니다.
{-> println("function call")}
{println("function call")}
매개변수가 1개인 람다 함수의 경우에는 조금 다르게 사용할 수 있습니다. 일반적으로 매개변수가 1개인 람다함수는 다음처럼 작성합니다.
fun main() {
val some = {no: Int -> println(no)}
some(10)
}
<실행 결과>
10
그런데 위와 같은 람다 함수를 다음처럼 작성할 수도 있습니다.
fun main() {
val some: (Int) -> Unit = {println(it)}
some(10)
}
<실행 결과>
10
람다 함수 앞에 (Int) -> Unit이 매개변수가 1개인 람다 함수임을 알려줍니다. 이처럼 람다 함수의 매개변수가 1개일 때는 중괄호 안에서 매개변수 선언을 생략하고 println(it)처럼 it 키워드로 매개변수를 이용할 수 있습니다.
람다 함수도 함수이므로 자신을 호출한 곳에 결괏값을 반환해야 할 때가 있습니다. 람다 함수의 반환값은 본문에서 마지막 줄의 실행 결과입니다.
fun main() {
val some = {no1: Int, no2: Int ->
println("in lambda function")
no1 * no2
}
println("result : ${some(10, 20)}")
}
<실행 결과>
in lambda function
result : 200
함수 타입과 고차함수
코틀린에서는 함수를 변수에 대입해서 사용할 수 있습니다. 앞에서 살펴본 람다 함수도 주로 변수에 대입해서 사용했습니다. 그런데 변수는 타입을 가지며 유추할 수 있을 때를 제외하고는 생략할 수 없습니다. 그래서 변수에 함수를 대입하려면 변수를 함수 타입으로 선언해야 합니다.
함수 타입이란 함수를 선언할 때 나타나는 매개변수와 반환 타입을 의미합니다. 아래는 Int형 매개변수를 2개 받아서 결괏값을 Int 타입으로 반환하는 함수 선언입니다.
fun some(no1: Int, no2: Int): Int {
return no1 + no2
}
이를 (Int, Int) -> Int 로 표현할 수 있습니다. 함수를 대입할 변수를 선언할 때 이러한 함수 타입을 선언하고 그에 맞는 함수를 대입해야 합니다.
val some: (Int, Int) -> Int = { no1: Int, no2: Int -> no1 + no2 }
함수 타입을 typealias를 이용해 선언할 수 있습니다. typealias는 타입의 별칭을 선언하는 키워드로, 함수 타입뿐만 아니라 데이터 타입을 선언할 떄도 사용합니다. 예를 들어 정수를 표현하는 코틀린의 타입은 Int 인데 typealias를 이용해 정수를 표현하는 새로운 별칭을 선언할 수 있습니다.
typealias MyInt = Int
fun main() {
val data1: Int = 10
val data2: MyInt = 10
}
이처럼 typealias로 타입 별칭을 사용할 수 있는데 변수보다 함수 타입을 선언하는 데 주로 사용합니다.
typealias MyFunType = (Int, Int) -> Boolean
fun main() {
val someFun: MyFunType = {no1: Int, no2: Int -> no1 > no2}
println(someFun(10, 20))
println(someFun(20, 10))
}
만약 매개변수의 타입을 유추할 수 있다면 타입 선언을 생략할 수 있습니다.
typealias MyFunType = (Int, Int) -> Boolean
val someFun: MyFunType = {no1, no2 -> no1 > no2}
이처럼 타입 유추에 따른 타입 생략 기법은 typealias를 이용할 때뿐만 해당하는 것이 아니라 타입을 유추할 수 있는 상황이라면 어디서든 통합니다.
val someFun: (Int, Int) -> Boolean = {no1, no2 -> no1 > no2}
또한 다음처럼 함수의 타입을 유추할 수 있다면 변수를 선언할 떄 타입을 생략할 수도 있습니다.
val someFun = {no1: Int, no2: Int -> no1 > no2}
고차함수란 함수를 매개변수로 전달받거나 반환하는 함수를 의미합니다. 일반적으로 함수의 매개변수나 반환값은 데이터입니다. 그런데 데이터가 아닌 함수를 매개변수나 반환값으로 이용하는 함수를 고차 함수라 합니다. 이처럼 함수를 매개변수나 반환값으로 이용할 수 있는 것은 앞에서 살펴보았듯이 함수를 변수에 대입할 수 있기 때문입니다.
다음 코드에서 hotFun()은 고차 함수입니다. 매개변수를 하나 선언했지만 타입이 함수입니다. 따라서 이 함수를 호출하려면 인자로 함수를 전달해야 합니다. 또한 반환 타입 역시 함수이므로 호출 결과로 함수를 반환받습니다.
fun hotFun(arg: (Int) -> Boolean): () -> String {
val result = if(arg(10)) {
"valid"
} else {
"invalid"
}
return {"hotFun result : $result"}
}
fun main() {
val result = hotFun({no -> no > 0})
println(result())
}
<실행 결과>
hotFun result : valid
2. 널 안전성
널 안전성이란?
다음 코드에서 data1 변수에는 "hello"라는 데이터를 저장했습니다. 그런데 실제로는 "hello"라는 문자열 데이터가 저장된 주소가 대입되며 그 주소로 문자열 데이터를 이용합니다. data2 변수에는 null을 대입했습니다. 이렇게 하면 data2는 아직 주솟값을 가지지 못합니다. 즉, 변수가 선언되었지만 이용할 수 없는 상태입니다.
val data1: String = "hello"
val data2: String? = null
이처럼 널인 상태의 객체를 이용하면 널 포인트 예외가 발생합니다. 널 포인트 예외는 널인 상태의 객체를 이용할 수 없다는 오류입니다.
이때 널 안정성이란 널 포인트 예외가 발생하지 않도록 코드를 작성하는 것을 말합니다. 널 포인트 예외는 오류이므로 당연히 발생하지 않게 작성해야 하지만 중요한 건 이런 노력을 누가 하는가입니다.
다음 코드는 개발자가 널 안전성을 고려한 코드 입니다.
fun main() {
var data: String? = null
val length = if (data == null) {
0
} else {
data.length
}
println("data length : $length")
}
<실행 결과>
data length : 0
위 소스를 보면 String 타입의 data 변수를 null로 선언했습니다. if 문으로 data가 null인지 확인한 뒤 null이면 0을 반환하고 null이 아니면 data.length를 반환하게 작성했습니다. 이렇게 하면 data가 null이어도 널 포인트 예외가 발생하지 않습니다.
그런데 이렇게 널 안전성을 갖추는 노력을 개발자가 한다면 객체가 null인지 일일이 점검하는 코드를 작성해야 합니다. 즉, 널 안전성이 오롯이 개발자의 몫이라는 이야기입니다. 하지만 위 소스를 다음처럼 작성할 수도 있습니다.
fun main() {
var data: String? = null
println("data length : ${data?.length ?: 0}")
}
<실행 결과>
data length : 0
data가 null 이면 0을 반환하고 null이 아니면 length를 이용해 문자열의 개수를 얻는 소스입니다. 앞서작성한 소스와 똑같이 동작하지만 null을 점검하지는 않습니다. 즉, null 점검 코드를 작성하지 않았는데도 널 안정성을 확보한 것이죠.
널 안전성 연산자
코틀린에서는 변수 타입을 널 허용과 널 불허로 구분한다고 했습니다. 널 허용은 ? 연산자를 사용합니다.
var data1: String = "kkang"
data1 = null // 오류!
var data2: String? = "kkang"
data2 = null // 성공!
널 불허로 선언한 변수는 null을 대입할 경우 얼마든지 널 포인트 예외가 발생할 수 있습니다. 따라서 변수가 널인 상황을 고려해 프로그램을 작성해야 합니다.
다음 코드는 오류가 발생합니다.
var data: String? = "kkang"
var length = data.length // 오류!
data 변수에 데이터를 저장했더라도 변수를 널 허용으로 선언했으므로 언제든지 null이 대입될 수 있습니다. 이처럼 null이 대입될 수 있는 변수를 널 안전성을 고려하지 않고 작성하면 오류가 발생합니다.
따라서 널 허용으로 선언한 변수의 멤버에 접근할 때는 반드시 ?. 연산자를 이용해야 합니다. ?. 연산자는 변수가 null이 아니면 멤버에 접근하지만 null이면 멤버에 접근하지 않고 null을 반환합니다.
var data: String? = "kkang"
var length = data?.length // 성공!
엘비스 연산자란 ?: 기호를 말합니다. 이 연산자는 변수가 널이면 널을 반환합니다. 그런데 어떤 경우에는 변수가 널일 떄 대입해야 하는 값이나 실행해야 하는 구문이 있을 수도 있습니다. 이때 엘비스 연산자를 사용합니다.
fun main() {
var data: String? = "kkang"
println("data = $data : ${data?.length ?: -1}")
data = null
println("data = $data : ${data?.length ?: -1}")
}
<실행 결과>
data = kkang : 5
data = null : -1
!!는 객체가 널일 때 예외를 일으키는 연산자입니다. 객체가 널일 때 ?. 또는 ?: 연산자를 이용해 널 포인트 예외가 발생하지 않게 작성할 수도 있지만, 또 어떤 경우에는 널 포인트 예외를 발생시켜야 할 때도 있습니다. 이때 !! 연산자를 이용합니다.
fun some(data: String?): Int {
return data!!.length
}
fun main() {
println(some("kkang"))
println(some(null))
}
<실행 결과>
5
Excpetion in thread "main" java.lang.NullPointerException
'Android' 카테고리의 다른 글
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 7. 뷰를 배치하는 레이아웃 (0) | 2024.12.25 |
---|---|
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 6. 뷰를 이용한 화면 구성 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][2. 코틀린 이해하기] 4. 코틀린 객체지향 프로그래밍 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][2. 코틀린 이해하기] 3. 코틀린 시작하기 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][1. 안드로이드 앱 개발 준비하기] 2. 안드로이드 앱의 기본 구조 (0) | 2024.12.25 |