ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코틀린을 코틀린답게 사용하는 법 - (4) 표준 라이브러리
    안드로이드 2020. 3. 11. 14:33

    드디어 커니의 코틀린 4장에 진-입!

    원래 저번 주까지 8장 끝내는게 목표였는데,, 깔끔하게 실패했다. 😂

    그래도 늘어지지말고 파이팅 합시당 🐶❤️

     

     

    코틀린 표준 라이브러리에서 개발 시 유용하게 사용할 수 있는 함수들을 정리해보자.

    난 원래 조건을 확인하거나, 새로운 컬렉션을 만들어내거나 할 때 주로 if문 등을 이용했는데,

    이번 포스팅을 통해 표준 라이브러리에서 제공하는 함수들을 아주 능수능란하게 써서 더 깔끔하게 코드를 짤 수 있도록 연습해야겠다.

     

    조건 확인 함수

    특정 값의 일치 여부 확인 : check(), require()

    함수 혹은 생성자의 인자로 전달 받은 값을 사용하기 전에, 그 값의 유효성을 검사해야하는 경우가 있다.

    버그를 방지하기 위해 명시적으로 에러 발생 사실을 알리고 프로그램을 종료해야 하는 경우 check() 함수 또는 require() 함수를 사용하여 쉽게 처리할 수 있다.

    이 두 함수 모두 인자로 받은 표현식이 참이 아닌 경우 예외를 발생시킨다.

    • check(): IllegalStateException을 발생
      • fun check(value: Boolean)
      • fun check(value: Boolean, lazyMessage: () -> Any) - 예외를 발생시키고, lazyMessage로 넘겨진 함수 실행
    • require(): IllegalArgumentException을 발생
      • fun require(value: Boolean)
      • fun require(value: Boolean, lazyMessage: () -> Any) - 예외를 발생시키고, lazyMessage로 넘겨진 함수 실행
    fun showMessage(isPrepared: Boolean, message: String) {
        // 인자로 받은 isPrepared 값이 true가 아니라면 IllegalStateException 발생
        check(isPrepared)
        // 인자로 받은 message 문자열의 길이가 10 이상이 아니라면 IllegalArgumentException 발생
        require(message.length > 10)
        
        println(message)
    }

     

    또한, checkNotNull() 함수와 requireNotNull() 함수로 특정 값의 null 여부를 확인하고 nul이 아닌 값을 반환 받을 수 있다.

    fun showMessage(isPrepared: Boolean, message: String?){
        check(isPrepared)
        
        // 값 msg에는 인자로 받은 message 값이 null 값이 아닐 때에만 해당 값이 할당
        val msg = requiredNotNull(message)
        require(msg.length > 10)
        
        println(message)
    }

     

     

    명시적으로 실행 중단: error, TODO

    정상적으로 프로그램이 실행될 경우 호출될 가능성이 없는 영역이 있는데, 알 수 없는 이유로 실행 시점에서 이 영역에 진입하게 되는 경우가 있다. 그러면 그 부작용을 예측하기가 어려운데, 이 영역에 진입하게 되는 경우 임의로 예외를 발생시켜 프로그램의 실행을 막을 수 있다.

    코틀린에서는 error() 함수를 이용하여 이를 간편하게 구현할 수 있다.

    • fun error(message: String) : Nothing - 인자로 받은 message와 함께 IllegalStateException을 발생
    fun showMessage(isPrepared: Boolean, message: String){
        // 인자로 받은 값 isPrepared가 거짓일 경우
        // IllegalStateException: Not prepared yet 예외가 발생
        if(!isPrepared) {
        	error("Not prepared yet")
        }
        println(message)
    }

     

    또한, TODO 함수는 다음과 같이 정의되어 있다.

    • fun TODO(): Nothing - NotImplementedError 예외를 발생시켜 이 부분이 완성되지 않았음을 알려줌
    • fun TODO(reason: String): Nothing - 에러 메시지에 표시될 상세 내용을 reason 매개변수를 통해 전달

     

     

    컬렉션 생성 함수

    배열

    • fun <T> arrayOf(vararg elements: T): Array<T>
    • fun <T> emptyArray(): Array<T> - 특정 타입을 갖는 빈 배열 반환
    • fun <T> arrayOfNulls(size: Int): Array<T?> - 배열 내 각 값들이 모두 null 값으로 초기화

    자바의 원시 타입을 포함하는 배열은 코틀린의 배열과 다른 타입으로 취급된다.

    • fun 타입ArrayOf(vararg elements: 타입) : 타입Array

     

     

    리스트

    포함하는 요소를 읽을 수만 있고 수정할 수 없는 immutable인 리스트는 다음과 같이 생성할 수 있다.

    • fun <T> listOf(vararg elements: T): List<T>
    • fun <T : Any> listOfNotNull(vararg elements: T?): List<T>

     

    리스트에 포함된 요소를 수정할 수 있는 리스트는 mutableListOf() 함수를 사용하여 생성한다.

    • fun <T> mutableListOf(vararg elements: T): MutableList<T>

     

    또한, 안드로이드 앱 개발시 자주 사용하는 자료구조 중 하나인 ArrayList 또한 표준 라이브러리에서 제공하는 함수인 arrayListOf()를 사용해 쉽게 생성할 수 있다.

    • fun <T> arrayListOf(vararg elements: T): ArrayList<T>

    💡MutableList와 ArrayList 차이점,,은 무엇일까? 아시는 분은 댓글 달아주세여,,!

     

     

    포함하는 요소를 읽을 수만 있고 수정할 수 없는 immutable인 맵은 다음과 같이 생성할 수 있다.

    • fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>

     

    맵에 포함된 요소를 수정할 수 있는 맵은 mutableMapOf() 함수를 사용하여 생성한다.

    • fun <K, V> mutableMapOf(vararg pairs: Pair<K, V>): MutableMap<K, V>

     

    보다 명시적인 타입의 맵을 생성해야 하는 경우 hashMapOf(), linkedMapOf(), sortedMapOf() 함수를 사용할 수 있다.

    • fun 타입MapOf(vararg pairs: Pair<K, V>) : 타입Map<K, V>

     

     

    집합

    집합(set)은 중복되지 않는 요소들로 구성된 자료구조이다. 포함하는 요소를 읽을 수만 있고 수정할 수 없는 immutable인 집합은 다음과 같이 생성한다.

    • fun <T> setOf(vararg elements: T): Set<T>

    집합에 포함된 요소를 수정할 수 있는 집합은 mutableSetOf() 함수를 사용하여 생성한다.

    • fun <T> mutableSetOf(vararg elements: T): MutableSet<T>

    보다 명시적인 타입의 집합을 생성해야 하는 경우 hashSetOf(), linkedSetOf(), sortedSetOf() 함수를 사용할 수 있다.

    • fun <T> 타입SetOf(vararg elements: T) : 타입Set<T>

     

     

    스트림 함수

    자바 8에서는 리스트나 맵과 같은 컬렉션에 포함된 자료들을 손십게 다룰 수 있도록 스트림(stream) 기능을 제공한다. 스트림에서 제공하는 연산자들을 사용하면 컬렉션에 포함된 자료들을 다른 타입으로 변경하거나, 새로운 자료를 생성하는 등의 작업을 쉽게 구현할 수 있다.

     

    변환

    • map(): 컬렉션 내 인자를 다른 값 혹은 타입으로 변환할 때 사용한다.
    val cities = listOf("Seoul", "Tokyo", "Mountain View")
    
    // 도시 이름을 대문자로 변환
    cities.map{ city -> city.toUpperCase() }
    		.forEach{ println(it) }
    
    // 도시 이름을 받아, 각 이름의 문자열 길이로 변환
    cities.map{ city -> city.length }
    		.forEach{ println("length=$it") }

     

    • mapIndexed(): 사용하면 컬렉션 내 포함된 인자의 인덱스 값을 변환 함수 내에서 사용할 수 있다.
    // 0부터 10까지 정수를 포함하는 범위
    val numbers = 0..10
    
    // 변환 함수에서 각 인자와 인덱스를 곱한 값을 반환
    numbers.mapIndexed{ idx, number -> idx * number }
    		.forEach{ print("$it ")}
           
    // 0 1 4 9 16 25 36 49 64 81 100 출력

     

    • mapNotNull(): 컬렉션 내 각 인자를 변환함과 동시에, 변환한 결과가 null 값인 경우 이를 무시한다.
    • flayMap(): map()과 달리 변환 함수의 반환형이 Interable. 따라서 하나의 인자에서 여러 개의 인자로 매핑이 필요한 경우 사용한다.
    val numbers = 1..6
    
    // 1부터 시작하여 각 인자를 끝으로 하는 범위를 반환
    numbers.flatMap{ number -> 1..number }
    		.forEach{ println("$it ") }
            
    // 1 1 2 1 2 3 1 2 3 4 1 2 3 4 5 1 2 3 4 5 6 출력

     

    • groupBy(): 컬렉션 내 인자들을 지정한 기준에 따라 분류하며, 각 인자들의 리스트를 포함하는 맵 형태로 결과를 반환한다.
    val cities = listOf("Seoul", "Tokyo", "Mountain View")
    
    cities.grouyBy{ city -> if(city.length <= 5) "A" else "B" }
    		.forEach{ key, cities -> println("key=$key cities=$cities") }
            
    // key=A cities=[Seoul, Tokyo]
    // key=B cities=[Mountain View] 출력

     

     

    filter 등 빠짐

     

    범위 지정 함수

    개발을 하다 보면 특정 객체에 있는 함수를 연속해서 사용하거나, 다른 함수의 인자로 전달하기 위해 변수를 선언하고 다른 곳에서 사용하지 않는 등의 경우가 있다. 이럴 때 유용하게 사용할 수 있는 함수를 표준 라이브러리를 통해 제공한다.

     

    let() 함수

    : 해당 함수를 호출한 객체를 이어지는 함수 블록의 인자로 전달한다.

    함수의 정의 반환 값
    fun <T, R> T.let(block: (T) -> R): R block 함수의 결과

     

    쓰임새

    1. 불필요한 변수 선언을 방지할 수 있음

    ex) 커스텀 뷰를 작성하다 보면 길이를 계산한 값을 변수에 저장해 두고, 이를 함수 호출 시 인자로 전달하는 경우가 있는데, let() 함수를 사용하면 변수 선언 없이 계산한 값을 함수의 각 인자로 전달할 수 있다.

    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 
    	16f, resources.displayMetrics).toInt().let {
        // 계산된 값을 인자로 받으므로, 함수에 바로 대입 가능
        setPadding(it, 0, it, 0)
    }

     

    2. null 값이 아닌 경우를 체크한 후 특정 작업을 수행하는 코드에 사용

    fun doSomething(message: String?) {
        // message가 널이 아닌 경우에만 let 함수를 호출
        message?.let{
        	Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        }
    }

     

     

    apply() 함수

    : 해당 함수를 호출한 객체를, 이어지는 함수 블록의 리시버(receiver)로 전달한다.

    함수의 정의 반환 값
    fun <T> T.apply(block: T.() -> Unit): T 함수를 호출한 객체 반환

    함수를 호출한 객체를 함수형 인자 block의 리시버로 전달하므로, 이 블록 내에서는 해당 객체 내의 프로퍼티나 함수를 직접 호출할 수 있다. 따라서 객체 이름을 일일이 명시하지 않아도 된다.

     

    쓰임새

    1. 객체 이름 없이 직접 해당 객체 내부에 속성에 접근할 수 있음

    val param = LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.WRAP_CONTENT,
        LinearLayout.LayoutParams.WRAP_CONTENT).apply {
            gravity = Gravity.CENTER_HORIZONTAL
            weight = 1f
            topMargin = 100
            bottomMargin = 100
        }

     

     

    with() 함수

    : 인자로 받은 객체를 이어지는 함수 블록의 리시버로 전달한다.

    함수의 정의 반환 값
    fun <T, R> T.with(receiver: T, block: T.() - >R): R block 함수의 결과

     

    쓰임새

    1. 함수의 인자로 전달 받은 객체 내부에 속성에 접근할 수 있음

    fun manipulateView(messageView: TextView) {
    
        // 인자로 받은 messageView의 여러 속성을 변경
        with(messageView) {
            text = "Hello, World"
            gravity = Gravity.CENTER_HORIZONTAL
        }
    }

     

    💡 해당 함수는 let(), apply() 함수와 달리 이 함수에서 사용할 객체를 매개변수를 통해 받는다.

    따라서 안전한 호출을 사용하여 인자로 전달되는 객체가 널 값이 아닌 경우 함수의 호출 자체를 막는 방법을 사용할 수 없으므로 null 값이 아닌 것으로 확인된 객체에 이 함수를 사용해야한다.

     

     

    run() 함수

    : 인자가 없는 익명 함수처럼 사용하는 형태와, 객체에서 호출하는 형태를 제공

    함수의 정의 반환 값
    fun <R> run(block: () -> R): R block 함수의 결과
    fun <T, R> T.run(block: T.() -> R): R 해당 함수를 호출한 객체를 함수형 인자 block의 리시버로 전달 후 그 결과 반환

     

    쓰임새

    1. 인자가 없는 익명 함수처럼 사용하는 경우, 복잡한 계산을 위해 여러 임시 변수가 필요할 때 유용하게 사용

    run() 함수 내부에서 선언되는 변수들은 블록 외부에 노출되지 않으므로 변수 선언 영역을 확실히 분리

    val padding = run {
        // 이 블록 내부에서 선언하는 값들은 외부에 노출되지 X
        val defaultPadding = TypedValue.applyDimension(...)
        val extraPadding = TypedValue.applyDimension(...)
        
        // 계산된 값을 반환
        defaultpadding + extraPadding
    }

     

    2. 객체에서 run() 함수를 호출하는 경우 with() 함수와 유사한 목적으로 사용.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 액티비티 생성 시, 기존에 저장된 값이 있는 경우 UI 복원 수정
        savedInstanceState?.run {
        
            // Bundle 내에 저장된 값 추출
            val selection = getInt("last_selection")
            val text = getString("last_text")
            
            // UI 복원
            ...
        }
    }

    💡 run() 함수는 안전한 호출을 사용할 수 있으므로 null 값일 수 있는 객체의 속성이나 함수에 연속적으로 접근해야 할 때 유용

     

     

     

    위의 4가지 함수와 더불어 also() 라는 함수도 존재하는데, 이는 apply와 거의 같은 역할을 하며 정리는 아래의 표로 정리하도록 하겠다.

    아래의 표는 코틀린 표준 범위 지정 함수의 차이점을 정리해 놓은 것이다.

     

    코틀린 표준 범위 지정 함수의 차이점

     

     

    헷갈리는 위 5가지 함수를 적재적소에 사용하고 싶으면 어떻게 해야할까?

     

    1. apply vs run

    apply는 객체의 생성과 동시에 연속된 작업을 할 때 사용하고,

    run은 이미 생성된 객체에 연속된 작업을 할 때 사용한다.

     

    2. apply vs also

    apply는 수신 객체의 함수는 사용하지 않고, 프로퍼티를 set하거나 get할 때 사용하고,

    also는 수신 객체의 속성을 변경하지 않는 경우에 사용한다.

    class Book(author: Person) {
        val author = autor.also {
            requireNotNull(it.age)
            print(it.name)
        }
    }

     

    3. let

    • 지정된 값이 null이 아닌 경우에만 코드를 실행해야 하는 경우
    • Nullable 객체를 다른 Nullable 객체로 변환하는 경우
    • 단일 지역 변수의 범위를 제한하는 경우에 let을 사용한다.
    getNullablePerson()?.let {
        promote(it)
    }
    
    val driversLicense: License? = getNullablePerson()?.let {
        // nullable person 객체를 nullable driversLicense 객체로 변경
        lincenseService.getDrviersLicense(it)
    }
    
    val person: Person = getPerson()
    getPersonDao().let { dao ->
        // 변수 dao의 범위는 이 블록 안으로 제한
        dao.insert(person)
    }

     

    4. with

    Null이 될 수 없는 수신 객체이고, 결과가 필요하지 않는 경우에 with를 사용한다.

    val person: Person = getPerson()
    with(person) {
        print(name)
        print(age)
    }

     

     

     

     

     

     

     

    드뎌 기본적인 코틀린을 코틀린답게 사용하기 시리즈가 끝났다...! 👏

    아주 기본부터 다시 시작하지는 않고, 내가 생각했을 때 중요하고 프로젝트를 할 때 잘 쓰일 것 같은 것만 써서 몇부분 빠진 것도 있지만,,

    일단 포스팅을 4개나 했다는 것에 뿌-듯

     

     

     

     

     

Designed by Tistory.