Effective Kotlin - Item03. 최대한 플랫폼 타입을 지양하자

Platform Type

  • 코틀린의 Null 타입 안정성은 자바에서 빈번하게 볼수 있었던 NPE (Null-Pointer Exception)이 발생할 포인트를 줄여준다.

  • 문제는 코틀린과 다른 프로그래밍 언어를 연결해서 사용할때이다. 아래와 같이 annotation 으로 nullable 여부를 판단할 수 있다면 정확한 타입을 추정할 수 있다.

1
2
3
4
5
public class Sample {

@NotNull
private String test;
}

만약 annotation이 붙어있지 않다면 , Kotlin은 해당 타입을 nullable인지 아닌지 판단할 방법이 없다. 따라서 Kotlin은 해당 타입을 Platform Type으로 간주한다. Platform Type은 타입 이름뒤에 ! 기호를 붙여서 표기한다 (String!)

Platform Type은 코틀린이 Null 관련 정보를 알 수 없기 떄문에, 컴파일러는 Nullable 타입으로 처리하든, 아니든 모든 연산을 허용한다 하지만 개발자에게 전적으로 NPE을 처리할 책임이 넘어간다.

JSR-Annotation

  • java와 코틀린을 같이 사용할때에는 Java 코드를 만약 직접 변경할 수 있다면, 가능한 @Nullable , @NotNull annotation을 붙이는 게 안전하다.

대표적인 annotation 예시는 다음과 같다.

  • org.jetbrains.annotation : @Nullable , @NotNull
  • javax.annotation JSR-305 : @Nullable , @NonNull
  • Lombok :@NonNull

정리

  • 다른 프로그래밍 언어에서 Nullable 여부를 알 수 없는 타입을 Platform type이라고 하며 , 코틀린 컴파일러는 이에 대한 연산을 제약하진 않는다. 다만, 개발자가 전적으로 NPE을 핸들링 해야 한다, 따라서 Platform Type을 지양하고, 자바 쪽에서 수정이 가능하다면 Annotation 을 붙이자.
Read more

Effective Kotlin - Item02. 변수의 스코프를 최소화하라

변수의 스코프 최소화

  1. property보다는 지역변수를 사용하는게 좋다.
  2. 최대한 변수의 사용 스코프를 줄이는게 좋다. 예를 들어 변수가 반복문안에서만 사용된다면, 변수를 반복문 블록 내부에 작성하는 게 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1
var user : User
for( i in users.indices){
user = users[i]
println("user at $i is $user")
}
// 2
for(i in users.indices){
val user = users[i]
println("user at $i is $user")
}
// 3
for((i,user) in users.withIndex()){
println("user at $i is $user")
}
  • 1번예는 user를 반복문 블록 외부에서도 사용 가능하다. 반면 2,3번 예에서는 user의 스코프 블록을 for 반복문 내부로 제한한다.

  • 2번,3번예는 변수를 반복문 내부로 감추고, 3번예의 경우에는 구조 분해 선언을 통해 변수를 초기화하고 있다. 이렇게 변수의 스코프를 좁게 만듦으로서 갖는 장점은 프로그램 변경 요소를 줄여, 이해하기 쉽고 디버깅이 쉽게 만든다.

  • 반대로 변수의 스코프 범위가 너무 넓으면 다른 개발자에 의해 변수가 잘못 사용될 가능성이 있다. 따라서 변수는 정의할때 초기화되는게 가장 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
//1
val user:User
if(hasValue){
user = getValue()
} else{
user = User();
}
//2
val user:User = if(hasValue){
getValue()
}else {
User()
}

위 코드에서처럼 2번과 같이 선언과 동시에 초기화하는것이 좋다. 만약 여러 프로펕치를 한꺼번에 설정해야 한다면 구조분해 선언을 활용하는게 좋다.

1
2
3
4
5
6
7
fun updateWeather(degrees: Int) {
val(description, color) = when{
degrees <5 -> "cold" to Color.BLUE
degrees >23 -> "mild" to Color.YELLOW
else -> "hot" to Color.RED
}
}
Read more

Effective Kotlin - Item01.가변성을 제한하라

  • 클래스,프로퍼티와 같은 요소가 var 또는 mutable 객체를 사용하면 상태를 가질 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BankAccount{
// 가변 상태
var balance = 0.0
private set
fun deposit(depositAmount :Double){
balance += depositAmount
}
@Throws(InsufficientFunds::class)
fun withdraw(widthdrawAmount : Double){
if (balance < widthdrawAmount){
throw InsufficientFunds()
}
balance -= widthdrawAmount
}
}

class InsufficientFunds :Exception()

위처럼 BankAccount 클래스는 잔액을 나타내는 변화할 수 있는 상태가 있다, 변화할 수 있는 상태를 가지는 요소는 다음과 같은 단점을 가진다.

  • 프로그램을 이해하고 디버깅하기 힘들어진다. 오류시 상태 변경을 추적해야 한다.
  • 멀티쓰레드 환경에서 동기화가 필요하다
  • 테스트가 어렵다. 모든 상태에 대해서 테스트를 염두해두어야 한다.
  • 상태변경에 따른 추가적인 조치가 필요할수도 있다. 예를 들면 항상 정렬된 경우로 유지되야할경우 값이 추가되었을떄 정렬작업이 필요하다.

반면 불변성을 유지하였을때 갖는 장점은 다음과 같다.

  • 한번 객체의 상태가 정의되고 나서 변경되지 않으므로, 코드 이해가 쉽다.
  • 병렬 처리에 안전
  • 방어적 복사본을 만들지 않아도 된다.
  • Set,Map의 Key로 사용이 가능하다. 요소의 값이 변경되지 않기 때문이다.

멀티쓰레드환경에서 쓰레드간 공유되는 변수의 값을 변경할때 가변상태를 가지는 경우 값이 부정확하게 나올 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
fun main() {
var num = 0
for (i in 1..1000){
thread {
Thread.sleep(10)
num+=1
}
}
Thread.sleep(5000)
println(num)
}

위 연산은 매번 실행할떄마다 공유변수에 값을 여러 쓰레드에서 변경함으로 , 연산이 덮어씌워지는 경우가 생겨 다른 값이 나온다.
이를 동기화하려면 아래와 같이 공유변수에 Lock을 걸어서, 접근을 제한하고 순차적으로 값을 증가시켜야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
var lock = Any()
var num = 0
for (i in 1..1000){
thread {
Thread.sleep(10)
synchronized(lock){
/*Lock을 획득하고 공유변수에 접근가능하도록 동기화*/
num+=1
}
}
}
Thread.sleep(5000)
println(num)
}

Kotlin에서 가변성을 제한하는 방법

  • Kotlin은 언어차원에서 가변성을 제한할수 있는 방법을 설계하였다.

val

  • Kotlin은 읽기 전용 프로퍼티 (val)을 사용하여, 변수에 재할당이 불가능하도록 만들 수 있다 (java의 final과 유사) 사실 val을 사용한다고 해서 불변성이 보장되는 것은 절대 아니고, 단지 재할당이 불가능하게 setter를 금지한다.
1
2
val x = mutableListOf(1,2,3)\
x.add(4) // 가변
  • 부가적인 내용인데, val는 var로 overriding 이 가능하다.
1
2
3
4
5
6
7
interface Element{
val active : Boolean
}

class ActualElement : Element{
override var active: Boolean = false
}

가변 Collection과 읽기 전용 Collection (read-only)

  • Kotlin은 Collection을 MutableCollection과 읽기 전용인 Collection으로 구분한다.

  • Kotlin은 Collection , Set ,List를 기본적으로는 읽기 전용으로 내부의 상태를 변경하기 위한 method를 제공하지 않는다. MutableCollection , MutableSet , MutableList 인터페이스는 읽기 전용 인터페이스를 상속받아서, 추가적으로 변경을 위한 method를 붙였다.

  • 주의해야할점은 읽기 전용 Collection 을 가변 Collection으로 downcasting하면 안된다는 점이다.

1
2
3
4
val list = listOf(1,2,3)
if (list is MutableList){
list.add(4) // java.lang.UnsupportedOperationException 예외 발생
}
  • Jvm에서 listOf는 Java의 List 인터페이스를 구현한 Array.ArrayList 객체를 반환하는데 이는 add,set 을 모두 가지고 있기에 MutableList로 다운캐스팅이 된다. 하지만 Arrays.ArrayList 객체는 이러한 연산을 구현하고 있지 않기 떄문에 위와 같이 UnsupportedOperationException이 터진다.

읽기 전용 Collection에서 MutableCollection으로 꼭 변경해야 한다면 , copy를 사용해서 변경해야 한다.

1
2
3
4
fun main() {
val list = listOf(1,2,3)
list.toMutableList(); // 새로운 객체 반환
}

이렇게 구현하면 기존 객체는 새로 반환된 객체에 영향받지 않고 수정이 가능하다.

Data Class의 Copy

  • immutable 객체는 자기 자신의 상태가 일부 다른 경우에도 새로운 객체를 만들어야 되기 때문에, 자신의 일부를 수정해서 새로운 객체를 만들어 줄 수 있는 method를 가져야 한다.

  • 이떄 data 한정자를 붙이면 자동으로 copy method를 만들어주는데, copy method는 모든 기본생성자 프로퍼티가 동일한 새로운 객체를 만들어 낼 수 있다. 따라서 원래의 불변객체가 존재하는데, 특정 상태만 바꾼 새로운 객체를 만들어내고 싶다면 copy method를 활용하면 된다.

1
2
3
4
5
6
7
data class Account(val money:Int,val owner:String)

fun main() {
val myPoorAccount = Account(10000,"김찬수")
val myHappyAccount = myPoorAccount.copy(money = 1000_000_000);
println(myHappyAccount)
}
Read more

Annotation use-site targets

사용 지점 대상 Annotation

  • 코틀린에서는 소스코드에서 한 선언을 컴파일한 결과가 여러 자바 선언과 대응되는 경우가 많다. 따라서 여러 자바 선언에서 각각 Annotation을 붙여야 하는 경우가 존재한다.

예를 들면 코틀린의 property는 기본적으로 Java의 property + Getter method 선언과 대응된다.
만약 var 타입이라면 Setter method선언까지 대응되는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Kotlin Code
class Test(var value:String?)

// Java Code
final class Test {

String value;

public String getValue(){
return this.value;
}

public void setValue(String value){
this.value = value;
}
}

따라서 코틀린에서 특정 프로그래밍 요소에 Annotation을 붙일떄는 사용 지점 대상 선언으로 어떤 프로그래밍 요소에 Annotation을 붙일 것인지 명시할 수 있다.

사용 지점 대상은 @적용대상:Annotation명으로 아래와 같이 사용한다.

1
2
// 사용 지점 대상  : 적용 Annotation 명 
@get:Rule

위 코드의 뜻은 @Rule Annotation을 Getter 에 적용하라는 뜻이다.

Read more

코루틴(Coroutines)

코루틴이란?

  • 비선점형 (협력형) 멀티 태스킹 (non-preemptive multitasking)으로 실행을 일시 중단(suspend) 하고 재개(resume) 할 수 있는 여러 진입 지점을 허용한다.

  • 서로 협력해서 실행을 주고받으면서 작동하는 여러 서브루틴을 말한다.

  • 일반적인 서브루틴은 오직 한 가지 진입 지점만을 가진다. 함수를 호출하는 부분이며, 그때마다 활성 레코드 (activation record)가 스택에 할당되면서 서브루틴 내부의 로컬 변수등이 초기화 된다. 또한 서브루틴에서 반환되고 나면 활성 레코드가 스택에서 사라지기 떄문에 모든 상태를 잃어버린다.

1
2
3
4
5
6
7
8
9
fun main() {
//main routine
val extension = getFileExtension("cs.txt")
}

fun getFileExtension(fileName:String) :String{
// subroutine
return fileName.substringAfter(".")
}
  • 반면 코루틴은 실행을 일시 중단하고 진입 지점을 허용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fun log(msg:String , self : Any?) = println("Current Thread : ${Thread.currentThread().name} / this : $self :$msg")


fun main() {
log("main routine started",null)
yieldExample()
log("main routine ended",null)
}


fun yieldExample(){
runBlocking{ //내부 코루틴이 모두 끝난뒤 반환
launch {
log("1",this)
yield() //
log("3",this)
yield()
log("5",this)
}
log("after first launch",this)
launch {
log("2",this)
yield()
log("4",this)
yield()
log("6",this)
}
log("after second launch",this)
}

}

실행로그

1
2
3
4
5
6
7
8
9
10
Current Thread : main / this : null :main routine started
Current Thread : main / this : BlockingCoroutine{Active}@33e5ccce :after first launch
Current Thread : main / this : BlockingCoroutine{Active}@33e5ccce :after second launch
Current Thread : main / this : StandaloneCoroutine{Active}@2ac1fdc4 :1
Current Thread : main / this : StandaloneCoroutine{Active}@3ecf72fd :2
Current Thread : main / this : StandaloneCoroutine{Active}@2ac1fdc4 :3
Current Thread : main / this : StandaloneCoroutine{Active}@3ecf72fd :4
Current Thread : main / this : StandaloneCoroutine{Active}@2ac1fdc4 :5
Current Thread : main / this : StandaloneCoroutine{Active}@3ecf72fd :6
Current Thread : main / this : null :main routine ended

위 코드를 분석하기 전에 각각 함수가 하는 역할을 정리하면 다음과 같다.

  • runBlocking : coroutine builder 로서 내부 코르틴이 모두 끝난 다음에 반환된다.
  • launch : coroutine builder로서 , 넘겨받은 코드 블록으로 새로운 코르틴을 생성하고 실행시켜준다.
  • yield : 해당 코르틴이 실행권을 양보하고, 실행 위치를 기억하고, 다음 호출때는 해당 위치부터 다시 실행한다.

위 코르틴에서 1,3,5를 출력하는 코르틴과 2,4,6를 출력하는 코르틴이 서로 실행권을 양보해가면서 실행된다. 한가지 유의할점은 마치 병렬적으로 실행되는 것처럼 보이지만 다른 쓰레드가 아니라 하나의 쓰레드에서 수행된다는 점이다. 따라서 Context Switching 도 발생하지 않는다.

  • Launch coroutine Builder는 Job 객체를 반환한다. Job은 N개 이상의 coroutines의 동작을 제어할 수도 있으며, 하나의 coroutines 동작을 제어할수도 있다.
1
2
3
4
5
6
7
8
9
10
suspend fun main()  = coroutineScope {
// Job 객체는 하나이상의 Coroutine 의 동작을 제어할 수 있다.
val job : Job = launch {
delay(1000L)
println("World!")
}
println("Hello,")
job.join()
println("Done.")
}

Job

코루틴의 Job 객체는 코루틴의 상태를 가지고 있다.

1
2
3
4
5
6
7
8
9
10
11
                                      wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
  • start : job을 실행하고, 호출시 코루틴이 동작중이면 true, 준비 및 완료 상태면 false를 반환된다.
  • join : job을 실행하고, Job의 동작이 완료될떄까지 job을 호출한 코루틴을 일시중단한다.
  • cancel : 현재 코루틴에 종료를 유도하고, 대기하지 않는다.
  • cancelAndJoin : 현재 코루틴을 즉시 종료하라는 신호를 보낸후 대기한다.
  • cancelChildren : 하위 자식 코루틴을 종료시킨다.

Async

  • Async는 사실상 Launch와 같은일을 수행하는데, 차이점은 Launch는 Job객체를 반환하는 반면 , Async는 Deffered를 반환한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public interface Deferred<out T> : Job {

    public suspend fun await(): T

    public val onAwait: SelectClause1<T>

    @ExperimentalCoroutinesApi
    public fun getCompleted(): T

    @ExperimentalCoroutinesApi
    public fun getCompletionExceptionOrNull(): Throwable?
    }

  • Deffered는 Job을 상속한 클래스로서, 타입 파라미터가 있는 제너릭 타입이며, Job과 다르게 await 함수가 정의되어 있다.

  • Deffered의 타입 파라미터는 Deffered 코루틴이 계산 후 돌려주는 값의 타입이다. 즉 Job은 Deffered<Unit>라고 생각할수도 있다.

정리하면 async는 코드 블록을 비동기로 실행 할 수 있고, async가 반환하는 Deffered의 await를 사용해서 코루틴이 결과 값을 내놓을떄까지 기다렸다가 결과값을 받아올 수 있다.

이떄 비동기로 실행할떄 제공되는 코루틴 컨텍스트에 따라 하나의 Thread안에서 제어만 왔다 갔다 할수도 있고, 여러 Thread를 사용할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
fun sumAll(){
runBlocking {
val d1 = async { delay(1000L); 1 }
println("after d1")
val d2 = async { delay(2000L); 2 }
println("after d2")
val d3 = async { delay(3000L); 3 }
println("after d3")
println("1+2+3 = ${d1.await()+d2.await()+d3.await()}")
}
}

실행로그를 보면 다음과 같다.

1
2
3
4
after d1
after d2
after d3
1+2+3 = 6 // 코루틴이 결과값을 내놓을떄까지 기다렸다가 결과값을 받아온다.

만약 위 코드를 직렬화해서 실행하면 최소 6초의 시간이 걸리겠지만, async로 비동기적으로 실행하면 3초가량이 걸리며 더군다나 위 코드는 별개의 thread가 아니라 main thread 단일 thread로 실행되어 이와 같은 성능상 이점을 얻을수 있다.

특히 이와 같은 상황에서 코루틴이 장점을 가지는 부분은 I/O로 인한 장시간 대기 , CPU 코어수가 작아 동시에 병렬적으로 실행 가능한 쓰레드 개수 한정된 상황 이다.

코루틴 컨텍스트

  • Launch , Async 등은 모두 CoroutineScope의 확장함수로 실제로 CoroutineScope는 CoroutineContext 필드를 이런 확장함수 내부에서 사용하기 위한 매개체 역할을 수행한다. 원한다면 launch,aync 확장함수에 CoroutineContext를 넘길수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

// --------------- launch ---------------

/**
* Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job].
* The coroutine is cancelled when the resulting job is [cancelled][Job.cancel].
*
* The coroutine context is inherited from a [CoroutineScope]. Additional context elements can be specified with [context] argument.
* If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.
* The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden
* with a corresponding [context] element.
*
* By default, the coroutine is immediately scheduled for execution.
* Other start options can be specified via `start` parameter. See [CoroutineStart] for details.
**/
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//...
}

// --------------- async ---------------

/**
* Creates a coroutine and returns its future result as an implementation of [Deferred].
* The running coroutine is cancelled when the resulting deferred is [cancelled][Job.cancel].
* The resulting coroutine has a key difference compared with similar primitives in other languages
* and frameworks: it cancels the parent job (or outer scope) on failure to enforce *structured concurrency* paradigm.
* To change that behaviour, supervising parent ([SupervisorJob] or [supervisorScope]) can be used.
*
* Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument.
* If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.
* The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden
* with corresponding [context] element.
*
* By default, the coroutine is immediately scheduled for execution.
* Other options can be specified via `start` parameter. See [CoroutineStart] for details.
*

*/
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
//...
}

그렇다면 CoroutineContext가 하는 역할은 무엇일까?

  • 코루틴이 실행중인 여러 작업과 디스패처를 저장하는 일종의 맵으로 이 CoroutineContext를 사용해 다음에 실행할 작업을 선정하고, 어떻게 Thread에 배정할지에 대한 방법을 결정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun context(){
runBlocking {
launch {
// 부모 컨텍스트를 사용
println("use parent context :${getThreadName()}")
}
launch(Dispatchers.Unconfined) {
// 특정 Thread에 종속되지 않고, Main Thread 를 사용
println("use main thread :${getThreadName()}")
}
launch(Dispatchers.Default){
println("use default dispatcher :${getThreadName()}")
}
launch(newSingleThreadContext("MyOwnThread")){
// 직접 만든 새로운 Thread 사용
println("use thread that i created : ${getThreadName()}")
}
}
}

실행로그를 보면 같은 launch 확장함수를 사용한다고 하더라도 실행되는 CoroutineContext에 따라 다른 Thread상에서 코루틴이 실행됨을 확인할 수 있다.

1
2
3
4
use main thread :main
use default dispatcher :DefaultDispatcher-worker-1
use parent context :main
use thread that i created : MyOwnThread

Coroutine Builder 와 Suspending Function

  • 앞선 Launch , Async , runBlocking , CoroutineScope모두 코루틴 빌더라고 , 새로운 코루틴을 만들어주는 함수이다.

  • delay , yield 함수는 일시중단 함수로 이외에도 다른 일시중단 함수들이 존재한다.

    • withContext: 다른 컨텍스트로 코루틴 전환
    • withTimeOut : 일정 시간내 코루틴이 실행되지 않으면 예외 발생
    • withTimeOutOrNull : 일정 시간내 코루틴이 실행되지 않으면 null 반환
    • awaitAll : 모든 작업의 성공을 대기 , 만약 하나라도 예외 발생시 실패처리
    • joinAll : 모든 작업이 종료될떄까지 현재 작업 대기
Read more

Kotlin 고차 함수안에서 흐름 제어

람다 안에서의 return 문

  • Java와 다르게 Kotlin에서는 람다안에서 return 을 사용하면 람다로부터만 반환되는게 아니라, 그 람다를 호출하는 함수가 실행을 끝내고 반환된다.

  • 이를 Non-Local Return 이라고 부른다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// java
List.of("a","b","c").forEach((item)-> {
if (item.equals("a")){
return;
}
// b,c에 대해서도 실행됨.
});
// kotlin
listOf("a","b","c").forEach {
if (it.equals("a")){
return;
}
println(it)
// a에서 종료됨
}

Non-Local Return 이 적용되는 상황

  • 람다 안의 return 문이 바깥쪽 블록의 함수를 반환시킬 수 있는 상황은 람다를 인자로 받는 함수가 인라인 함수인 경우에만 가능하다. 즉 위의 forEach 함수는 인라인이기에 Non-local return 이 가능한 것이다.

Label을 사용한 Local return

  • 람다식안에서 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 이어서 실행하기 위해서는 Local Return을 사용하면 된다.

  • Non-Local Return 과 구분하기 위해서 Local Return에는 레이블을 추가해야 한다.

1
2
3
4
5
6
7
8
9
10
fun lookForBob(people : List<Person>){
people.forEach label@{
if (it.name == "Bob"){
println("found Bob!")
return@label
}
}
// Local Return을 사용하면 람다가 종료되고 람다 아래의 코드가 실행된다.
println("end of function")
}
  • 또는 인라인 함수의 이름을 label로 사용하여도 위의 코드와 동일하다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fun lookForBob(people : List<Person>){
    people.forEach {
    if (it.name == "Bob"){
    println("found Bob!")
    return@forEach
    }
    }
    println("end of function")
    }

Anonymous Function 을 사용한 깔끔한 Local Return

  • 앞선 Local Return 방식은 레이블을 통해 구현하여, 조건 분기에 따라 여러번 Return 문을 기입해야 할떄는 반환문이 장황해 질 수 있다.

  • Anonymous Function은 (익명,무명함수) 코드 블록을 함수에 넘길때 사용할 수 있는 방법 중에 하나로 일반 함수와 차이점은 함수 이름과 파라미터 타입을 생략 가능하다는 점이다.

1
2
3
4
5
6
7
8
9
fun lookForBob(people : List<Person>){
people.forEach (fun(person){
if (person.name == "Bob"){
println("found Bob!")
return
}
println("end of anonymous function")
})

  • 기본적으로 익명함수도 반환타입을 기입해줘야 하지만, 함수 표현식 (expression body)를 바로 쓰는 경우에는 반환타입 생략이 가능하다.

정리

  • Inline 함수의 경우 람다안의 return문이 바깥쪽 block의 함수를 반환시키는 Non-Local Return을 사용할 수 있다.

  • Anonymous Function을 활용하면 람다를 대체해서 Local Return을 깔끔하게 Label 사용없이 작성 가능하다.

Read more

kotlin에서 null type 다루기

코틀린에서 Null type 허용하는 방법

1
2
3
4
5
fun test() {
val name = "dolly";
name =null;
// compile error
}

변수에 null을 할당 가능하게 하려면 다음과 같이 변수의 타입을 정의할떄, 물음표를 추가하면 된다.

1
2
3
4
5
6
7
8
class Person ( val first: String ,
// middle 변수에 null값이 들어갈수도있음.
val middle: String? ,
val last: String) {

val jkRolling = Person("Joanne",null,"Rowling")
val northWest = Person("North",null,"West")
}

코틀린은 nullable한 String? 이 null값이 아님이 보증이 되면 String type으로 smart cast 해준다.

Read more