일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- ㅋㅁ
- 빌터패턴
- F
- 옵저버 패턴
- 싱글톤
- Singleton
- Observer Pattern
- 프로토타입 패턴
- 디자인패턴
- 추상팩토리패턴
- Abstract Factory
- 팩토리 메소드
- 함수형프로그래밍
- El
- ㅓ
- Functional Programming
- PrototypePattern
- 디자인패턴 #
- builderPattern
- r
- Kotlin
- 코틀린
- a
- designPattern
- Design Pattern
- 추상 팩토리
- factory method
- Today
- Total
오늘도 더 나은 코드를 작성하였습니까?
코틀린 타입 시스템 ( Null 가능성) 본문
코틀린을 비롯한 최신 언어에서는 null에 대한 접근방법은 가능한 이문제를 runtTime 시점이 아닌 compile 시점으로 옮기는 것이다. null 이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해서 실행 시점에 발생하는 예외를 줄이는 것이다.
Null 이 될 수 있는 타입
- 코틀린은 null 이 될 수 있는 타입을 명시적 지원한다
- TYPE? = TYPE or null
- null이 될 가능성이 있는 타입의 변수는 메서드의 직접 호출을 할 수 없다.
- null이 될 가능성이 있는 타입의 변수를 null 이 될 수 없는 타입의 변수에 대입할 수 없다.
- null이 될 가능성이 있는 타입을 null 될 수 없는 타입을 파라미터로 받는 함수에 전달할 수 없다.
타입의 의미
- 타입은 어떤 값들이 가능한지와 그타 입에 대해 수행할 수 있는 연산의 종류를 결정한다.
- String name = null; 에서 String과 null은 완전히 다른 종류이다.
- null이 될 수 있는 타입은 타입은 있자만 null인지 확인을 추가로 검사하지 않는 이상 연산을 수행할 수 있을지 알 수 없다.
코틀린은 놀이 가능한 타입과 널리 불가능한 타입을 명시하고 각 타입에 대해 어떤 연산이 수행될지 명확히 이해할 수 있으며 실행 시점에 예외가 발생하는 연산 판단 자체를 피할 수 있다.
안전한 호출 연산자 (?.)
?. 은 null 검사와 메서드 호출을 한 번의 연산으로 수행한다.
즉 널이면 널을 대입하고 널이 아니면 호출한다.
s?. toUpperCase() ===> if (s!= null ) s.toUpperCase() else null
안전한 호출의 결과로 반환되는 타입도 널이 될 수 있는 타입이라는 사실이 중요하다.
s?. toUpperCase()의 결과 타입은 String?이다.
예시
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 Person.countryName(): String{
val country = this.company?.address?.country
return if (country != null) country else "UnKnown"
}
fun main() {
val person = Person("hik", null)
println(person.countryName())
}
엘비스 연산자 (?:)
코틀린은 null을 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 제공한다.
fun foo(s: String?){
val t: String = s ?: "empty"
}
fun main() {
val str :String? = null
println(foo(str))
}
?: 은 이항 연산자이다 좌측 피연산자를 벌인 지 검사 후 널이 아니면 좌측을 선택하고 널이면 우측 피연산자를 선택한다.
엘비스 연산자를 객체가 널인 경우 널을 반환하는 안전 호출 연산자와 함께 사용해서 객체가 널인 경우에 대비한 값을 지정한다.
fun strLenSafe(s: String?): Int = s?.length ?: 0
fun main() {
val str1 = "abc"
val str2 = null
println(strLenSafe(str1))
println(strLenSafe(str2))
}
코틀린에서는 return이나 throw 연산도 식이다. 따라서 엘비스 연산자의 우항에 return 또는 throw 등의 연산을 넣을 수 있고 엘비스 연산자를 이용하여 더욱 편하게 사용할 수 있다.
fun printShippingLabel(person: Person){
val address = person.company?.address
?: throw IllegalArgumentException("No Address")
with(address){
println(streetAddress)
println("$zipCode $city $country")
}
}
fun main() {
val address = Address("Elsestr 47",80687,"Munich", "Germany")
val jetbrains = Company("JetBrains", address)
val person = Person("hik", jetbrains)
val person2 = Person("hoho", null)
printShippingLabel(person)
printShippingLabel(person2)
}
안전한 캐스트 ( as?)
코틀린은 타입 캐스트 연산을 할 때 대상 값을 as로 지정한 타입으로 변환이 될 수 없으면 ClassCastException이 발생한다. 물론 as를 사용하기 전에 Is (스마트 캐스트)를 통해 미리 as로 변환 가능한지 검사를 해볼 수 있지만,
안전하면서 간결한 해법을 코틀린은 제공해준다.
as? 는 어떤 값을 지정한 타입으로 캐스트 한다. 변환이 가능하면 진행하고 변환이 되지 않으면 null을 반환한다.
안전한 캐스트를 사용할 때는 일반적 패턴은 엘비스 연산자를 사용한다.
class Person(val firstName: String, val lastName: String){
override fun equals(other: Any?): Boolean {
val otherPerson = other as? Person ?: return false
return otherPerson.firstName == firstName &&
otherPerson.lastName == lastName
}
}
fun main() {
val p1 = Person("Dmitry", "Jemerov")
val p2 = Person("Dmitry", "Jemerov")
println(p1 == p2)
println(p1.equals(100))
널 아님 단언!!
- 가장 단순하면서 무딘 도구다
- 어떤 값을 가진 타입이든 날이 될 수 없는 타입으로 강제 변환한다. (널값에 대해!! 적용 시 연산을 하면 NPE 발생한다. )
fun ignoreNulls(s: String?){
val sNotNull = s!!
println(sNotNull.length)
}
fun main() {
ignoreNulls(null)
}
위 코드를 실행 시면 NPE가 발생한다. 발생한 지점이 sNotNull.length가 아닌 val sNotNull = s!!이다.
근본적으로!! 는 컴파일러에게 나는 이 값이 null 이 아님을 잘 알고 있다. 만약 null 이 된다면 예외가 발생해도 감수하겠다는 표현이다.
코틀린 설계자들은 컴파일러가 검증할 수 없는 단언 !! 을 사용하기보단 더 나은 방법을 찾아보라는 표현으로 !! 기호를 택했다.
또한 !! 한 줄에 연속해서 사용하지 말라.
그렇다면 어떤 경우에!! 를 사용할까?
어떤 함수가 값이 널 인지 이미 확인한 후 다른 함수를 호출하려고 할 때도 코틀린 컴파일러는 호출된 함수 안에서 그 값을 안전하게 사용할 수 있음을 인식할 수 없다.
다른 함수에서 이미 널값이 아닌 값을 전달받는 게 확실하다면 다시 널 검사를 할 필요가 없다.
class CopyList(val list: List<String>?){
fun isEnabled(): Boolean =
list != null
fun executeCopy(): List<String>{
val newList = arrayListOf<String>()
if (isEnabled())
newList.addAll( list!! )
return newList
}
}
let 함수
let 함수를 안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널 인지 검사한 다음에 그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한번에 처리할 수 있다.
널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우다.
A? 를 A 타입의 인자로 넘기고 싶을때!
let은 자신을 수신 객체를 인자로 전달 받은 람다에게 넘긴다.
널이 될 수 있는 값에 대해 안전한 호출 구문을 사용해 let을 호출하되
널이 될 수 없는 타입을 인자로 받는 람다를 let에 전달한다.
foo?. let { it ->
// it은 날이 될 수 없는 타입이다.
}
foo가 널이면 아무 일도 일어나지 않는다.
fun sendEmailTo(email: String){
println("Sending email to $email")
}
fun main() {
var email: String? = "hahah@gmail.com"
var email2 = null
email?.let { sendEmailTo(it) }
email2?.let { sendEmailTo(it) }
}
어떤 식이 있고 그 값이 널이 아닐 때 수행해야 되는 로직이 있을 때 let을 쓰면 훨씬 편하다.
fun sendEmailTo(email: String){
println("Sending email to $email")
}
class Person(val name: String, val email:String)
fun getTheBestPersonInTheWorld(): Person? = null
fun main() {
val person: Person? = getTheBestPersonInTheWorld()
if (person != null) sendEmailTo(person.email)
getTheBestPersonInTheWorld()?.let { sendEmailTo(it.email) }
}
여러 값에 대한 null 검사를 let을 호출을 중첩시켜 처리하기보단 if로 하는 것이 낫다.
나중에 초기화할 프로퍼티 ( lateinit )
코틀린의 클래스 안에 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 따로 특별한 메서드에서 초기화할 수 없다.
코틀린에서는 기본적으로 생성자에서 모든 프로퍼티를 초기화해야 된다.
프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 초기화를 해야 된다.
lateinit은 var 프로퍼티에만 사용할 수 있다.
val 은 final필드로서 반드시 생성자 안에서 초기화해야 된다.
생성자 밖에서 초기화해야 되는 경우 프로퍼티는 var이어야 한다.
지연 초기화하는 프로퍼티는 널이 될 수 없는 타입이라 해도 더 이상 생성자 안에서 초기화할 필요가 없다.
널이 될 수 있는 타입의 확장
널이 될 수 있는 타입에 대한 확장 함수를 정의하면 null값을 다루는 강력한 도구로 활용할 수 있다.
특정 메서드를 호출하기 전에 수신 객체가 널이 될 수 없다고 보장해주는 것 대신
직접 메서드를 호출해도 확장 함수인 메서드가 알아서 널 처리를 하게 한다.
fun verifyUserInput(input: String?){
if(input.isNullOrBlank()){ // 널이 될수있는 타입에 안전호출 하지않음
println("please fill in the required fields")
}
}
fun main() {
verifyUserInput(null)
verifyUserInput(" ")
}
input(널이 될 수 있음). isNullOrBlank() (널이 될 수 있는 타입의 확장 함수)
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrBlank(): Boolean {
contract {
returns(false) implies (this@isNullOrBlank != null)
}
return this == null || this.isBlank()
}
CharSequence? 널이 될 수 있는 타입에 대한 확장 함수를 정의하면 안전 호출 없이 호출 가능하다.
return this == null || this.isBlank() 2번째 this 스마트 캐스트가 적용된다.
. isNullOrBlank() 을 호출했다고 해서 널이 될 수 없는 타입으로 변하진 않는다.
* let은 널이 될 수 있는 값에 안전 호출을 사용하지 않고 호출 시 실제 수신 객체가 널인경우 에러가 발생한다.
let은 반드시 안전 호출과 같이 사용하는 것이 좋다.
확장함수를 작성할 때 처음에는 널이 될 수 없는 타입에 대해서 정의를 하라 그러고 나서 나중에 널이 될 수 있는 타입에 대해서 그 함수를 호출할 상황이 생긴다면 확장 함수 안에서 널을 처리하게 되면 안전하게 그 확장 함수를 널이 될 수 있는 타입에 대한 확장 함수로 바꿀 수 있다
< T > 타입 파라 피터의 널 가능성
코틀린에서 함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다.
즉, 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름끝에 ?가 없더라도 널이 될 수 있는 타입이다.
fun <T> printHashCode(t: T){
println(t?.hashCode())
}
fun main() {
printHashCode(null) // T의 타입은 Any? 로 추론된다
}
T? 아닐지라도 t는 널값을 받을 수 있다.
타입 파라미터가 널이 아님을 확신한다면 널이 될 수 없는 타입 상한을 지정해야 한다.
fun <T:Any> printHashCode(t: T){
println(t?.hashCode())
}
타입 파라미터는 널이 될 수 있는 타입을 표시하려면 반드시 물음표를 타입 이름에 뒤에 붙여하는 규칙의 유일한 예외다.
널 가능성과 자바
코틀린은 자바와 상호 운용성을 강조하는 언어이다. 그렇다면 자바와 코틀린을 조합하면 어떤 일이 생길까?
1. 자바 코드에도 애노테이션으로 표시된 널 가능성 정보가 있다. 이런 정보가 존재하면 코틀린의 컴파일러도 그 정보를 활용한다.
@Nullable String ---> String?
@NotNull String ---> String
코틀린이 이해하는 널 가능성 애노테이션들은 JSR-305 표준, 젯브레인스 도구가 지원하는 애노테이션, 안드로이드 스튜디오 등이다.
2 플랫폼 타입.
String! 은 자바에서 온 플랫폼 타입이란 것이다.type뒤에! 은 널 가능성에 대한 어떤 정보도 없다는 뜻이다.
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName(){
return name;
}
}
fun main() {
val person = Person("kotlin")
val name1: String = person.name
val name2: String? = person.name
}
플랫폼 타입은 코틀린에게 있어 널 관련 정보를 알 수 없는 타입이다.자바와 마찬가지로 플랫폼 타입에 있어 모든 연산의 책임은 개발자에게 있다. 컴파일러는 모든 연산을 허용한다.하지만 널인 값에 대한 연산 수행 시 NPE를 발생한다.
fun yellAt(person: Person){
println(person.name.toUpperCase()+"!!!")
}
fun main() {
yellAt(Person(null))
}
코틀린 컴파일러는 공개 가시성 코틀린 함수의 널이 아닌 타입인 파라미터와 수신 객체에 대한 널 검사를 추가해준다.
즉, toUpperCase 호출 시점에 person.name의 널 검사를 해서 위와 같은 에러가 발생하는 것이다.
fun yellAt(person: Person){
println( (person.name ?: "Anyone").toUpperCase()+"!!!")
}
fun main() {
yellAt(Person(null))
}
자바 API 문서를 잘 읽고 그 메서드가 널을 반환 할지 알아내고 널을 반환 한다면 추가적인 검사를 해야 된다.
3. 상속 및 구현
코틀린에서 자바 메서드를 오버라이드 할 경우 파라미터와 반환 타입을 널이 될수있는 타입인지 널이 될 수 없는 타입인지 선언할 것인지 결정해야 된다.
public interface StringProcessor {
void process(String value);
}
class StringPrinter: StringProcessor{
override fun process(value: String) {
println(value)
}
}
class NullableStringPrinter: StringProcessor{
override fun process(value: String?) {
if (value != null)
println(value)
}
}
아래와 같은 2가지 타입에 대한 구현 전부를 받아드릴 수 있다.
널이 될수없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언 문들 코틀린 컴파일러가 생성하여
다른 코틀린 코드가 이 함수를 사용할 때 만약 자바 코드가 null을 넘기면 단언 문이 발동해 예외가 발생한다.
'Kotlin in Action > 코틀린 기초' 카테고리의 다른 글
코틀린 타입 시스템( 컬렉션과 배열) (0) | 2020.08.21 |
---|---|
코틀린 타입 시스템(코틀린 원시 타입) (0) | 2020.08.20 |
람다로 프로그래밍 (자바 함수형 인터페이스 사용) (0) | 2020.08.18 |
람다로 프로그래밍 (지연계산(Lazy) 컬렉션 연산) (0) | 2020.08.16 |
람다로 프로그래밍(컬렉션 함수형 API) (0) | 2020.08.16 |