-
코틀린을 코틀린답게 사용하는 법 - (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개나 했다는 것에 뿌-듯
'안드로이드' 카테고리의 다른 글
안드로이드 프로젝트에 RxJava 적용하기 (1) 2020.04.12 '슥삭'의 아키텍쳐 (1) MVVM 디자인패턴 (0) 2020.03.11 코틀린을 코틀린답게 사용하는 법 - (3) (0) 2020.03.08 코틀린을 코틀린답게 사용하는 법 - (2) (1) 2020.03.03 코틀린을 코틀린답게 사용하는 법 - (1) (0) 2020.02.29 - check(): IllegalStateException을 발생