관리 메뉴

너와 나의 스토리

[Kotlin] 코틀린 타입 시스템 - ? / !! /.? / as? / let 함수 / lateinit 본문

Programming Language/Kotlin

[Kotlin] 코틀린 타입 시스템 - ? / !! /.? / as? / let 함수 / lateinit

노는게제일좋아! 2021. 6. 17. 09:48
반응형

타입 시스템(Type system)

 

NULL 가능성(Nullability)

  • NullPointerException(NPE) 오류를 피할 수 있도록 돕는 코틀린 타입 시스템의 특성이다.
  • 코틀린에서는 널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해서 실행 시점에 발생할 수 있는 예외의 가능성을 줄일 수 있다.

 

NULL이 될 수 있는 타입

  • 코틀린의 모든 타입은 기본적으로 널이 될 수 없는 타입이다.
  • 코틀린은 널이 될 수 있는 타입을 명시적으로 지원한다.
    • 타입 옆에 물음표(?)를 표시한다.
  • NULL이 될 수 있는 타입의 변수이지만, 현재 NULL이 아님을 주장할 수 있다.
    • 느낌표 2개(!!)를 변수 뒤에 붙인다.
    • 이 표시를 통해 null이 될 수 없는 변수에 null이 될 수 있는 타입을 주입할 수 있다.
  • 널이 될 수 있는 타입인 변수에 대해 메서드를 호출하면 NPE가 발생할 수 있으므로, 코틀린은 그런 메서드 호출을 금지함으로써 NTE 발생을 예방한다.
// var a:Int = null -> error
var a:Int? = null 
var b:Int? = 10
// var c:Int = b  -> error
var c:Int = b!!

 

 

안전한 호출 연산자: ?.

  • (?.)은 null 검사와 메서드 호출을 한 번의 연산으로 수행한다.
  • 예: 
    • foo?.bar()
    • foo가 null이면 bar() 메서드 호출이 무시되고 null이 결과 값이 된다.
    • foo가 null이 아니면 bar() 메서드를 정상 실행하고 결과값을 얻어온다.
class Employee(val name: String, val manager: Employee?)

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

 

엘비스 연산자: ?:

  • null 대신 사용할 디폴트 값을 지정할 때 편리한 연산자
  • 사용 방법:
    • fun foo(s: String?) {
          val t: String = s ?: ""
      }
    • s가 null이면 ""(빈 문자열)을 t에 넣고
    • s가 null이 아니면 t에 s를 넣는다.
  • 예 1:
    • fun strLenSafe(s: String?): Int = s?.length ?: 0
  • 예 2:
    • var b:Int? = 10
      // var d:Int = b ->> error 
      var d:Int = b ?: 3
    • b가 null이면 3을 준다고 지정해 b가 null이 아님을 증명함으로써 디폴트 타입을 가지는 d에게 null이 될 수 있는 타입을 가진 b를 넘겨줄 수 있다.
  • 코틀린에서는 return이나 throw 등의 연산도 식이다. 
    • 따라서 엘비스 연산자의 우항에 return, throw 등의 연산을 넣을 수 있다.
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)

fun printShippingLabel(person: Person) {
    val address = person.company?.address ?: throw IllegalArgumentException("No address")

    with(address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

fun main(args: Array<String>) {
    val address = Address("Elsestr. 47", 88432, "Munich", "Germany")
    val jetbrains = Company("JetBrains", address)
    val person = Person("Dmitry", jetbrains)
    printShippingLabel(person)
    /*
    * output:
    * Elsestr. 47
    * 88432 Munich, Germany
    * */
}
  • 다음과 같이 with 연산자를 사용하면 불필요한 중복을 줄일 수 있다. 
    • with를 사용하지 않았더라면 println(address.streetAddress)처럼 사용해야 하지만, with를 사용함으로써 address를 생략하고 필드 값을 호출할 수 있다.
fun main(args: Array<String>) {
    val jetbrains = Company("JetBrains", null)
    val person = Person("Dmitry", jetbrains)
    printShippingLabel(person)
    /*
    output:
    Exception in thread "main" java.lang.IllegalArgumentException: No address
	at MainKt.printShippingLabel(main.kt:6)
	at MainKt.main(main.kt:18)
    */
}

 

 

안전한 캐스트: as?

  • 자바 타입 캐스트와 마찬가지로 대상 값을 as로 지정한 타입으로 바꿀 수 없다면 ClassCastException이 발생한다.
    • 그래서 자바에서는 보통 is를 통해 미리 as로 변환 가능한 타입인지 검사해 봄
  • as? 연산자는 어떤 값을 지정한 타입으로 캐스트하고, 변환할 수 없으면 null을 반환한다.
  • 예:
    •  
    • foo as? Type
    • foo is Type이면 foo는 Type으로 변환하고
    • foo !is Type이면 null을 반환한다.
  • 엘비스 연산자와 함께 사용
    • class Person(val name: String, val company: Company?) {
          override fun equals(o: Any?): Boolean {
              val otherPerson = o as? Person ?: return false
      
              return otherPerson.name == name && otherPerson.company == company
          }
      }
    • as?을 토해 o가 Person을 캐스트 안되면 null이 되고, 이는 엘비스 연산자(?:)로 인해 false가 리턴된다.

 

let 함수

  • let 함수를 안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 다음에 그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한꺼번에 처리할 수 있다.
fun sendEmailTo(email: String) {
    println("Sending email to $email")
}

fun main(args: Array<String>) {
    var email: String? = "yole@example.com"
//    sendEmailTo(email) -> error
    email?.let { sendEmailTo(it) }
}

 

 

나중에 초기화할 프로퍼티

  • 코틀린에서 클래스 안의 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메서드 안에서 초기화할 수 없다. 
    • 코틀린에서는 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다.
    • 게다가 프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 그 포로퍼티를 초기화해야 한다. 
    • 그런 초기화 값을 제공할 수 없으면 널이 될 수 있는 타입을 사용할 수 밖에 없는데, 이러면 모든 접근에 대해 널 검사를 넣거나 !! 연산자를 써야한다.
    • 예:
      class MyTest {
          private var myService: MyService? = null
          @Before
          fun setUp() {
              myService = MyService()
          }
      
          @Test
          fun testAction() {
              Assert.assertEquals("foo", myService!!.performAction())
          }
      }
  • lateinit 변경자: 프로퍼티를 나중에 초기화
    • 나중에 초기화하는 프로퍼티는 항상 var여야 한다. 
    • val 프로퍼티는 final 필드로 컴파일되며, 생성자 안에서 반드시 초기화해야 한다.
    • 예:
      class MyTest {
          private lateinit var myService: MyService
          @Before
          fun setUp() {
              myService = MyService()
          }
      
          @Test
          fun testAction() {
              Assert.assertEquals("foo", myService.performAction())
          }
      }​

 

 

널이 될 수 있는 타입 확장: isNullOrEmpty(), isNullOrBlank

  • 널이 될 수 있는 타입에 대한 확장 함수를 정의하면 null 값을 다루는 강력한 도구로 활용할 수 있다. 
var emptyString = ""
println(emptyString.isNullOrBlank()) // output: True
println(emptyString.isNullOrEmpty()) // output: True

 

타입 파라미터의 널 가능성

  • 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다. 
    • t가 널이 될 수 있으므로 안전한 호출(?.)을 써야만 한다.
fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}

fun main(args: Array<String>) {
    printHashCode(null) // output: null
}
  • 타입 파라미터가 널이 아님을 지정할 수 있다. -> 타입 상한(upper bound)를 지정
fun <T : Any> printHashCode(t: T) {
    println(t.hashCode())
}

fun main(args: Array<String>) {
	// printHashCode(null) -> error
}

 

 

 

출처:

- [Kotlin IN ACTION]

반응형
Comments