kotlin의 타입추론은 할당 될 때의 정확하게 오른쪽에 있는 피연산자에 맞게 설정된다.
다음과 같은 예를 보자.
open class Animal
class Zebra: Animal()
fun main() {
var animal = Zebra()
animal = Animal() // 오류 : Type Mismatch
}
이경우 animal이 Zebra 타입으로 정의되어 상위클래스인 Animal을 설정하여도 오류가 남을 볼 수 있다.
절대로 슈퍼클래스 또는 인터페이스로는 설정되지 않는다.
fun main() {
var animal : Animal = Zebra()
animal = Animal() // Possible!
}
원하는 타입의 상위 타입을 명시적으로 지정하여 사용하면 이러한 문제는 해결된다.
타입을 명확하게 지정해야 하는 경우에는 명시적으로 타입을 지정해야 한다는 원칙을 가지고 진행하자. 추론타입은 프로젝트가 진전될 때, 제한이 너무 많아지거나 예측하지 못한 결과를 낼 수 있다는 것을 기억하자.
확실하게 어떠한 경우에도 동작해야 하는 코드가 있다면 예외를 활용해 보자.
코드 동작에 제한을 거는 방법으로는 다음과 같은 방법이 있다.
이렇게 제한을 걸어서 다른 개발자도 문제를 확인할 수 있다.
문제가 있을경우 예외를 throw해준다. 예상치 못한 동작을 하는 것보다, 예외를 throw하는 것이 문제를 놓치지 않을 수 있고, 코드가 더 안정적으로 작동하게 한다.
함수를 정의할 때 타입 시스템을 활용해서 아규먼트에 제한을 걸자.
다음과 같은 예제를 보자
fun factorial(n : Int){
require(n >=0)
return if (n<=1) 1 else factorial(n-1) * n
}
입력의 유효성 검사는 코드의 함수 가장 앞부분에 배치하고, 읽는 사람도 쉽게 확인할 수 있게 하자.
조건을 만족하지 못한다면 IllegalArgument Exception을 발생시킨다.
다음과 같이 람다를 활용하여 지연 메시지를 정의할 수도 있다.
fun factorial(n : Int){
require(n >=0){
"0보다 작은 수로는 팩토리얼을 연산할 수 없습니다."
}
return if (n<=1) 1 else factorial(n-1) * n
}
구체적인 조건을 만족할 때만 함수를 사용할 수 있게 해야 할 때가 있다.
다음과 같은 경우를 보자.
사용 예제를 보자.
fun speak(text: String){
check(isInitiallized)
// ...
}
fun getUserInfo(): UserInfo{
checkNotNull(token)
// ...
}
fun next(): T{
check(isOpen)
// ...
}
check 함수는 require과 비슷하지만, 지정된 예측을 만족하지 못할 때, IllegalStateException을 throw한다.
Assert 계열 함수는 테스트를 할 떄만 활성화 된다. 단위 테스트의 갯수를 줄이거나 간략화 해줄 수 있으며 다음과 같은 장점이 있다.
코틀린은 require과 check 블록으로 어떤 조건을 확인해서 true가 나왔다면 해당 조건은 이후로도 true일 것이라 판단하고 스마트 캐스팅을 진행한다.
public inline fun require(value : Boolean): Unit{
contract{
returns() implies value
}
require(value) { "Failed requirement."}
}
이러한 특징은 어떤 대상이 null인지 확인할 때 유용하다.
class Person(val email: String?)
fun sendEmail(person: Person, message: String){
require(person.email != null)
val email: String = person.email
// ...
}
null 체크를 진행할 경우 requireNotNull
, CheckNotNull
이라는 특수 함수를 사용해도 괜찮다. 둘 다 스마트 캐스트를 지원함으로, 변수를 언팩하는 용도로 활용할 수 있다.
또한 nullability를 목적으로 오른쪽에 throw 또는 return을 두고 Elvis 연산자를 활용하는 경우도 많다. 이러한 코드는 보기좋고, 읽기 좋고, 유연하게 사용할 수 있다.
오른쪽에 return을 넣으면 오류를 발생시키지 않고 단순하게 함수를 중지할 수 있다.
또는 run 함수를 조합하여 로그를 출력할 수도 있다.
fun sendEmail(person: Person, text : String){
val email: String = person.email ?: return
}
// Activities
context ?: return binding.root
fun sendEmail(person: Person, text : String){
val email: String = person.email ?: run{
log("이메일이 보내지지 않았습니다. 이메일 주소가 없어요")
return
}
// ...
}
지금까지 let을 사용하여 null safe 처리를 진행하였는데,
text?.let {
view.text = it.toString()
}
이러한 코드들이 허용될 수 있으나, 언제나 올바른 let의 사용법은 아니라고 한다.
다음과 같은 예를 보자
fun process(str: String?) {
str?.let { /*Do something*/ }
}
이러한 process 함수를 자바로 디컴파일을 진행하면 다음과 같은 형태가 된다.
public final void process(@Nullable String str) {
if (str != null) {
boolean var4 = false;
/*Do something*/
}
}
쓸대 없이 추가 변수가 늘었고, 이러한 작업이 많이 수행되면 아무 이득없이 추가적인 작업만 더 하는 코드가 된다.
null 체크를 진행할 때에는 다음과 같이 진행하도록 하자
if (str != null) {
/*Do something*/
}
str ?: {
// /* Do something */
} ()
참고 자료
출처: https://tourspace.tistory.com/208 [투덜이의 리얼 블로그:티스토리]