科特林——好、坏、丑
优点
坏与丑
判决
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
大家好!
这篇文章我一直想写很久。我对Kotlin已经相当了解,也曾在生产环境中使用过它。Kotlin 是一种基于 JVM 的语言,由 JetBrains 开发,这家公司以其 IDE 和开发者工具而闻名。从语言的角度来看,Kotlin 非常有趣,它有很多优点,但也存在一些缺陷和不太合理的设计选择。让我们直接进入正题吧!
优点
关于我喜欢 Kotlin 的理由,我能写好几本书。以下是一些亮点概述。
分号是可选的
不是那种“大多数情况下不用它们也能运行,只是偶尔会有所不同”的“可选”功能,而是“用不用它们,程序都不会改变”的功能。代码更简洁,我的手腕也因此感觉轻松多了。
NULL 是一种单独的类型
String在 Kotlin 中,非空字符串(non- nullString) 和String?可空字符串 (nullable String) 是两种不同的类型。如果你遵循 Kotlin 的方法论,你的程序就不会出现令人头疼的空NullPointerException字符串错误。
扩展功能
你是否曾经想要在 Java 中null使用安全的方法myStringVariable.isNullOrEmpty()?Kotlin 可以满足你的需求!你可以编写静态方法(具有特定的签名),并像调用普通成员方法一样调用它们。这也标志着令人头疼的、充斥着静态方法的类的终结StringUtils。现在,所有这些方法都可以是扩展方法了!
流程类型和类型推断
对于所有局部变量,你只需使用 ` var__type__`,编译器就会自动推断其类型。Java 10 也支持这种特性,但 Kotlin 更强大的类型系统使其更具价值。Kotlin 也不需要强制类型转换运算符。如果你检查一个对象是否属于instanceOf某个类,那么在该if代码块内,变量将被视为该类,无需手动进行向下转型。Kotlin 将其称为“智能类型转换”。
轻松完成决赛
在 Java 中,final必须使用 `final` 修饰符才能使局部变量不可赋值。这是一个非常有用的特性,因为它避免了很多潜在的错误。然而,`final` 修饰符final String myString非常冗长,几乎没人愿意使用它。在 Kotlin 中,`final` 变量和非 `final` 变量的字符数完全相同:var myString可变变量和val myString不可变变量的字符数相同。
智能标准库
Kotlin 标准库是一个充满惊喜的宝库。你是否曾经因为 Java 中没有优雅的方式来使用锁而感到困扰try-with-resources?在 Kotlin 中,你可以使用lock.withLock{ doWork() }`undefined`。这虽然是个小功能,但确实非常实用。你是否经常忘记在 ` finallyundefined` 语句中释放锁?或者误解锁了读锁而不是写锁?Kotlin 标准库中充满了这类实用的小工具。
这里还有一个例子:mutableListOf<T>(T... elments)Kotlin 编译器足够智能,如果你提供了元素,它就能自动推断出来T。因此,对这个方法的调用将有两种形式:
// empty list, need to specify the type argument explicitly
val myList = mutableListOf<String>()
// pre-filled list, no need for type argument
val yourList = mutableListOf("Hello", "World"!)
我们不妨停下来片刻,欣赏一下这两段代码在两种情况下都如同散文般流畅易读。此外,Kotlin 能够区分可变集合和只读集合,同时仍然与 Java 兼容。这本身就是一项了不起的成就,也使得 Kotlin 代码能够更好地抵御一些不易察觉的错误(比如,谁修改了这个集合?!)。
流,精简
如果你想List在 Java 中过滤数据,你需要这样做:
List<String> newList = oldList.stream()
.filter( s -> s.startsWith("a") )
.collect(Collectors.toList());
如果我们真正想做的只是:
List<String> newList = oldList.filter(s -> s.startsWith("a"));
那么使用 `Stream` 的意义何在呢stream()?通过将过滤器(以及映射操作等等)包裹在 `Stream`stream()和`Stream` 中collect(...),Java 流可以实现惰性求值。然而,你可能会认为,如果你只想进行一次过滤器操作,那么惰性求值就毫无意义。你的说法没错,对于一次简单的过滤器操作来说,使用流无论从语法还是概念上来说都是过度设计。但在 Java 中,`Stream`List#filter(...)本身并不存在(你可以使用静态工具类,但这和使用流完全不同)。
Kotlin 以一种非常优雅的方式解决了这个问题:
// single filter, quick and easy
val newList = oldList.filter { it.startsWith("a") }
// streams for multiple filters (same syntax!)
val newList = oldList.asSequence().filter { it.startsWith("a") }.toList()
所以,Kotlin既提供了基于流的惰性链(称为序列),也提供了一次性操作。它最棒的地方在于,对filter列表执行操作的语法与对序列执行操作的语法完全相同filter。如果你在修改现有代码时发现需要添加另一个过滤器,只需在过滤器前后插入相应的语句asSequence()即可toList()使其惰性化,而保留原有的过滤器。
万岁!Iterator
迭代器是一个非常基础且强大的概念。它们从 Java 1.0 版本就存在了(这足以说明问题)。然而,Java 对迭代器的待遇却相当低下。你不能像对数组for那样对迭代器进行循环Collection。Collection#addAll它只接受其他Collection类型的参数,不接受Iterable数组本身。甚至连 `__init__` 都不能用iterator.stream()。我一直不明白为什么。
Kotlin 解决了上述所有问题。在 Kotlin 中处理迭代器与处理集合一样便捷,同时又保留了迭代器的优势:
for(element in iterator) { ... } /* works! */
iterator.asSequence().filter { ... } /* works! */
myList.addAll(iterable) /* works! */
Java 互操作
……真的很好。是“真的非常好”。你可以在同一个项目中混合使用 Kotlin 和 Java 的源代码(以及二进制文件!),而不会出现任何问题。不过,也存在一些陷阱;我们稍后会详细介绍。
多平台,同一种语言
Kotlin 可以编译成 JVM 字节码以及 JavaScript。原生实现也在开发中。这无疑是一件意义重大的事情。
坏与丑
Kotlin 虽然优点众多,但也存在一些缺点。这些缺点虽然算不上致命缺陷,但仍然值得注意。
无static修饰符
在 Kotlin 中,没有static`__init__` 关键字。如果您只想定义一个实用方法,这会有些麻烦。您需要这样做:
class MyClass {
companion object {
@JvmStatic
fun myStaticMethod(/*...*/) { /*...*/ }
}
}
这很糟糕。平心而论,Kotlin 有const常量(大致相当于public static finalJava 中的成员),而且你可以在任何类之外声明顶级函数(这实际上使它们成为静态方法static)。但如果你真的想要一个静态方法,那么上面提到的方法是唯一的选择(据我所知)。
最初引入伴生companion object对象是为了简化单例的创建。伴生对象是它所在类的单例实例。我认为这是一个糟糕的决定。首先,静态方法很有用,不应该需要如此复杂的语法;其次,单例模式本身并不冗长到需要这种简写,而且它的实用性也不足以支撑如此重要的语言特性。
关键词open
Kotlin 中的所有类默认都是…… final。好好想想这句话。
class MyClass {
}
class SubClass : MyClass { /* NOPE: compile error! MyClass is final! */
}
这只是冰山一角。想象一下像 Spring 或 Hibernate 这样的框架,它们会在运行时生成扩展你类的字节码。不行,不能这么做——类是私有的final。转眼间,你所有的 Kotlin 类就突然与几乎所有常用框架都不兼容了。有两种方法可以绕过这个问题:
- 要么将你的类声明为
open class MyClass或 - 为 Kotlin 编译器添加编译器插件。
两种方案都不太理想。Kotlin 语言开发者的逻辑是,可扩展性应该是一个可选功能。只有当你的类注定要被扩展时,它才应该允许被扩展。在我看来,这种逻辑是有缺陷的:防御性编程的最佳实践会告诉你,你应该让你的代码能够在最意想不到的环境下运行。如果你编写了一个在被继承时可能会出错的类,那么你就写了一个糟糕的类。Kotlin 的做法恰恰相反,它简单地将一切都封闭起来,从而提供了完全错误的激励。
建造者们
Kotlin 语言对构造函数有着一种非常奇特的偏爱。请看:
// primary constructor
class PersonA(val firstName: String, val lastName: String) { }
// regular constructor
class PersonB {
val firstName: String
val lastName: String
constructor(firstName: String, lastName: String){
this.firstName = firstName
this.lastName = lastName
}
}
以上两个例子做的是完全相同的事情。你可能会认为使用“主构造函数”的版本更简洁,因此更好。但我不同意,原因如下:
- 主构造函数必须始终列出(并赋值)所有字段。当需要为本地缓存添加一个额外的瞬态字段时,这会非常麻烦。
- 类声明行变得非常长。
- 如果你继承自一个具有主构造函数的基类,那么……
- 必须自行声明一个主构造函数。
- 必须调用基类的主构造函数
- 必须在类声明中完成所有这些操作
总之,一旦你被锁定在主构造函数工作流程中,就无法摆脱它了。你的类声明会迅速变得非常庞大。想想看:
class Student(val firstName: String, val lastName: String, val studentId: String) : Person(firstName,lastName) {
}
当然,这样的代码行用途很广。它声明了一个子类,声明了字段,声明了构造函数,说明了基类,最后还说明了继承字段的填充方式。信息密度非常高。但我仍然不喜欢这种写法。信息太多太杂,对人来说很难理解。我不建议使用主构造函数(除非是数据类,实在没办法),但有时由于标准库中某些类的限制,你不得不使用它们。
数据类——一个绝妙的概念,但执行得却差强人意。
数据类本身就是一个绝妙的想法。请看:
data class Person(val firstName: String, val lastName: String)
您指定类的字段及其类型,即可得到:
- 指定字段
- getter/setter
- 一个构造函数,它的功能完全符合你的预期。
- 哈希码
- 等于
- 克隆
- 各种公用事业
……这一切都是免费的,无需编写任何代码。这意味着你的代码hashCode()永远不会与你的文档不同步equals()。你toString()永远不会忘记打印你最近添加到类中的字段。这一切之所以成为可能,是因为这些内容没有文本表示;编译器直接生成字节码。这是一个非常酷的概念,远胜于通过 IDE 生成上述所有内容(因为生成的代码很容易不同步)。
问题在于,直到最后才经过深思熟虑。主要问题在于,编写数据类时不能使用继承。数据类不能继承任何基类,它们final本身也是(没错,这里也不能使用open)。做出这个决定的理由是,尝试引入继承可能会导致一些问题(尤其是在克隆工具中)。在我看来,这完全是无稽之谈。UML 和 Ecore 都已经展示了如何正确地使用多重继承和克隆工具来编写数据类。
数据类缺乏继承选项极大地限制了其应用范围。即使是简单的数据传输对象 (DTO) 或 JPA 实体(数据类在这些情况下非常有用!),通常也需要继承。
关键词丰富
Kotlin 拥有非常庞大的关键字和运算符集合。Kotlin(和 C# 一样)引入了“软关键字”的概念,这意味着某些词仅在特定上下文中被视为关键字,但在代码的其他所有地方都可以用作标识符(例如变量名)。其中一些关键字相当晦涩难懂,而且非常具体,例如 `@` crossinline。无论如何,这使得 Kotlin 语言学习起来相当困难,精通起来更是难上加难,因为你必须掌握所有这些关键字。在日常使用中,你需要掌握的关键字数量与 Java 差不多。但如果你想阅读和理解库函数,你会经常遇到它们。我个人认为,很多关键字如果用注解来表达会更好(例如,@Actual用 `@` 代替 `@` 修饰符actual)。
判决
我强烈建议所有 Java 程序员至少仔细了解一下 Kotlin。在很多方面,它都远胜于 Java。它继承了 Java 的所有基础架构和库,并在此基础上添加了许多巧妙的设计。它拥有更优秀的类型系统,从而增强了安全性和编译时错误检查。仅凭其null默认安全这一特性,就足以证明 Kotlin 优于 Java 的合理性。然而,它并非完美无缺。Kotlin 让编写简洁明了的代码变得更加容易,但它也可能被用来生成看似随机的字符组合,让人两天后都无法理解。尽管存在一些缺点、不足以及偶尔出现的奇怪设计,它仍然是一门优秀的语言。
您在使用这门语言方面有什么经验可以分享吗?您是否在生产环境中使用它,并且对它的表现满意?请在下方留言。
文章来源:https://dev.to/martinhaeusler/kotlin---the-good-the-bad-and-the-ugly-3jfo