Action Kotlin 5 - 코틀린 타입 시스템

Null

null

null이 될 수 있는 타입 뒤에는 ?를 붙이면 null참조를 저장할 수 있다.

?.

Kotlin에서 제공하는 안전한 호출 연산자이다.

일반적인 Null 체크는 if문을 통해 체크할 수 있지만, ?.를 이용한다면, Null검사와 메서드 호출을 동시에 할 수 있다.

/// 두 코드는 동일하다.
s?.toUpperCase()
if(s!=null) s.toUpperCase() else null

또한 null이 될 수 있는 프로퍼티 접근 시 안전한 호출도 가능하다.

?:

?:null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있다.

사용 방법은 다음과 같다.

fun foo(s : String?){
    val t : String = s ?: ""
}

snull이라면, t""을 저장한다는 의미이다.

Kotlin에서는 returnthrow이기 때문에, 엘비스 연산자 우항에 해당 식을 넣어줄 수도 있다.

안전한 캐스트 as?

kotlin에서 타입 캐스팅을 하는 방법은 as를 이용하는 것이다.

만약, as를 이용한 타입 캐스팅시 지정한 타입으로 바꿀 수 없는 경우라면, ClassCastException을 반환하게 된다.

이를 방지하기 위해 is를 이용해 미리 변환 가능한지 체크할 수 있지만, 매 방법마다 is를 통해 체크하는 것은 번거로울 수 있다.

이를 해결하기 위해 as?를 사용한다.
as?는 어떤 값을 지정한 타입으로 캐스팅한다. as?는 대상 타입으로 변환할 수 없다면, null을 반환한다.

널이 아님을 선언 !!

!!은 어떤 값이든 null이 될 수 없는 타입으로 강제로 바꿀 수 있다.
만약, null이 될 수 있는 값에 !!를 사용하게 되면, NPE가 발생한다.

!!은 여러 단언문을 한 줄에 쓰는 것을 피하는 것이 좋다.

let

let을 이용하면, null이 될 수 있는 식을 더 잘 처리할 수 있다.

만약에 어떤 값이 null이 아니라면, 어떠한 작업을 수행한다고 가정하면,
값?.let{}다음과 같이 작성하여 null을 체크해주면 된다.

let은 자신의 수신 객체를 lambda에 넘기므로, 안전한 호출을 사용할 수 있게 해준다.

예를들어 email의 주소값이 null이 아니면 mail을 보내도록 하는 코드를 작성하면,

email?.let{sendEmail(it)}

다음과 같이 작성할 수 있다.

let을 이용한 체크를 해야만 하는 걸까?

정답은 아니다! 복잡한 로직이나 여러 조건을 동시에 확인해야 하는 경우에는 if를 사용하는 것이 더 좋다

lateinit

lateinit은 나중에 초기화 한다는 뜻이다.

lateinit의 프로퍼티는 반드시 var이어야 한다. valfinal 필드이므로, 컴파일 단계에서 반드시 초기화되야 한다. 그렇기에 나중에 초기화하는 프로미터는 var을 사용해야 한다.

DI와 함께 사용하는 경우가 많다.
Hilt를 사용할 때, @Inject과 함께 의존성을 주입할 때 lateinit으로 작성한다.
나중에 선언하는 것처럼 선언되어 있지만, Hilt에 의해 의존성이 주입된다.

타입 파라미터의 Null 가능성

kotlin에서 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 ?가 없더라도 null이 될 수 있는 타입이다.

null이 아님을 확실히 하고 싶다면?
타입 상한 을 해야한다!

타입 상한

타입 상한이란, 특정 타입이나, 서브 타입만을 허용하도록 하는 기능이다.

예를 들어

class Box<T: Number>(val value: T)

위의 코드처럼 TNumber로 제한해서 Number서브 타입만이 허용되도록 제한 하는 것이다. Number의 서브타입은, - Int Double Float Long Short Byte가 있으므로, 해당 서브 타입만 허용하도록 한다.

val intBox: Box<Int> = Box(10) // 정상 작동 
val doubleBox: Box<Double> = Box(10.5) // 정상 작동 
val stringBox: Box<String> = Box("Hello") // 컴파일 에러

3개의 상황을 예시로 들면, IntDouble의 경우에는 서브 타입이므로 문제가 없다.
하지만, String의 경우에는 서브 타입이 아니므로, 컴파일 에러가 발생한다.

그렇다면, 왜 타입 상한을 쓰는 것일까?

장점은 다음과 같다.(with. GPT4)

  1. 타입 안전성(Type Safety): 제네릭 타입의 범위를 제한함으로써, 런타임에 발생할 수 있는 타입 관련 오류를 컴파일 시점에 잡아낼 수 있다.

  2. 코드의 명확성: 타입 상한을 사용함으로써 개발자는 해당 제네릭 클래스나 함수가 어떤 타입의 값을 받을 수 있는지 명시적으로 표현할 수 있으며, 이는 코드의 가독성과 유지보수성을 높여준다.

  3. 추가 기능의 사용: 타입 상한을 통해 특정 타입의 메서드나 프로퍼티에 접근할 수 있게 된다. 예를 들어, TNumber 타입으로 제한되어 있을 때, T 타입의 변수에서 Number 클래스의 메서드와 프로퍼티를 사용할 수 있다.

코틀린의 원시타입

java에서의 원시타입과 참조타입에 대해 설명해보겠다.

원시타입

  • 원시타입은 Int, Boolean 등이 있다.
  • 원시타입은 메모리에 값이 직접 저장이 된다.

    참조 타입

  • 참조타입은 메모리상에 객체의 주소를 저장한다.

두 특징을 경험과 함께 생각해보면, 코딩을 배울 때 이런 경험을 해본적이 다들 있을 것 같다.

예를 들어 원시타입의 Int는 값을 다른 변수에 할당해도 서로에게 영향을 주지 않는다. 하지만 참조 타입을 변수에 할당 하고 할당한 변수값을 수정하게 되면 두 변수 모두 수정된 경험이 있을 것이다.

참조 타입은 주소를 저장하기 때문에, 변수에 할당하면 주소값이 할당되기 때문이다.

java에서는 컬렉션에 원시타입을 담을 수 없다.
Collection<Int>는 불가능하다. java는 참조타입이 필요한 경우에 특별한 래퍼타입으로 원시 타입을 감싸야 한다.

그렇기에 Collection<Integer>를 사용해야 한다.

하지만, Kotlin에서는 원시 타입과 래퍼 타입을 구분하지 않는다. 그렇기에 항상 같은 타입을 사용한다.

그렇다면 코틀린에서는 모든 타입을 객체로 표현하는 것인가?

정답은 아니다. 코틀린은 실행 시점에서 가장 효율적인 방식으로 표현한다.

Int의 경우에는 Null이 될 수 없다, 하지만 Kotlin에서는 IntNull로 사용할 수 있다, 코틀린에서는 Int로 저장하는 것이 아닌, Integer로 효율적으로 저장하기 때문에 가능하다.

숫자 변환

Kotlin에서는 숫자를 변환할 때, 다른 타입의 숫자를 다른 타입에 저장할 때 자동으로 타입이 변환되지 않는다.

직접 변환 메서드를 호출해야 가능하다.

Any, Any? : 최상위 타입

java에서는 object가 최상위 타입이지만, kotlin에서는 Any타입이 모든 널이 될 수 없는 타입의 조상이다.

java에서는 원시타입은 object를 정점으로하는 계층에 없다. 그렇기에, 사용하려면 integer와 같은 래퍼타입으로 감싸주어야 가능하다.

Anynull이 올 수 없다. 그렇기에 null을 허용하려면 Any?를 사용해야한다.