Пару недель назад тимлид сделал для компании презентацию о том, что в Котлине хорошо. Одним из самых интересных вопросов был «А как в Котлине выстрелить себе в ногу?» Так получилось, что ответил на этот вопрос я.
Disclaimer:
Не стоит воспринимать эту статью как «Kotlin — отстой». Хотя я отношусь скорее к категории тех, кому и со Scala хорошо, я считаю, что язык неплохой.
Все пункты спорные, но раз в год и палка стреляет. Когда-то вы себе прострелите заодно и башку, а когда-то у вас получится выстрелить только в полночь полнолуния, если вы предварительно совершите черный ритуал создания плохого кода.
Наша команда недавно закончила большой проект на Scala, сейчас делаем проект помельче на Kotlin, поэтому в спойлерах будет сравнение со Scala. Я буду считать, что Nullable в Kotlin — это эквивалент Option, хотя это совсем не так, но, скорее всего, большинство из тех, кто работал с Option, будут вместо него использовать Nullable.
1. Пост-инкремент и преинкремент как выражения
Цитирую вопрошавшего: «Фу, это ж баян, скучно». Столько копий сломано, миллион вопросов на собеседованиях C++… Если есть привычка, то можно было его оставить инструкцией (statement'ом). Справедливости ради, другие операторы, вроде +=, являются инструкциями.
Цитирую одного из разработчиков, abreslav:
Смотрели на юзкейсы, увидели, что поломается, решили оставить.На нет и суда нет. Разумеется, в здравом уме никто так делать не будет, но случайно — может быть.
Замечу, что у нас тут не С++, и на собеседовании про инкремент спросить особо нечего. Разве что разницу между префиксным и постфиксным.
var i = 5
i = i++ + i++
println(i)
var a = 5
a = ++a + ++a
println(a)
var b = 5
b = ++b + b++
println(b)
var c = 5
c = c++ + ++c
println(c)
var d = 5
d = d + d++ + ++d + ++d
println(d)
var e = 5
e = ++e + ++e + e++ + e
println(e)
25
28
2. Одобренный способ
val foo: Int? = null
val bar = foo!! + 5
В документации говорится, что так делать стоит только если вы очень хотите получить NullPointerException. Это хороший метод выстрелить себе в ногу: !! режет глаз и при первом взгляде на код все понятно. Разумеется, использование !! предполагается тогда, когда до этого вы проверили значение на null и smart cast по какой-нибудь причине не сработал. Или когда вы почему-то уверены, что там не может быть null.
val foo: Option[Int] = None
val bar = foo.get + 5
3. Переопределение invoke()
Начнем с простого: что делает этот кусок кода и какой тип у a?
class A(){...}
val a = A()
А здесь что будет?
class В private constructor(){...}
val b = B()
class B private constructor(){
var param = 6
constructor(a: Int): this(){
param = a
}
companion object{
operator fun invoke() = B(7)
}
}
Для класса может быть определена фабрика. А если бы она была в классе A, то там все равно вызывался бы конструктор.
Теперь вы ко всему готовы:
class С private constructor(){...}
val c = C()
class C private constructor(){
...
companion object{
operator fun invoke() = A(9)
}
}
У переменной c будет тип A. Заметьте, что A и С не связаны родственными узами.
class A(){
var param = 5
constructor(a: Int): this(){
param = a
}
companion object{
operator fun invoke()= A(10)
}
}
class B private constructor(){
var param = 6
constructor(a: Int): this(){
param = a
}
companion object{
operator fun invoke() = B(7)
}
}
class C private constructor(){
var param = 8
constructor(a: Int): this(){
param = a
}
companion object{
operator fun invoke() = A(9)
}
}
class D(){
var param = 10
private constructor(a: Int): this(){
param = a
}
companion object{
operator fun invoke(a: Int = 25) = D(a)
}
}
fun main(args: Array<String>) {
val a = A()
val b = B()
val c = C()
val d = D()
println("${a.javaClass}, ${a.param}")
println("${b.javaClass}, ${b.param}")
println("${c.javaClass}, ${c.param}")
println("${d.javaClass}, ${d.param}")
}
Результат выполнения:
class A, 5 class B, 7 class A, 9 class D, 10
К сожалению, придумать короткий пример, где у вас реально все поломается, я не смог. Но пофантазировать немного можно. Если вы вернете левый класс, как в примере с классом C, то скорее всего, компилятор вас остановит. Но если вы никуда не передаете объект, то можно сымитировать утиную типизацию, как в примере. Ничего криминального, но человек, читающий код, может сойти с ума и застрелиться, если у него не будет исходника класса.
Если у вас есть наследование и функции для работы с базовым классом (Animal), а invoke() от одного наследника (Dog) вернет вам другого наследника (Duck), то тогда при проверке типов (Animal as Dog) вы можете накрякать себе беду.
4. lateinit
class SlowPoke(){
lateinit var value: String
fun test(){
if (value == null){ //компилятор здесь говорит, что проверка не нужна (и правильно делает)
println("null")
return
}
if (value == "ololo")
println("ololo!")
else
println("alala!")
}
}
SlowPoke().test()
class SlowBro(){
val value: String? = null
fun test(){
if (value == null) {
println("null")
return
}
if (value == "ololo")
println("ololo!")
else
println("alala!")
}
}
SlowBro().test()
Я бы сказал, что это тоже одобренный способ, но при чтении кода это неочевидно, в отличие от !!. В документации немного завуалированно говорится, что, мол, проверять не надо, если что, мы кинем тебе Exception. По идее, этот модификатор используется тогда, когда вы точно уверены, что поле будет инициализированно кем-то другим. То есть никогда. По моему опыту, все поля, которые были lateinit, рано или поздно стали Nullable. Неплохо это поле вписалось в контроллер JavaFX приложения, где Gui грузится из FXML, но даже это «железобетонное» решение было свергнуто после того, как появился альтернативный вариант без пары кнопок. Один раз так получилось, что в SceneBuilder изменил fx:id, а в коде забыл. В первые дни кодинга на Kotlin немного взбесило, что нельзя сделать lateinit Int. Я могу придумать, почему так сделали, но сомневаюсь, что совсем нет способа обойти эти причины (читай: сделать костыль).
5. Конструктор
class IAmInHurry(){
val param = initSecondParam()
/*tons of code*/
val twentySecondParam = 10
/*tons of code*/
fun initSecondParam(): Int{
println("Initializing by default with $twentySecondParam")
return twentySecondParam
}
}
class IAmInHurryWithStrings(){
val param = initSecondParam()
/*tons of code*/
val twentySecondParam = "Default value of param"
/*tons of code*/
fun initSecondParam(): String{
println("Initializing by default with $twentySecondParam")
return twentySecondParam
}
}
fun main(args: Array<String>){
IAmInHurry()
IAmInHurryWithStrings()
}
Initializing by default with null
Все просто — к полю идет обращение до того, как оно было инициализировано. Видимо, тут стоит немного доработать компилятор. По идее, если вы пишете код хорошо, такая проблема у вас не должна возникнуть, но всякое бывает, не с потолка же я взял этот пример (коллега себе так выстрелил в ногу, случайно через цепочку методов в редко срабатывающем коде вызвал поле, которое было не инициализировано).
object Initializer extends App{
class IAmInHurry(){
val param = initSecondParam()
/*tons of code*/
val twentySecondParam = 10
/*tons of code*/
def initSecondParam(): Int = {
println(s"Initializing by default with $twentySecondParam")
twentySecondParam
}
}
class IAmInHurryWithStrings(){
val param = initSecondParam()
/*tons of code*/
val twentySecondParam = "Default value of param"
/*tons of code*/
def initSecondParam(): String = {
println(s"Initializing by default with $twentySecondParam")
twentySecondParam
}
}
override def main(args: Array[String]){
new IAmInHurry()
new IAmInHurryWithStrings()
}
}
Initializing by default with null
6. Взаимодействие с Java
Для выстрела тут простор достаточно большой. Очевидное решение — считать все, что пришло из Java, Nullable. Но тут есть долгая и поучительная история. Как я понял, она связана в основном с шаблонами, наследованием, и цепочкой Java-Kotlin-Java. И при таких сценариях приходилось делать много костылей, чтобы заработало. Поэтому решили от идеи «все Nullable» отказаться.
Но вроде как один из основных сценариев — свой код пишем на Kotlin, библиотели берем Java (как видится мне, простому крестьянину-кодеру). И при таком раскладе, лучше безопасность в большей части кода и явные костыли в небольшой части кода, которые видно, чем «красиво и удобно» + внезапные грабли в рантайме (или яма с кольями, как повезет). Но у разработчиков другое мнение:
Одна из основных причин была в том, что писать на таком языке было неудобно, а читать его — неприятно. Повсюду вопросительные и восклицательные знаки, которые не очень-то помогают из-за того, что расставляются в основном, чтобы удовлетворить компилятор, а не чтобы корректно обработать случаи, когда выражение вычисляется в null. Особенно больно в случае дженериков: например, Map<String?, String?>?..
Сделаем небольшой класс на Java:
public class JavaCopy {
private String a = null;
public JavaCopy(){};
public JavaCopy(String s){
a = s;
}
public String get(){
return a;
}
}
И попробуем его вызвать из Kotlin:
fun printString(s: String) {
println(s)
}
val j1 = JavaCopy()
val j1Got = j1.get()
printString(j1Got)
Тип у j1 — String! и исключение мы получим только тогда, когда вызовем printString. Ок, давайте явно зададим тип:
val j2 = JavaCopy("Test")
val j3 = JavaCopy(null)
val j2Got: String = j2.get()
val j3Got: String = j3.get()
printString(j2Got)
printString(j3Got)
Все логично. Когда мы явно указываем, что нам нужен NotNullable, тогда и ловим исключение. Казалось бы, указывай у всех переменных Nullable, и все будет хорошо. Но если делать так:
printString(j2.get())
то ошибку вы можете обнаружить нескоро.7. infix нотация и лямбды
Сделаем цепочку из методов и вызовем ее:
fun<R> first(func: () -> R): R{
println("calling first")
return func()
}
infix fun<R, T> R.second(func: (R) -> T): T{
println("calling second")
return func(this)
}
first {
println("calling first body")
}
second {
println("calling second body")
}
calling first body
Oops!
calling second body
Подождите-ка… тут какая-то подстава! И правда, «забыл» один метод вставить:
fun<T> second(func: () -> T): T{
println("Oops!")
return func()
}
И чтобы заработало «как надо», нужно было написать так:
first {
println("calling first body")
} second {
println("calling second body")
}
calling first body
calling second
calling second body
Всего один перенос строки, который легко при переформатировании удалить/добавить переключает поведение. Основано на реальных событиях: была цепочка методов «сделай в background» и «потом сделай в ui треде». И был метод «сделай в ui» с таким же именем.
object Infix extends App{
def first[R](func: () => R): R = {
println("calling first")
func()
}
implicit class Second[R](val value: R) extends AnyVal{
def second[T](func: (R) => T): T = {
println("calling second")
func(value)
}
}
def second[T](func: () => T): T = {
println("Oops!")
func()
}
override def main(args: Array[String]) {
first { () =>
println("calling first body")
} second { () => //<--------type mismach
println("calling second body")
}
}
}
Зато, пытаясь подогнать скаловский код хотя бы для неочевидности засчет implicit/underscore, я взорвал все вокруг.
object Infix2 extends App{
def first(func: (Unit) => Unit): Unit = {
println("calling first")
func()
}
implicit class Second(val value: Unit) extends AnyVal{
def second(func: (Unit) => Unit): Unit = {
println("calling second")
func(value)
}
}
def second(func: (Unit) => Unit): Unit = {
println("Oops!")
func()
}
override def main(args: Array[String]) {
first { _ =>
println("calling first body")
} second { _ =>
println("calling second body")
}
}
}
И результат:
Exception in thread "main" java.lang.VerifyError: Operand stack underflow Exception Details: Location: Infix2$Second$.equals$extension(Lscala/runtime/BoxedUnit;Ljava/lang/Object;)Z @40: pop Reason: Attempt to pop empty stack. Current Frame: bci: @40 flags: { } locals: { 'Infix2$Second$', 'scala/runtime/BoxedUnit', 'java/lang/Object', 'java/lang/Object', integer } stack: { } Bytecode: 0000000: 2c4e 2dc1 0033 9900 0904 3604 a700 0603 0000010: 3604 1504 9900 4d2c c700 0901 5701 a700 0000020: 102c c000 33b6 0036 57bb 0038 59bf 3a05 0000030: b200 1f57 b200 1fb2 001f 57b2 001f 3a06 0000040: 59c7 000c 5719 06c6 000e a700 0f19 06b6 0000050: 003c 9900 0704 a700 0403 9900 0704 a700 0000060: 0403 ac Stackmap Table: append_frame(@15,Object[#4]) append_frame(@18,Integer) same_frame(@33) same_locals_1_stack_item_frame(@46,Null) full_frame(@77,{Object[#2],Object[#27],Object[#4],Object[#4],Integer,Null,Object[#27]},{Object[#27]}) same_frame(@85) same_frame(@89) same_locals_1_stack_item_frame(@90,Integer) chop_frame(@97,2) same_locals_1_stack_item_frame(@98,Integer) at Infix2$.main(Infix.scala)
8. Перегрузка методов и it
Это, скорее, метод подгадить другим. Представьте, что вы пишите библиотеку, и в ней есть функция
fun applier(x: String, func: (String) -> Unit){
func(x)
}
Разумеется, народ ее использует довольно прозрачным способом:
applier ("arg") {
println(it)
}
applier ("no arg") {
println("ololo")
}
Код компилируется, работает, все довольны. А потом вы добавляете метод
fun applier(x: String, func: () -> Unit){
println("not applying $x")
func()
}
И чтобы компилятор не ругался, пользователям придется везде отказаться от it (читай: переписать кучу кода):
applier ("arg") { it -> //FIXED
println(it)
}
applier ("no arg") { -> //yes, explicit!
println("ololo")
}
Хотя, теоретически, компилятор мог бы и угадать, что если есть it, то это лямбда с 1 входным аргументом. Думаю, что с развитием языка и компилятор поумнеет, и этот пункт — временный.
object Its extends App{
def applier(x: String, func: (String) => Unit){
func(x)
}
def applier(x: String, func: () => Unit){
println("not applying $x")
func()
}
override def main(args: Array[String]) {
applier("arg", println(_))
applier("no arg", _ => println("ololo"))
}
}
9. Почему не стоит думать о Nullable как об Option
Пусть у нас есть обертка для кэша:
class Cache<T>(){
val elements: MutableMap<String, T> = HashMap()
fun put(key: String, elem: T) = elements.put(key, elem)
fun get(key: String) = elements[key]
}
И простой сценарий использования:
val cache = Cache<String>()
cache.put("foo", "bar")
fun getter(key: String) {
cache.get(key)?.let {
println("Got $key from cache: $it")
} ?: println("$key is not in cache!")
}
getter("foo")
getter("baz")
baz is not in cache!
Но если мы вдруг захотим к кэше хранить Nullable...
val cache = Cache<String?>()
cache.put("foo", "bar")
fun getter(key: String) {
cache.get(key)?.let {
println("Got $key from cache: $it")
} ?: println("$key is not in cache!")
}
getter("foo")
getter("baz")
cache.put("IAmNull", null)
getter("IAmNull")
baz is not in cache!
IAmNull is not in cache!
Зачем хранить null? Например, чтобы показать, что результат не вычислим. Конечно, тут было бы правильнее использовать Option или Either, но, к сожалению, ни того, ни другого в стандартной библиотеке нет (но есть, например, в funKTionale). Более того, как раз при реализации Either, я наступил на грабли этого пункта и предыдущего. Решить эту проблему с «двойным Nullable» можно, например, возвратом Pair или специального data class.
object doubleNull extends App{
class Cache[T]{
val elements = mutable.Map.empty[String, T]
def put(key: String, elem: T) = elements.put(key, elem)
def get(key: String) = elements.get(key)
}
override def main(args: Array[String]) {
val cache = new Cache[String]()
cache.put("foo", "bar")
def getter(key: String) {
cache.get(key) match {
case Some(value) => println(s"Got $key from cache: $value")
case None => println(s"$key is not in cache!")
}
}
getter("foo")
getter("baz")
cache.put("IAmNull", null)
getter("IAmNull")
}
baz is not in cache!
Got IAmNull from cache: null
10. Объявление методов
Бонус для тех, кто раньше писал на Scala. Спонсор данного пункта — lgorSL.
Цитирую:
…
Или, например, синтаксис объявления метода:
В scala: def methodName(...) = {...}
В kotlin возможны два варианта — как в scala (со знаком =) и как в java (без него), но эти два способа объявления неэквивалентны друг другу и работают немного по-разному, я однажды кучу времени потратил на поиск такой «особенности» в коде.
…
Я подразумевал следующее:
fun test(){ println(«it works») }
fun test2() = println(«it works too»)
fun test3() = {println(«surprise!»)}
Чтобы вывести «surprise», придётся написать test3()(). Вариант вызова test3() тоже нормально компилируется, только сработает не так, как ожидалось — добавление «лишних» скобочек кардинально меняет логику программы.
Из-за этих граблей переход со скалы на котлин оказался немного болезненным — иногда «по привычке» в объявлении какого-нибудь метода пишу знак равенства, а потом приходится искать ошибки.
Заключение
На этом список наверняка не исчерпывается, поэтому делитесь в комментариях, как вы шли дорогой приключений, но потом что-то пошло не так…
У языка много положительных черт, о которых вы можете прочитать на официальном сайте, в статьях на хабре и еще много где. Но лично я не согласен с некоторыми архитектурными решениями (классы final by default, java interop) и иногда чувствуется, что языку нехватает единообразия, консистентности. Кроме примера с lateinit Int приведу еще два. Внутри блоков let используем it, внутри with — this, а внутри run, который является комбинацией let и this что надо использовать? А у класса String! можно вызвать методы isBlank(), isNotBlank(), isNullOrBlank(), а «дополняющего» метода вроде isNotNullOrBlank нет:( После Scala нехватает некоторых вещей — Option, Either, matching, каррирования. Но в целом язык оставляет приятное впечатление, надеюсь, что он продолжит достойно развиваться.
P.S. Хабровская подсветка Kotlin хромает, надеюсь, что администрация habrahabr это когда-нибудь поправит…
UPD: Выстрелы от комментаторов (буду обновлять)
Неочевидный приоритет оператора elvis. Автор — senia.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (37)
senia
29.02.2016 10:30+3Меня, когда я впервые пробовал пописать на котлине (на дне открытых дверей в Jetbrains), очень сильно удивил приоритет elvis operator:
fun main(args: Array<String>) { val i: Int? = 0 println(2*1 + 1) println(i ?: 0 + 1) }
Попытайтесь угадать результат3
0
MaximChistov
29.02.2016 10:42+1ну в варианте скалы вы обозначили сами приоритет
это как в котлине написать println((i ?: 0) + 1)
nerumb
29.02.2016 10:53+3Все же в парадигме языка приоритет мне кажется вполне правильным..)
senia
29.02.2016 11:58Вполне возможно. Но когда я после a?.method() по аналогии написал a?:0 + 1, я несколько удивился результату.
Да, понятен сценарий применения (например throw MyException...).
И привыкнуть можно, но для меня это было способом выстрелить себе в ногу.
Самое неприятное, что нет возможности этого избежать без дополнительного уровня вложенности скобочек вокруг потенциально длинного выражения с nullable результатом.
NeoCode
29.02.2016 11:08+1А я сижу и не пойму, в чем разница между Elvis operator и Null coalescing operator. Она есть?
NeoCode
29.02.2016 22:21+1Кажется понял, если кому надо:
http://blog.adamcameron.me/2015/07/cfml-elvis-operator-and-null-coalescing.html
Null-coalescing проверяет исключительно на null (нуллабельность), а Elvis — просто на true/false как обычный тернарный оператор.
andreich
29.02.2016 11:37Мне кажется, тут очевидный приоритет элвис оператора, если понимать как он работает с nullable.
По другому просто нельзя, верней можно, но это будет ну очень странно.
nerumb
29.02.2016 10:50+1Final by default действительно очень спорный вопрос. Как по мне, было бы гораздо лучше если бы везде было open by default. По этому поводу на форуме kotlin, как раз идет большой холивар.
Рассуждение авторов языка на тему выбора defaults: статьяov7a
29.02.2016 10:55Эта тема вообще достойна отдельной статьи. Они ссылаются на спорный пункт Effective Java, который, в свою очередь, ссылается на другой спорный пункт Effective Java. При этом в других частях языка Effective Java может не выполняться.
andreich
29.02.2016 11:42+1В общем по пунктам. Согласен только с тремя: 5, 8 и 9.
Остальное работает так, как и должно. Не увидел ничего странного.
Что касается первого пункта, просто не надо так писать. Это код с неочевидным результатом на большинстве языков. От такого всегда стоит избавляться.
Для сравнения приведу паззлеры на груви. Вот там действительно не очевидно.ov7a
29.02.2016 12:12Повторюсь: если инкремент нельзя так использовать, зачем тогда оставлять его как expression? В питоне, например, его убрали как раз из тех соображений, что на нем можно написать что-то неочевидное.
gildor
29.02.2016 11:54+1Совершенно непонятна претензия к инкременту, он абсолютно логично работает, причем идентично Java и Groovy.
Так же странным выглядит упоминание оператора !!, совершенно непонятно как можно выстрелить себе в ногу. Максимум что тут можно ожидать, это статическая проверка на уровне IDE.ov7a
29.02.2016 12:14-1Ну например, вы проверили var i на null, а потом везде пишете i!!. В это время другой тред изменил значение i на null, и вы ловите NPE.
gildor
29.02.2016 12:43+5Если вы именно таким способом используете переменную в разных потоках, то боюсь что вы выстрелили в ногу себе давно и!!! ваша наименьшая проблема
ov7a
29.02.2016 12:52+1Использование этого оператора в любом виде подразумевает, что вы будете стрелять себе в ногу (получением NPE как в примерах или просто плохим кодом, где можно было использовать smart-cast или дополнительную переменную). Опишите хороший кейс для этого оператора, где выстрелить невозможно, если вы считаете, что упоминание этого оператора излишне.
grossws
29.02.2016 13:26Пункт 5 (про инициализацию полей, названный почем-то "Конструктором") работает так же как в яве (где это описано в JLS).
JagaJaga
29.02.2016 18:35-4Прошу Вас, называйте язык так, как хотели разработчики — джава, а не ява. Умоляю. Всем сердцем.
Prototik
29.02.2016 19:44Не совсем — примитивы инициализируются сразу. А вот ссылочные поля да, по порядку, и тоже можно выстрелить.
Throwable
29.02.2016 13:53+6Если это все выстрелы, то все очень даже неплохо. В scala есть один хороший способ выстрелить себе одновременно в ногу, затылок и задницу, и задеть всех, кто стоит рядом. Это implicit. И если его добавить в пункт 5, то будет просто прекрасно.
Вообще во многих случаях scala ведет себя так, что то, что написано — это совсем не то, что имеется ввиду. В лучшем случае, вы смотрите на кусок кода и не понимаете что к чему без знания всего контекста. Так уж закономерность — чем мощнее инструмент, тем легче им отрезать палец.
P.S. в 9-м пункте ключи разные:
cache.put(«IAmNull», null)
getter(«IamNull»)nerumb
29.02.2016 14:30-3Жалко кармы не хватает плюсануть… полностью согласен с автором!
п.с. вообще возникает ощущение что "последователи scala" не спят, и только и делают что ищут отрицательные комментарии в сторону своего детища, чтобы потом их безжалостно топить…grossws
29.02.2016 16:23Может дело просто в том, что многие из этих комментариев просто не по существу?
ov7a
29.02.2016 21:34+1Аналог implicit здесь — extension method, если вы про implicit class. Если вы про что-то другое — можно пример?
Насчет закономерности мощность/возможность выстрелить я в целом согласен, хотя приведу два контр примера — C и Haskell.
9 пункт поправил, спасибо.Throwable
01.03.2016 13:21+1В Scala есть еще implicit convertions и implicit parameters. При помощи них можно придумать много чего самострельного. Хуже то, что для этого не нужно что-то изобретать — наступить эти на грабли можно даже не специально. Если Вам нужны примеры, зайдите на StackOverflow и поищите по «scala implicit» и среди 5800 результатов посмотрите какие у людей с ним проблемы. Кроме всего прочего, такой инструмент при неумелом использовании пораждает различные bad practice. А для умелого использования не хватает четко обозначенных use cases, где использовать это необходимо или разумно.
Не понял на счет контрпримеров. C — это мощный, сложный, но слаботипизированный язык (как говорил мой препод — язык среднего уровня), утыканный граблями. Haskell — наоборот, простой язык с четкой идеей.ov7a
01.03.2016 13:36+3Насчет implicit conversions — согласен (для тех, кому лень гуглить — вот пример). Насчет разумного — более-менее адекватно выглядит implicit conversion арифметики, например Int в Long, Double в MySuperDuperComplex и т.п. Кстати, в котлине придется явно указать преобразование.
Насчет мощности — мы, наверно, по-разному понимаем ее. В моем понимании мощность близка к выразительности и набору возможностей. И при таком раскладе — С не мощный, выстрелить элементарно, Haskel — мощный, выстрелить трудно.
nwalker
29.02.2016 14:40+1>> lateinit
>> lateinit var без аннотации
Вы просто не поняли, зачем нужен lateinit.
Для того, что у вас написано, действительно нужны Delegates.nonNull или nullable var.
>> Почему не стоит думать о Nullable как об Option
А почему вы решили, что стоит?..
>> внутри run… что надо использовать?
Из сигнатуры же очевидно, что this.ov7a
29.02.2016 21:38Хорошо, а для чего его тогда, по-вашему, надо использовать? Цитирую документацию:
Normally, properties declared as having a non-null type must be initialized in the constructor. However, fairly often this is not convenient. For example, properties can be initialized through dependency injection, or in the setup method of a unit test. In this case, you cannot supply a non-null initializer in the constructor, but you still want to avoid null checks when referencing the property inside the body of a class.
To handle this case, you can mark the property with the lateinit modifier
Почему многие подумают, что Nullable — это замена Option, я написал в начале статьи.
Сигнатуру я могу прочитать. Вот только если бы этот момент был бы очевидным, читать бы сигнатуру не пришлось.nwalker
29.02.2016 22:12-1Исключительно для того, что сказано в документации, IMHO, в первую очередь для injected-объектов. С другими кейсами я лично не сталкивался, может быть, тесты — но их я еще не писал.
IMHO, lateinit это практически костыль для ограниченного набора случаев, и не стоит его использовать, когда можно обойтись чем-то другим.ov7a
29.02.2016 22:31+1В документации прямо говорится, что это костыль, чтобы избежать лишних проверок. Вы можете некорректно сделать инъекцию, забыть вызвать setup метод — на 100% быть уверенным в том, что поле будет проинициализировано, нельзя. В статье я написал, что его вообще использовать не стоит.
nwalker
01.03.2016 14:25>> некорректно сделать инъекцию
>> забыть вызвать setup метод
И что, это штатная ситуация, которая должна быть обработана на уровне отдельного объекта? Конечно нет, это ваша ошибка, которую вам нужно как можно скорее исправить. Значит, использование nullable, которое вы предлагаете в посте совершенно некорректно. Зачем лепить проверки на null на поле, которое в нормальном состоянии всегда проинициализировано?
Для таких случаев нужно использовать Delegates.notNull или lateinit — в зависимости от прочих условий. Они следят за выполнением контракта «доступ к полю до инициализации — исключительная ситуация». Дальше вы это исключение обрабатываете или нет, это отдельный вопрос.
Почему же тогда я назвал lateinit «практически костылем»? Потому что он дублирует на уровне языка функциональность, которая реализована в стандартной библиотеке как delegated property Delegates.notNull.
Зачем он введен? Затем, что у delegated property в общем случае нет backing field, на который может потребоваться применить аннотацию, скажем, инжектора.
То есть, lateinit это элемент совместимости совместимости с деталями платформы, которые отсутствуют в языке в явном виде. То есть, практически костыль.
ov7a
01.03.2016 10:38От рид-онли nicky1038:
Что мне ещё не понравилось (это не к выстрелам в ногу относится) — что нет clone/copy методов у коллекций
А то, вроде, несколько строк только написал, а уже забомбило.
youtrack.jetbrains.com/issue/KT-11221
NeoCode
А в чем прикол с постинкрементом в самом первом примере?
Если он "пост" инкремент, то должен выполняться ПОСЛЕ вычисления всего выражения выражения, после "точки с запятой" которой в этом языке нет. Сначала i = 5+5, т.е.10, затем еще два раза ++, то есть 12. Почему 11?
senia
Так нагляднее:
"После" — это после вычисления аргумента функции sum. Вычислили первый аргумент, добавили 1 к i, вычислили второй, добавили 1 к i, вычислили sum, присвоили значение в i.
С оператором точно то же самое.
ov7a
Вот байт-код
Прочитали 5, увеличили на 1, прочитали 6, увеличили его на 1, сложили 5 и 6, записали туда, где было 7. Упс, не успел.