Null

Kotlin

  |   7 min czytania
(Kotlin v1.3)

Wstęp

NullPointerException (NPE) czyli dostęp do referencji null jest koszmarem wielu programistów, potrafi wystąpić w nieoczekiewanych sytuacjach i doprowadził już do nie jednego crasha. Ze względu na swoją specyfikę znany jest jako The Billion Dollar Mistake. Kotlin został stworzony m.in. z myślą o eliminacji zagrożeń związanych z referencją null poprzez wykluczenie NPE z kodu. Jedynymi możliwościami, aby przypisać wartość null lub wywołać NPE jest jawne wywołanie KotlinNullPointerException, użycie operatora !!, niespójność danych w stosunku do inicjalizacji (np. wyciek pamięci do referencji this) czy też kooperacja z kodem języka Java (z którego może przyjść null).

Referencje

W Kotlin referencje, które nie mogą trzymać wartości null zwane są non-nullable references, natomiast te które mogą przyjmować null zwane są nullable references. Aby zmienna danego typu mogła przyjąć wartość null musi być w sposób jawny oznaczona w momencie definicji operatorem ?. Jeśli zmienna nie deklaruje typu w sposób jawny i w momencie inicjalizacji przyjmuje wartość null wówczas jest typu Nothing (nie posiada żadnej wartości i służy do oznaczenia nieosiągalnych miejsc w kodzie).

var nothing = null //it's okay because type inferred from context is Nothing?
var a: String = "text"
a = null //compiler error - null can not be a value of a non-null type
var b: String? = "text"
b = null //this references is nullable type so it's okay
print(b) //print null - no NPE thrown
print(b.length) //compiler error - only safe or non-null asserted operator can be used on a nullable reference

W celu uniknięcia sytuacji błędu kompilatora dostępu do właściwości zmiennej typu nullable można posłużyć się standardowym podejściem instrukcji warunkowych znanych z większości języków. Jeśli sprawdzana referencja jest stałą i nie przyjmuje wartości null, wówczas w dalszych warunkach klauzuli następuje automatyczne rzutowanie do typu non-null.

val text: String? = "text"
if(text != null && text.length > 0) //text.length is allowed because of auto non-nullable cast
    print("String is not empty")
else 
    print("String is empty")

Jednakże Kotlin dostarcza kilka dedykowanych rozwiązań dostępu do właściwości zmiennych typu nullable.

Operator bezpiecznego wywołania

Bezpieczne wywołanie odbywa się za pomocą operatora ?. co pozwala na dostęp do właściwości referencji typu nullable oraz nie wyklucza jego użycia w referencjach typu non-nullable. Jeśli zmienna jest null wówczas zwracana jest wartość null, a w przeciwnym wypadku wartość żądana.

var text: String? = "text"
print(text?.length) //print 5
text = null
print(text.length) //compiler error - use safe operator to get value
print(text?.length) //no compile error - print null - no NPE thrown

Bezpieczne wywołanie może być użyteczne również w łańcuchu wywołań. Jeśli któraś z właściwości łańcucha jest null wówczas zwracany jest null lub pomijana jest operacja przypisania wartości.

var name: String? = building?.floor?.room?.person?.name //return null only if any property is null
building?.floor?.room?.person?.name = "Jack" //skipp assignment if any property is null

Jeśli żądana operacja ma wykonać się tylko dla referencji, które nie są null można wykorzystać w tym celu funkcję zakresu np.: let.

var text: String? = "text"
text?.let { print("not null") } //print is okay
text = null 
text?.let { print("not null") } //print is ignore because of null

Operator Elvis

Instrukcje warunkowe mogą być zastąpione operatorem Elvis ?:, który zwraca wartość po swojej lewej stronie jeśli jest różna od null, a w przeciwnym wypadku wartość po prawej stronie. Ponadto zwracana wartość w przypadku wartości null może być zgłaszana wyrażeniem throw, które zwraca obiekt typu Nothing.

var text: String? = "text"
//if-else
var length: Int = if(text != null) text.length else -1
//instead of if-else just use Elvis
length = text?.length ?: -1

//if true then value is null and has Nothing? type
var room = building?.floor?.room ?: throw Exception("No room provided")
var name = person?.name ?: null 

Operator asercji

Operator asercji not-null !! konwertuje zmienną do typu non-nullable i wyrzuca wyjątek jeśli wartość jest równa null. Służy on do wywołania w sposób jawny NPE, co w Kotlinie nie jest w cale łatwe. W związku z tym jego użycie może stwarzać zagrożenia jakie wiążą się z NPE i jest niezalecane.

var text: String? = null
val length = text!!.length //KotlinNullPointerException because text is null!

Wyjątki

Wszystkie klasy wyjątków (exception) w Kotlin są pochodnymi klasy Throwable, natomiast nowe klasy wyjątków powinny dziedziczyć po klasie Exception. Instancje klas wyjątków zawierają wiadomość, ślad stosu i opcjonalny powód wywołania, a rzucanie wyjątku odbywa się za pomocą wyrażenia throw. Kotlin w przeciwieństwie do Java nie posiada wyjątków typu checked (wszystkie są unchecked) w związku z czym metoda zgłaszająca wyjątek nie musi go deklarować.

//some custom function and exception class
class CustomException(message: String) : Exception(message)

@Throws(CustomException::class, Exception::class) //this is optional
fun action(text: String) {
    if(text.length != 5 || !text.contains("PL")) { 
        throw CustomException("Passed code hasn't valid polish format")
    }
    else {
        //do some job
    }
}

Wywołanie newralgicznego kodu i ewentualne przechwytywanie wyjątku odbywa się w klauzuli try-catch-finally, które jest wyrażeniem w związku z czym ostatnia instrukcja bloku try lub catch może zwracać rezultat. Warto zauważyć, że w związku brakiem wyjątków oznaczonych jako checked, kod wywołujący metodę, która może zgłaszać wyjątek nie musi umieszczać jej w klazuli try-catch.

val isValid: Boolean = try {
    action("PL12")
    true
}
catch(e: CustomException) {
    print(e) //CustomException: Passed code hasn't valid polish format
    false
}
catch(e: Exception) { 
    //depends on expected exception types more than one catch block can be declared
    false
}
finally {
    print("This block is optional")
    true //this return value is ignored
}
print(isValid) //false