[Kotlin in Action] 6장 코틀린 타입 시스템

Sdoubleu·2023년 3월 19일
0

Kotlin in Action

목록 보기
5/9
post-thumbnail

6장에서 다루는 내용

  1. 널이 될 수 있는 타입과 널을 처리하는 구문의 문법
  2. 코틀린 원시 타입 소개와 자바 타입과 코틀린 원시 타입의 관계
  3. 코틀린 컬렉션 소개와 자바 컬렉션과 코틀린 컬렉션의 관계

6.1 널 가능성

  • 널 가능성은 NullPointerException 오류 를 피할 수 있게 돕기 위한 코틀린 타입 시스템 특성

6.1.1 널이 될 수 있는 타입

  • 코틀린 타입 시스템은 널이 될 수 있는 타입을 명시적으로 지원한다
    -> null을 허용

  • 널과 문자열을 인자로 받을 수 있게 하려면 타입 이름 뒤에 물음표(?)를 명시해야 한다

Type? = Type or null

  • 널이 될 수 있는 타입인 변수에 대해 변수.메소드()처럼 직접 호출할 수 없다

  • 널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없다

  • 널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 파라미터를 받는 함수에 전달할 수 없다

6.1.2 타입의 의미

  • 타입은 분류(classification),
    어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 종류를 결정

실행 시점에 널이 될 수 있는/없는 타입의 객체는 같다
널이 될 수 있는 타입은 감싼 래퍼 타입이 아니다
모든 검사는 컴파일 시점에 수행된다

6.1.3 안전한 호출 연산자: ?.

  • ?.
    null 검사와 메소드 호출을 한 번의 연산으로 수행한다

6.1.4 엘비스 연산자: ?:

  • 엘비스 연산자( ?: )
    이항 연산자로 좌항을 계산한 값이 널인지 검사한다
    널이 아니면 좌항 값을, 널이면 우항 값을 결과로 한다

6.1.5 안전한 캐스트: as?

  • as? 연산자
    어떤 값을 지정한 타입으로 캐스트한다
    as?는 값을 대상 타입으로 변환할 수 없으면 null을 반환한다

6.1.6 널 아님 단언: !!

  • 널 아님 단언( !! )
    어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다

6.1.7 let 함수

  • let 함수
    안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 다음에 그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한꺼번에 처리할 수 있다

  • 여러 값이 널인지 검사해야 한다면 let 호출을 중첩시킬 수 있지만 코드가 복잡해지기 때문에 if문을 사용하는 편이 낫다

6.1.8 나중에 초기화할 프로퍼티

  • 코틀린의 클래스 안의 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화할 수는 없다
    -> 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다

  • lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다
    -> 나중에 초기화하는 프로퍼티는 var여야 한다
    -> val은 생성자 안에서 반드시 초기화해야하기 때문

6.1.9 널이 될 수 있는 타입 확장

  • 널이 될 수 있는 타입에 대한 확장 함수를 정의하면 null 값을 다루는 강력한 도구로 활용 가능

  • 어떤 메소드를 호출하기 전에 수신 객체 역할을 하는 변수가 널이 될 수 없다고 보장하는 대신, 직접 변수에 대해 메서드를 호출해도 확장 함수인 메서드가 알아서 널을 처리해준다

  • 널이 될 수 있는 타입에 대한 확장을 정의하면 널이 될 수 있는 값에 대해 그 확장 함수를 호출할 수 있다.
    자바의 경우 메서드 안의 this는 그 메서드가 호출된 수신 객체를 가리키므로 항상 널이 아니다
    그러나 코틀린에서는 널이 될 수 있는 타입의 확장 함수 안에서 this가 널이 될 수 있다

  • let을 사용할 때 수신 객체가 널이 아닌지 검사하고 싶다면
    안전한 호출 연산인 ?. 을 사용해야 한다

6.1.10 타입 파라미터의 널 가능성

  • 코틀린에서는 함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다

  • 널이 될 수 있는 타입을 포함하는 어떤 타입이라도 타입 파라미터를 대신할 수 있다
    -> 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다

// 널이 될 수 있는 타입 파라미터 다루기
fun <T> printHashCode(t: T) {
	println(t?.hashCode()) // "t"가 null이 될 수 있으므로 안전한 호출 써야함
}
>>> printHashCode(null) // "T"의 타입은 "Any?"로 추론된다
null

// 타입 파라미터에 대해 널이 될 수 없는 상한을 사용하기
fun <T: Any> printHashCode(t: T) { // 이제 "T"는 널이 될 수 없는 타입
	println(t.hashCode())
}
>>> printHashCode(null) // 널이 될 수 없는 타입의 파라미터에 널을 넘길 수 없다
Error: Type parameter bound for `T` is not satisfied
>>> printHashCode(42)
42

6.1.11 널 가능성과 자바


6.2 코틀린의 원시 타입

  • Int, Boolean, Any 등의 원시 타입에 대해 살펴본다

  • 코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다

6.2.1 원시 타입: Int, Boolean 등

  • 원시 타입의 변수(int 등)에는 그 값이 직접 들어간다

  • 참조 타입의 변수(String 등)에는 메모리상의 객체 위치가 들어간다

  • 자바는 참조 타입이 필요한 경우 특별한 래퍼 타입으로 원시 타입 값을 감싸서 사용한다

  • 코틀린은 원시 타입과 래퍼 타입을 구분하지 않으므로 항상 같은 타입을 사용한다

즉, 자바처럼 Integer와 int로 구분되지 않고 Int 하나만 존재한다
코틀린의 타입은 컴파일 시 자바의 primitive 또는 wrapper 타입으로 자동 변환된다

6.2.2 널이 될 수 있는 타입: Int?, Boolean? 등

  • null 참조를 자바의 참조 타입의 변수에만 대입할 수 있기 떄문에 널이 될 수 있는 코틀린 타입은 자바 원시 타입으로 표현할 수없다
    -> 코틀린에서 널이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일된다
/* 코틀린 null이 될 수 있는 Int? 타입 */
val price: Int? = null

/* 자바로 변환된 형태 */
Integer price = (Integer)null;

6.2.3 숫자 변환

  • 코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다
    -> 결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우조차도 자동 변환은 불가능하다
    -> 직접 변환 메소드를 호출해야 한다

  • 코드에서 동시에 여러 숫자 타입을 사용하려면 예상치 못한 동작을 피하기 위해 각 변수를 명시적으로 변환해야 한다

  • 숫자 리터럴을 사용할 때는 보통 변환 함수를 호출할 필요가 없다
    ex) 42L or 42.0f

6.2.4 Any, Any?: 최상위 타입

  • 코틀린에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상 타입이다

  • 원시 타입 값을 Any 타입의 변수에 대입하면 자동으로 값을 객체로 감싼다

val answer: Any = 42 <- Any가 참조 타입이기 때문에 42가 박싱된다

박싱이란 원시타입->래퍼타입으로 변환하는 것,
언박싱이란 래퍼타입->원시타입으로 변환하는 것을 의미

  • Any가 널이 될 수 없음에 유의
    -> 널 포함 모든 값을 대입할 변수를 선언하려면 Any? 타입 사용

6.2.5 Unit 타입: 코틀린의 void

  • 코틀린 Unit 타입은 자바 void와 같은 기능을 한다

  • 반환 타입 선언 없이 정의한 블록이 본문인 함수와 같다

fun f(): Unit { }
fun f() { }
  • Unit과 void의 차이점은
    • Unit은 모든 기능을 갖는 일반적인 타입, void와 달리 타입 인자로 쓸 수 있다
    • Unit 타입에 속한 값은 단 하나뿐이며, 이름도 Unit이다
    • Unit 타입의 함수는 Unit 값을 묵시적으로 반환한다
  • 제네릭 파라미터를 반환하는 함수를 오버라이드하면서 반환 타입으로 Unit을 쓸 때 유용하다
interface Processor<T> {
	fun process(): T
}

class NoResultProcessor : Processor<Unit> { // Unit을 반환하지만 타입을 지정할 필요는 없다.
	override fun process() {
	// do stuff
	} //여기서 따로 return을 명시할 필요가 없다.
}

------------------ Unit의 쉬운 이해 ---------------------

/* Unit 반환 타입 명시 */
fun init() : Unit { ... }

/* Unit 반환 타입 생략 */
fun init() { ... }

/* Unit 타입을 인자로 사용 */
class NoResultProcessor : Processor<Unit> {
   override fun process() { //Unit을 반환하지만 반환 타입을 생략해도 됌
      ...
   }
}

6.2.6 Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다

  • 코틀린에는 반환 값이라는 개념 자체가 의미 없는 함수가 일부 존재

  • Nothing 타입은 아무 값도 포함하지 않는다

  • Nothing은 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다
    -> 그 외의 다른 용도로 사용하는 경우 Nothing 타입의 변수를 선언하더라도 그 변수에 아무 값도 저장할 수 없으므로 의미 없다

Nothing type은 이 함수가 정상적으로 끝나지 않는다는 걸 명시적으로 표현하는 타입
예를 들면,
함수 중간에 throw 시키도록 한다면 해당 함수는
정상적으로 끝나지 않는 함수가 되겠죠?
이런 함수의 반환 type을 Nothing으로 하면
컴파일러가 정상 종료되지 않는 함수임을 미리 알 수가 있습니다
Nothing 함수는 엘비스 연산자의 우항에 사용해서 전제 조건을 검사하는 데에 주로 사용됩니다.


6.3 컬렉션과 배열

6.3.1 널 가능성과 컬렉션

  • 타입 인자로 쓰인 타입에도 ?를 붙이면 널을 저장할 수 있다

  • 경우에 따라 널이 될 수 있는 값으로 이뤄진 널이 될 수 있는 리스트를 정의해야 할 수도 있다
    -> List<Int?>?로 표현
    -> 이런 리스트를 처리할 때는 변수에 대해 널 검사 수행 후 리스트에 속한 모든 원소에 대해 널 검사 수행해야 한다

  • 널이 될 수 있는 값으로 이뤄진 컬렉션으로 널 값을 걸러낼 때
    -> filterNotNull 함수 사용
    -> 컬렉션 안에 널이 들어있지 않음을 보장해주므로
    List<Int>으로 사용 가능

6.3.2 읽기 전용과 변경 가능한 컬렉션

  • 가능한 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼자

  • 읽기 전용 컬렉션이 항상 스레드 안전 하지는 않다

6.3.3 코틀린 컬렉션과 자바

  • 변경 가능한 인터페이스는 자신과 대응하는 읽기 전용 인터페이스를 확장(상속)한다

컬렉션 타입읽기 전용 타입변경 가능 타입
ListlistOfmutableListOf, arrayListOf
SetsetOfmutablesetOf, hashSetOf, linkedSetOf, sortedSetOf
ListmapOfmutableMapOf, hashMapOf, linkedMapOf, sortedMapOf
  • 코틀린 컴파일러는 자바 코드가 컬렉션에 대해 어떤 일을 하는지 완전히 분석할 수 없다

6.3.4 컬렉션을 플랫폼 타입으로 다루기

6.3.5 객체의 배열과 원시 타입의 배열

  • 코틀린 배열은 타입 파라미터를 받는 클래스
    -> 배열의 원소 타입은 타입 파라미터에 의해 정해진다

코틀린에서 배열을 만드는 방법

  • arrayOf 함수에 원소를 넘기면 배열을 만들 수 있다
  • arrayOfNulls 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다
    물론 원소 타입이 널이 될 수 있는 타입인 경우에만 이 함수를 쓸 수 있다
  • Array 생성자는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화해준다
    arrayOf를 쓰지 않고 각 원소가 널이 아닌 배열을 만들어야 하는 경우 이 생성자를 사용한다
  • toTypedArray
    데이터가 이미 컬렉션에 들어 있다면 컬렉션을 배열로 변환시켜준다
// 컬렉션을 vararg 메소드에게 넘기기
>>> val strings = listOf("a", "b", "c")

// vararg 인자를 넘기기 위해 스프레드 연사자를 사용
>>> println("$s/$s/$s".format(*strings.toTypedArray())
a/b/c
  • 다른 제네릭 타입에서처럼 배열 타입의 타입 인자도 항상 객체 타입이 된다

  • 코틀린은 원시 타입의 배열을 표현하는 별도 클래스(IntArray , ByteArray , CharArray , BooleanArray등)를 각 원시 타입마다 하나씩 제공한다

원시 타입의 배열을 만드는 방법

  • 각 배열 타입의 생성자는 size 인자를 받아서 해당 원시 타입의 디폴트 값(보통은 0)으로 초기화된 size 크기의 배열을 반환한다
  • 팩토리 함수는 여러 값을 가변 인자로 받아서 그런 값이 들어간 배열을 반환한다
  • 크기와 람다를 인자로 받는 생성자를 사용한다

📌참고자료

원시 타입, Unit 등등의 설명

profile
개발자희망자

0개의 댓글