Nullable, Nonnull, Kotlin scope function

유진·2024년 1월 21일
0
post-custom-banner

Nullable, Nonnull, Kotlin scope function

💫 유진 코틀린 공식문서 첫 영접..! - 코틀린 공식문서와 gpt의 영어 번역 도움을 받았습니다.

Nullable & Nonnull → Null Safety

Null safety | Kotlin

  • Kotlin에서 NPE가 발생할 수 있는 유일한 경우
  1. 명시적으로 throw NullPointerException()를 호출하는 경우.
  2. !! 연산자를 사용하는 경우 (아래에서 설명).
    null이 아니라고 단언한 경우임 → 근데 Null이 들어갔으니 에러를 발생하는 것이 당연
  3. 초기화와 관련된 데이터 무결성 문제:
    - 생성자에서 사용 가능한 초기화되지 않은 **this**가 어딘가에 전달 및 사용되는 경우 ("leaking this").
    - 슈퍼클래스 생성자가 파생된 클래스에서 초기화되지 않은 상태를 사용하는 경우.
  4. Java와의 상호 운용성:
    - 플랫폼 유형의 null 참조의 멤버에 액세스하려고 시도하는 경우.
        Kotlin은 "플랫폼 유형(platform types)"이라는 개념을 도입하여 Java와의 상호 운용성을 지원합니다. 플랫폼 유형은 null 안전성에 대한 정보가 충분하지 않은 경우에 사용됩니다. 즉, Kotlin에서 정확히 Nullable 또는 Non-Nullable 여부를 결정할 수 없는 상황에서 발생합니다. 따라서 Kotlin에서 플랫폼 유형에 액세스하면 컴파일러는 null 안전성 검사를 적용하지 않습니다. 이로 인해 NPE가 발생할 수 있으므로 주의해야 합니다.
        
        플랫폼 유형에 대한 작업을 수행할 때는 주의해야 합니다. 주로 Java에서 가져온 코드와 상호 작용하는 경우 발생할 수 있습니다. 이런 경우에는 명시적으로 Nullable 여부를 확인하고 안전하게 처리해야 합니다.
        
    - Java 상호 운용성을 위해 사용되는 제네릭 타입의 Nullable 문제. 예를 들어, Java 코드에서 Kotlin MutableList<String>에 null을 추가하면 Kotlin에서는 MutableList<String?>이 필요할 수 있습니다.
        
        Kotlin의 제네릭 타입과 Java의 제네릭 타입 간의 차이 때문에 이러한 문제가 발생할 수 있습니다. Kotlin에서의 제네릭은 기본적으로 Non-Nullable이며, Java에서의 제네릭은 Nullable이 될 수 있습니다. 따라서 Kotlin에서 Java로부터 제네릭 타입을 사용할 때, Kotlin 컴파일러는 해당 타입을 Nullable로 처리할 수 있습니다.
        
        예를 들어, Java에서 MutableList<String>에 null 값을 추가하면 Kotlin에서는 MutableList<String?>이 필요할 수 있습니다. 이 경우, Kotlin에서는 MutableList에 String이 아닌 Nullable String을 추가할 수 있는 유연성이 필요하므로 Nullable 제네릭 타입을 사용해야 합니다.
        
        이러한 상황에서는 주로 컴파일러의 경고나 에러 메시지를 주의 깊게 확인하고, 필요한 경우 명시적으로 Nullable 또는 Non-Nullable을 지정하여 상호 운용성을 관리해야 합니다. 예를 들어, Kotlin에서 Java로 Java로부터 제네릭 타입을 전달하는 경우 **`as`** 연산자를 사용하여 Nullable 또는 Non-Nullable을 명시적으로 지정할 수 있습니다.
        
        ```kotlin
        val javaList: MutableList<String> = ArrayList<String>()
        val kotlinList: MutableList<String?> = javaList as MutableList<String?>
        ```
        
        이렇게 상호 운용성 문제를 다루면 Kotlin과 Java 코드가 함께 작동하고 NullPointer 예외를 방지할 수 있습니다. 하지만 주의가 필요하며, 코드를 안전하게 관리하기 위해 명시적인 Nullable 및 Non-Nullable 타입 지정을 사용해야 합니다.
        
  5. 외부 Java 코드에서 발생하는 기타 문제.

1. Nullable types and non-nullable types (널이 될 수 있는 타입과 널이 될 수 없는 타입):

Kotlin은 변수의 타입에 따라 널이 될 수 있는지 여부를 구분합니다. 

예를 들어, **`var a: String`**은 널이 될 수 없는 타입으로 선언되어 있으며, **`a = null`**과 같은 시도는 컴파일 오류를 발생시킵니다. 

```kotlin
var a: String = "abc" // 기본적인 초기화는 기본적으로 null을 허용하지 않음
a = null // 컴파일 오류
```

반면, **`var b: String?`**은 널이 될 수 있는 타입으로 선언되어 있어, **`b = null`**을 허용합니다.

```kotlin
var b: String? = "abc" // null로 설정 가능
b = null // 허용
```

**`a`**와 같은 변수에서 메소드를 호출하거나 속성에 액세스하면 NPE를 발생시키지 않으므로 다음과 같이 안전하게 사용할 수 있습니다:

```kotlin
val l = a.length
```

그러나 **`b`**와 같은 변수의 동일한 속성에 액세스하려면 안전하지 않으며 컴파일러는 오류를 보고합니다:

```kotlin
val l = b.length // 오류: 변수 'b'는 null일 수 있음
```

2. Safe calls (안전한 호출):

널이 될 수 있는 변수에 대한 안전한 접근은 **`?.`** 연산자를 사용하여 수행합니다. 이것은 해당 변수가 널이 아닌 경우에만 속성 또는 메서드를 호출하고, 널인 경우에는 **`null`**을 반환합니다.

```kotlin
var timestamp: Instant? = null
val isoTimestamp = timestamp?.toString() // Returns a String? object which is `null`
if (isoTimestamp == null) {
   // Handle the case where timestamp was `null`
}
```

3. Elvis operator (엘비스 연산자):

Elvis 연산자 **`?:`** 를 사용하여 "만약 변수가 널이 아니라면 그 값을 사용하고, 널이라면 다른 값을 사용하라"와 같은 조건을 간단하게 표현할 수 있습니다.

```kotlin
val l = b?.length ?: -1
```

```kotlin
fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    // ...
}
```

4. The !! operator (널 단언 연산자):

**`!!`** 연산자를 사용하면 널이 될 수 있는 변수를 널이 아닌 타입으로 강제로 변환할 수 있지만, 변수가 널인 경우 NPE (NullPointerException) 예외를 발생시킵니다. 이것은 널에 대한 명시적인 처리를 요구합니다.

예를 들어, **`b!!`**를 사용하면 **`b`**의 non-null 값 (예: 위의 예에서 **`String`**)을 반환하거나 **`b`**가 null인 경우 NPE를 throw합니다.

```kotlin
val l = b!!.length
```

기본적으로 Kotlin은 null 안전성을 제공하여 NPE를 방지하도록 설계되었으며, 가능한한 **`!!`** 연산자를 사용하는 것은 피해야 합니다. 왜냐하면 이 연산자는 NPE를 명시적으로 요청해야 하며, 예외가 예상치 못한 곳에서 발생하지 않도록 합니다.

5. Safe casts (안전한 캐스팅):

**`as?`** 연산자를 사용하면 타입 변환 시, 대상 타입으로 변환이 실패하면 **`null`**을 반환하게 됩니다.

또한 안전한 캐스트를 사용할 수 있으며, 이러한 캐스트는 대상 타입이 아닌 경우 null을 반환합니다. 아래는 예시입니다:

```kotlin
val a Int: Int? = a as? Int
```

6. 필터링

또한 nullable 타입의 원소로 이뤄진 컬렉션에서 non-nullable 원소를 필터링하려면 **`filterNotNull`**를 사용할 수 있습니다. 아래는 예시입니다:

```kotlin
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
```

Kotlin scope function

Scope functions | Kotlin

Kotlin 표준 라이브러리에는 객체의 컨텍스트 내에서 코드 블록을 실행하는 것이 목적인 여러 함수가 포함되어 있습니다. 이러한 함수를 객체에 람다 표현식을 제공하여 호출하면 임시 범위가 형성됩니다. 이 범위 내에서 객체에 대한 이름없이 접근할 수 있습니다. 이러한 함수는 범위 함수라고 불립니다. 다섯 가지 범위 함수가 있으며, 이들은 let, run, with, apply 및 also입니다.

이러한 함수들은 기본적으로 모두 동일한 작업을 수행합니다. 차이점은 이 객체가 블록 내에서 어떻게 사용 가능해지며 전체 표현식의 결과가 무엇인가입니다.

다음은 범위 함수를 사용하는 전형적인 예제입니다:

Person("Alice", 20, "Amsterdam").let {
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

let을 사용하지 않고 동일한 코드를 작성하면 새 변수를 도입하고 사용할 때마다 해당 이름을 반복해서 사용해야 합니다.

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

범위 함수는 새로운 기술적 기능을 도입하지 않지만 코드를 더 간결하고 가독성있게 만들 수 있습니다.

범위 함수 간에 많은 유사점이 있기 때문에 사용 사례에 따라 적절한 함수를 선택하는 것은 까다로울 수 있습니다. 선택은 주로 의도와 프로젝트 내에서의 일관성에 따라 달라집니다. 아래에서 각 범위 함수의 차이점에 대한 자세한 설명을 제공합니다.

함수 선택
사용 목적에 따라 적절한 범위 함수를 선택하는데 도움이 되도록 다음 표를 제공합니다.

함수객체 참조반환 값확장 함수 여부
letit람다 결과
runthis람다 결과
run-람다 결과아니오 called without the context object
withthis람다 결과아니오 takes the context object as an argument.
applythis컨텍스트 객체
alsoit컨텍스트 객체
  • let: 람다로 실행할 경우 람다 결과 반환. 객체의 일련의 작업을 실행할 수 있음. run: 람다로 실행할 경우 람다 결과 반환. 객체의 함수를 호출하거나 속성에 값을 할당하는 작업을 주로 하는 람다에서 사용. run: 람다로 실행할 경우 람다 결과 반환하지 않음. 컨텍스트 객체를 인자로 사용. with: 람다로 실행할 경우 람다 결과 반환하지 않음. 컨텍스트 객체를 람다 인자로 사용. apply: 컨텍스트 객체 반환. 객체의 멤버에 대한 주요 작업을 하는 코드 블록에서 사용. also: 컨텍스트 객체 반환. 객체의 속성과 함수 대신 객체에 대한 참조가 필요한 작업 또는 바깥 범위에서 this 참조를 가리키지 않을 때 사용. 각 범위 함수에는 반환 값과 객체가 어떻게 참조되는 방법의 차이가 있으며, 사용할 때 이를 고려해야 합니다.

차이점

범위 함수는 본질적으로 유사하므로 그들 사이의 차이점을 이해하는 것이 중요합니다. 각 범위 함수 간의 주요 차이점은 다음 두 가지입니다.

  • 컨텍스트 객체를 참조하는 방식
  • 반환 값

컨텍스트 객체: this 또는 it

범위 함수의 람다 내에서 컨텍스트 객체는 실제 이름 대신 짧은 참조로 사용할 수 있습니다. 각 범위 함수는 컨텍스트 객체를 참조하는 두 가지 방법 중 하나를 사용합니다: 람다 수신자(this) 또는 람다 인수(it). 둘 다 동일한 기능을 제공하므로 각각 다른 사용 사례에 대한 장단점을 설명하고 사용에 대한 권장 사항을 제공합니다.

this:

run, with 및 apply는 컨텍스트 객체를 람다 수신자(this)로 참조합니다. 따라서 람다 내에서 객체는 일반적인 클래스 함수에서와 같이 사용할 수 있습니다. 대부분의 경우, 수신자 멤버에 액세스할 때 this를 생략할 수 있어 코드를 짧게 만들 수 있습니다. 그러나 this를 생략하면 수신자 멤버와 외부 객체나 함수 간의 구분이 어려워질 수 있으므로 주로 객체의 멤버에 대한 함수 호출이나 속성에 값 할당을 주요 작업으로 하는 람다에서 컨텍스트 객체를 수신자로 사용하는 것이 좋습니다.

this를 사용한 예시:

val adam = Person("Adam").apply {
    age = 20                       // this.age = 20과 동일
    city = "London"
}

it:

반면에 let 및 also는 람다 인수(it)로 컨텍스트 객체를 참조합니다. 인수 이름이 지정되지 않으면 기본 이름 it으로 객체에 액세스됩니다. it는 this보다 짧으며 it를 사용한 표현식은 보통 더 읽기 쉽습니다. 그러나 객체의 함수나 속성을 호출할 때 this와 달리 람다 내에서 객체를 암묵적으로 사용할 수 없습니다. 따라서 객체가 주로 함수 호출 인수로 사용되는 람다에서 it를 사용하는 것이 더 좋습니다. 코드 블록에서 여러 변수를 사용하는 경우에도 it을 사용하는 것이 더 효율적입니다.

it을 사용한 예시

fun getRandomInt(): Int {
    return Random.nextInt(100).also { it ->
        writeToLog("getRandomInt() generated value $it")
    }
}

또한 it을 인수로 사용할 때 인수 이름을 지정하여 사용할 수도 있습니다.

fun getRandomInt(): Int {
    return Random.nextInt(100).also { value ->
        writeToLog("getRandomInt() generated value $value")
    }
}

반환 값

범위 함수는 반환 값에 따라 다릅니다.

  • let, run 및 with는 람다 결과를 반환합니다. 람다 결과: let, run 및 with는 람다 결과를 반환합니다. 따라서 결과를 변수에 할당하거나 결과에 대한 연산을 연결하고 싶을 때 사용할 수 있습니다.
  • apply와 also는 컨텍스트 객체를 반환합니다. 컨텍스트 객체: apply와 also의 반환 값은 컨텍스트 객체 자체입니다. 따라서 동일한 객체에서 계속해서 함수 호출을 연결할 수 있으며 연속적으로 호출 체인을 포함하는 복잡한 처리에도 사용할 수 있습니다.

코드에서 무엇을 하려는지를 고려하여 반환 값을 신중하게 선택해야 합니다. 이려면 코드에서 다음 단계를 무엇을 하려고 하는지 고려하십시오. 이것은 어떤 범위 함수를 사용할지 선택하는 데 도움이 됩니다.

또한 반환 값을 무시하고 범위 함수를 사용하여 지역 변수에 대한 한정된 범위를 만들어 코드를 더 읽기 쉽게 작성할 수도 있습니다.

참고: 범위 함수를 남용하지 않는 것이 좋습니다. 범위 함수를 과도하게 사용하면 코드가 읽기 어려워지고 오류가 발생할 수 있습니다. 또한 범위 함수를 중첩하거나 연결할 때 현재 컨텍스트 객체와 this 또는 it의 값에 혼동이 생길 수 있으므로 주의해야 합니다.


추가 지식

범위 함수 내에서 pravite 함수나 변수에 대한 접근은 안된다!

let→ null이 아닐때

이름이 가지고 있는 의미를 좀 느껴보셈

profile
안드로이드 학생 개발자 에디 / 유진입니다
post-custom-banner

0개의 댓글