코틀린학습 - #5 타입 시스템

2020. 11. 17. 13:04개발&TIL/kotlin

코틀린의 타입 시스템은 코드의 가독성을 향상시키는데 도움이 되는 몇가지 특성을 제공

널이 될수 있는 타입

읽기 전용 컬렉션

널 가능성

널이 인자로 들어올 수 없다면 다음과 같은 함수 정의 가능

fun strLen(s: String) = s.length

strLen() 함수는 String 인스턴스여야 하므로 결코 실행시점에 NullPointException이 발생하지 않는다.
이 함수가 nul과 문자열을 인자로 받을 수 있게 하려면 타입 이름 뒤에 물음표(?)를 명시해야 한다.
모든 타입은 기본적으로 널이 될수 없는 타입이다.

Type? = Type 또는 null
  • if 검사를 통해 null 값 다루기
fun strLenSafe(s: String?) : Int =
    if (s != null) s.length else 0

val x: String? = null  
println(strLenSafe(x))  
//0  
println(strLenSafe("abc"))  
//3

안전한 호출 연산자: ?.

안전한 호출 연산자는 널이 아닌 값에 대해서만 메소드를 호출한다.

s?.toUpperCase() == if (s != null) s.toUpperCase() esle null 과 같다.

메소드 호출뿐 아니라 프로퍼티를 읽거나 쓸 때도 안전한 호출을 사용할 수 있다.

class Employee(val name: String, val manager: Employee?)  
fun managerName(employee: Employee): String? = employee.manager.?.name

val ceo = Employee("Da boss", null)  
val developer = Employee("Bob Smith", ceo)

println(deveploter)  
println(ceo)

엘비스 연산자: ?:

null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 엘비스 연산자라고 한다.

fun foo(s: String?) {  
val t: String = s ?: "" //"s"가 널이면 결과는 빈 문자열이다.  
}

코틀린에서는 return이나 throw 등의 연산도 식이다. 따라서 엘비스 연산자 우항에 넣을 수 있다.

address = person.company?.address ?: throw IllegalArgumentException("No address")

안전한 캐스트: as?

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

                foo가 타입으로 캐스팅이 되면       foo as Type
foo as? Type
                foo가 타입으로 캐스팅이 불가하면     null

널 아님 단언: !!

어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다.

  • 널 아님 단언 사용하기
fun igonreNulls(s: String?) {  
    val sNotNull: String = s!! //예외가 아님을 확신하며 예외가 발생하면 이지점을 가르킨다.  
    println(sNotNull.length)  
}

igonreNulls(null)

//Exception in thread "main" kotlin.KotlinNullPointerException  
// at kotlininaction.test.code.Chapter06Kt$main$2.invoke(Chapter06.kt:20)

!! 단언문을 한 줄에 함께 쓰게 되면 어떤 값이 널인지 어떤 식의 문제인지 확인이 어렵다
단언문은 줄을 나눠서 사용하라

person.company!!.address!!.country // 이런식이면 어떤 함수, 값이 문제인지 확인이 어렵다.

let 함수

let을 사용하는 가장 흔한 용례는 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우다.

email?.let { email -> sendEmailtTo(email) }
  • let을 사용해 null이 아닌 인자로 함수 호출하기
fun sendEmailTo(email: String) {
    println("Sending email to $email")
}
var email: String? = "yole@example.com"
email?.let { sendEmailTo(it) }
//Sending email to yole@example.com
email = null
email?.let { sendEmailTo(it) }
  • if 검사를 let 함수로
val person: Person? = getTheBestPersonInTheWorld()
if (person != null) sendEmailTo(person.name)

getTheBestPersonInTheWorld()?.let { sendEmailTo(it.name) }

여러 값이 null인지 검사할때는 let 함수를 종첩시키기 보다는 복잡하지 않게 if를 사용하는 편이 낫다.

나중에 초기화할 프로퍼티

코틀린에서는 일반적으로 생성자에서 모든 프로터티를 초기화해야 한다.

lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화 할 수 있다.

나중에 초기화 하는 프로퍼티는 항상 var여야 한다.

  • 나중에 초기화하는 프로퍼티 사용하기
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private lateinit var myService: MyService //초기화하지 않고 널이 될 수 없는 프로퍼티 선언

    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        Assert.assertEquasl("foo", myService.performAction())
    }      

}

lateinit 프로퍼티를 의존관계 주입 프로임워크에서 함께 사용하는 경우가 많다.

liteinit 프로퍼티의 값을 DI 프레임워크가 외부에서 설정해준다.

널이 될 수 있는 타입 확장

객체의 동적 타입에 따라 적절한 메소드를 호출해주는 방식을 동적 디스패치 라 부른다.

반대로 컴파일러가 컴파일 시점에 어떤 메소드가 호출될지 결정해서 코드를 생성하는 방식을 직접 디스패치 라고 한다.

  • String? 타입의 수신 객체에 대해 호출할 수 있는 isNullOrEmpty(), isNullOrBlank() 메소드
fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) {
        println("Please fill in the required fields")
    }
}

verifyUserInput("");
//Please fill in the required fields
verifyUserInput(null);
//Please fill in the required fields

자바에서는 메소드안의 this는 그메소드가 호출된 수신 객체를 가리키므로 항상 null이 아니다.

코틀린에서는 널이 될 수 있는 타압의 확장 함수 안에서는 this가 null이 될 수 있다.

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

val person: Person? = Person("Alice", 3)
person.let { sendEmailTo(it.name) } //it이 null이 될수 있다.
person?.let { sendEmailTo(it.name) } //안전한 호출 사용

타입 파라미터의 널 가능성

함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다.

클래스나 함수안에서 타입 이름으로 사용하면 물음표가 없더라도 T가 널이 될 수 있는 타입니다.

  • 타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상한을 지정해야 한다.
fun <T> printHashCode1(t: T) { //T는 널이 될수 있다.
    println(t?.hashCode())
}
printHashCode1(null)

fun <T: Any> printHashCode2(t: T) { //T는 널이 될수 없다.
    println(t.hashCode())
}
printHashCode2(null) //호출 불
//Type parameter bound for T in fun <T : Any> printHashCode2(t: T): Unit
//is not satisfied: inferred type Nothing? is not a subtype of Any

@Nullable String 은 코틀린의 String? 과 같다.

@NotNull String 은 코틀랜의 String 과 같다.

플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입을 말한다.

플랫폼 타입은 선언할 수는 없고 자바 코드에서 가져온 타입만 플랫폼 타입이 된다.

자바 메소드를 오버라이드할 때 가 메소드의 파라미터와 반환 타입을 null 가능성(가능, 불가)을 선언해야 한다

코틀린의 원시 타입

원시타입과 래퍼 타입을 구분하지 않는다.

숫자 변환시에 코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.

대신 직접 변환 메소드를 호출해야 한다.

val i = 1
val l: Long = i //Error type mismatch

val l2: Long = i.toLong()

코틀린에서는 한타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.

명시적으로 변환 함수를 사용해야 한다.

Any 최상위 타입은 Int 등의 원시타입을 포함한 모든 타입의 조상 타입이다.

Unit 타입은 자바의 void와 같은 기능이며 Unit의 경우 타입 인자로도 사용가능 하다.

Nothing 타입은 함수의 변환 타입이나 반환 타입의 타입 파라미터로만 쓸 수 있다.
아무 의미가 없는 파라미터 반환시에 사용된다.

컬렉션과 배열

컬렉션의 널가능성은 원소가 널이 되거나 컬렉션이 널이 될 수 있다.

List<Int?>  //원소가 널 가능
List<Int>?  //컬렉션이 널 가능
List<Int?>? //원소, 컬렉션 모두 널 가능

filterNotNull 함수를 사용하면 컬렉션의 널 체크를 편하게 사용 가능

컬렉션의 데이터를 읽기만 할 경우 kotlin.collections.Collection 인터페이스 사용

컬렉션의 데이터를 수정하려면 kotlin.collection.MtableCollection 인터페이스 사용

기본적으로 읽기전용 인터페이스를 사용하되 변경이 필요할 경우 변경 가능한 버전을 사용하라

val과 var 를 구별한 것처럼 프로그램에서 데이터가 어떤 일이 일어나는지 더 쉽게 이해하기 위함

읽기전용 컬렉션이라 하더라도 객체를 가르키는 참조일때
읽기전용 컬렉션이 항상 thread safe 하지 않다는 점을 명심해야 한다.

  • 컬렉션 생성 함수
컬렉션 타입 읽기 전용 변경 가능
List listOf MutableListOf, arrayListOf
Set setOf MutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
Map mapOf MutableMapOf, hashMapOf, linkedMapOf, sortedMapOf

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

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

  1. arrayOf 함수에 원소를 넘기면 배열을 만들 수 있다.
  2. arrayofNulls 함수에 정수 값을 인자로 넘기면 원소가 null인 배열을 만들 수 있다.
  3. Array 생성자는 배열키기와 람다를 인자로 받아서 람다를 호출하여 배열을 만들어 준다.
  • Array 생성자로 배열만들기
val letters = Array<String>(26) { i -> ('a' + 1).toString() }
println(letters.joinToString(""))
//abcdefghijklmnopqrstuvwxyz
  • toTypedArray 메소드로 컬렉션을 배열로 만들기
val strings = listOf("a", "b", "c")
println("%s/%s/%s".format(*strings.toTypedArray())) //vararg 인자이기에 스프레드 연산자(*) 사용
  • 배열에서 forEachIndexed 사용하기
fun main(args: Array<String>) {
    args.forEachIndexed { index, element ->
        print("Argument $index is: $element\n")
    }
}
728x90

'개발&TIL > kotlin' 카테고리의 다른 글

코틀린학습 - #6 오버로딩  (0) 2020.11.17
코틀린학습 - #4 람다  (0) 2020.11.16
코틀린학습 - #3 클래스  (0) 2020.11.11
코틀린학습 - #2 함수  (0) 2020.11.09
코틀린학습 - #1 기초  (0) 2020.11.09