본문 바로가기

Programming/Kotlin

[5일차] 깡샘의 안드로이드 앱 프로그래밍 with 코틀린(Do it!) / 05 코틀린의 유용한 기법

해당 내용은 이지스 퍼블리싱 카페에서 공부하며 올렸던 글을 다듬었습니다.

< 람다 함수와 고차 함수 >

[ 람다 함수 ]

- 익명 함수(anonymous function) 정의 기법

- 주로 함수를 간단하게 정의할 때 사용

- 고차 함수를 이해하고 사용하려면 꼭 알아야 함

- fun 키워드를 이용하지 않고, 이름도 없음

- 중괄호 { }로 표현

// 함수 선언 형식
fun 함수명(매개변수) { 함수 본문}

// 람다 함수 선언 형식
{ 매개변수 -> 함수 본문 }

 

 

[ 람다 함수 - 사용 규칙 ]

- 람다 함수는 { }로 표현

- { } 안에 화살표(->)가 있으며 화살표 왼쪽은 매개변수, 오른쪽은 함수 본문

  { 매개변수 -> 함수 본문 }

- 함수의 반환 값은 함수 본문의 마지막 표현식 (return문 사용 불가)

 

 

- 람다 함수 선언 방법 예제

// 일반 함수 선언
fun sum(no1: Int, no2: Int): Int {
    return no1 + no2
}

// 람다 함수 선언
val sum = { no1: Int, no2: Int -> no1 + no2 }

 

 

- 람다 함수는 이름이 없어 함수명으로 호출할 수 없음

  * 보통은 람다 함수를 변수에 대입해서 사용

// 람다 함수 호출문
sum(10, 20)

// 람다 함수 선언과 호출
{ no3: Int, no4: Int -> no3 + no4 } (10, 20))

 

 

- 매개변수가 없는 람다 함수

  * 화살표까지 생략 가능

// 매개변수가 없는 람다 함수
{-> println("function call 1")}
{println("function call 2")}    // 화살표 생략 가능!

 

 

- 매개변수가 1개인 람다 함수

  * it 키워드 사용 시 매개 변수의 타입을 식별할 수 있을 때만 가능

  * 함수 타입이 무조건 있어야 함

    예) (Int) -> Unit = { println(it) }

// 매개변수가 1개인 람다 함수 => 일반적인 방법
val some = { no: Int -> println(no) }
some(10)

----- < 실행 결과 > -----
10
------------------------



// 매개변수가 1인 람다 함수에 it 키워드 사용
// (Int) -> Unit 은 함수 타입이라고 함
val some1: (Int) -> Unit = { println(it) }
some1(15)

----- < 실행 결과 > -----
10
------------------------

 

 

- 람다 함수의 반환값마지막 줄의 실행 결과

  * return문 사용 불가

// 람다 함수의 반환문
val some2 = {no1: Int, no2: Int ->
    println("in lambda function")
    no1 * no2    // 반환값
}
println("result: ${some2(10, 20)}")

----- < 실행 결과 > -----
in lambda function
result: 200
------------------------

 

 

- 함수 타입과 고차 함수

  * 변수는 타입을 가지며 타입을 유추할 수 있을 때를 제외하고는 생략 불가!

     => 변수를 함수 타입으로 선언해야 함

 

 

 

[ 함수 타입 ]

- 함수 타입이란?

  : 함수를 선언할 때 나타내는 매개변수와 반환 타입을 의미

// 함수 타입을 이용해 함수를 변수에 대입
//         ┌--- 함수 타입 --┐   ┌----------- 함수 내용 -----------┐
val some4: (Int, Int) -> Int = { no1: Int, no2: Int -> no1 + no2 }

 

 

- 타입 별칭(typealias) : 별칭을 선언하는 키워드. 함수 타입뿐만 아니라 데이터 타입을 선언할 때도 사용

  * 변수보다 함수 타입을 선언하는데 주로 사용

// 타입 별칭 선언
typealias MyInt = Int

fun main() {
    // 타입 별칭 사용
    val data1: Int = 10
    val data2: MyInt = 10
}
// 함수 타입 별칭
typealias MyFunType = (Int, Int) -> Boolean

fun main() {
    val someFun: MyFunType = {no1: Int, no2: Int ->
        no1 > no2
    }

println(someFun(10, 20))
println(someFun(20, 10))


----- < 실행 결과 > -----
false
true
------------------------

 

 

- 매개변수의 타입을 유추할 수 있다면 타입 선언 생략 가능

  * 타입 유추에 따른 타입 생략 기법은 타입이 유추 가능한 상황이면 어디서든 통함!!

// 매개변수 타입을 생략한 함수 선언
typealias MyFunType = (Int, Int) -> Boolean

val someFun2: MyFunType = { no1, no2 ->
        no1 > no2
}
// 매개변수 타입 선언 생략 예
// typealias를 사용하지 않았으나 someFun에 매개변수 타입을 Int로 선언했으므로
// 람다 함수에서 매개변수의 타입을 생략할 수 있음
//            ┌--------------------┐
val someFun1: (Int, Int) -> Boolean = {no1, no2 ->
    no1 > no2
}

// 변수 선언 시 타입 생략
val someFun2 = {no1: Int, no2: Int ->
    no1 > no2
}

 

 

 

 

[ 고차 함수 ]

- high order function

- 함수를 매개변수로 전달받거나 반환하는 함수

- 함수를 변수에 대입할 수 있기 때문에 사용 가능

// 고차 함수 예제
fun hofFun(arg: (Int) -> Boolean): () -> String {
    val result = if (arg(10)) {  // arg(10) 실제 값이 입력됨
        "valid"
    } else {
        "invalid"
    }
    return { "hofFun result: $result" }
}

fun main() {
    val result = hofFun({ no -> no > 0 })
    println(result())
}


----- < 실행 결과 > -----
hofFun result: valid
------------------------

 

 

 

 

 

 

< 널 안전성 (null Safety) >

- 널 포인트 예외가 발생하지 않도록 코드를 작성하는 것!!!

- null은 객체가 선언되었지만 초기화되지 않은 상태 (객체가 주소를 가지지 못한 상태)

- 객체는 참조 변수

- 객체에는 주소가 저장되며 이 주소로 메모리에 접근해서 데이터를 이용

// 객체에 널 대입
val dataOne: String = "hello"    // 실제로는 hello가 저장된 주소가 대입되며 그 주소로 문자열 데이터를 이용
val dataTwo: String? = null      // 선언되었지만 이용할 수 없는 상태 (주솟값을 가지지 못한 상태)

 

 

- null인 상태의 객체를 이용하면 널 포인트 예외(NullPointException)가 발생!!

// 널 안정성을 개발자가 고려한 코드 => 이러면 널 안전성이 오롯이 개발자의 몫임
var data: String? = null
var length = if(data == null) {
    0
} else {
    data.length
}
// 코틀린이 제공하는 널 안전성 연산자를 이용한 코드
var data1: String? = null
println("data1 length: ${data1?.length ?: 0}")

 

- 코틀린이 널 안전성을 제공하는 이유

: 객체가 널인 상황에서 널 포인터 예외가 발생하지 않도록 연산자를 비롯해 여러 기법을 제공한다는 의미

 

 

[ 널 안전성 연산자 ]

- 널 허용 ? 연산자

  * 널 허용과 널 불허로 구분

  * 변수에 null을 대입할 수 있는지를 선언할 때 타입으로 구분

var data1: String = "kkang"
data1 = null    // 오류! - 널 불허

//               ↓ 널 허용으로 선언
var data2: String? = "kkang"
data2 = null     // 성공! - 널 허용

 

 

- 널 안전성 호출 ?. 연산자

  * ?. 연산자는 변수가 null이 아니면 멤버에 접근하지만,

    null이면 멤버에 접근하지 않고 null을 반환

// 널 안전성 호출 연산자 사용
//               ↓ 물음표(?): 널 허용(nullable) 연산자
var data3: String? = "kkang"
var length1 = data3?.length   // 성공!
//                 ↑ ?. 연산자(널 안전성 호출 연산자)는
//                  변수가 null이 아니면 멤버에 접근,
//                   null이면 멤버에 접근하지 않고 null 반환

println(length1)    // null이 아니라서 멤버에 접근
// └> 출력 결과
//    null이면 null 출력
//    null이 아니면 length 출력

 

 

- 엘비스 ?: 연산자

  * 변수가 null이면 null을 반환

  * 변수가 null일 때 대입해야 하는 값이나 실행해야 하는 구문이 있을 때 사용

// 엘비스 연산자(?:) 사용 : 변수가 null일 때 대입해야 하는 값이나 실행해야 하는 구문이 있을 때 사용
var data4: String? = "kkang"
println("data4 = $data4 : ${data4?.length ?: -1}")
data4 = null
println("data4 = $data4 : ${data4?.length ?: -1}")    // null이면 -1 출력

 

 

- 예외 발생 !! 연산자

* !!는 객체가 null일 때 예외(exception)를 일으키는 연산자

* 널 포인트 예외를 발생시켜야 할 때 사용

// 예외 발생 연산자(!!) : null 포인트 예외를 발생시켜야 할 때 사용
fun some(data: String?): Int {
    return data!!.length
}

fun main() {
    println(some("kkang"))
    println(some(null))
}


----- < 실행 결과 > -----
5
Exception in thread "main" java.lang.NullPointerException (null exception 오류 발생)
------------------------