Kotlin 初窥门径[1]:基础概念
Kotlin 是由 JetBrains 开发的基于JVM的语言。JetBrains 因为创造了一个强大的Java开发 IDE(Intellij) 而被大家所熟知。Android Studio 就是基于Intellij IDEA 的开源社区版,今年的I/O大会上谷歌宣布 Kotlin 正式成为 Android 的官方语言。
Kotlin 是使用 Java 开发者的思维被创建的,相比于 JAVA 具有如下优势:
- 简洁,通过支持 variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation 等实现,你可以编写少得多的代码。
- 安全,在我们编译时期就处理了各种null的情况,避免了执行时异常。
- 函数式,Kotlin是基于面向对象的语言,但它使用了很多函数式编程的概念,如,使用lambda表达式来更方便地解决问题。
- 完美兼容JAVA,可以继续使用所有的你用Java写的代码和库,甚至可以在一个项目中使用Kotlin和Java两种语言混合编程。
包
为了方便管理大型软件系统中数目众多的类,解决类的命名冲突问题,类似于Java,Kotlin同样引入包(package)机制,提供类的多重类名空间。
声明包
package语句必须是文件中的第一行非注释的程序代码。
package foo.bar
fun doSomething(){} // 定义的函数
class User(){} //定义的类
源文件的所有内容(比如类和函数)都被包声明包括。 因此在上面的例子中,doSomething() 的全名应该是 foo.bar.doSomething , User 的全名是 foo.bar.User 。
如果没有任何包声明的话,则当中的代码都属于默认包,导入时包名即为函数名!
导入包
导入一个单独的名字
import foo.Bar
导入范围内的所有可用的内容 (包, 类, 对象, 等等):
import foo.*
如果将两个含有相同名称的的类库以 "*" 同时导入,将会存在潜在的冲突。例如:
import net.mindview.simple.*
import java.until.*
由于java.util.和net.mindview.simple.都包含Vector类。在创建一个Vector类时,编译器并不知道调用哪个类库的类,从而会报错误信息。
而此时就是 Kotlin 中强大的 as
关键字大显神威的时候了,此关键字用于局部重命名,以解决冲突。
import net.mindview.simple.Vecotr
import java.until.Vecotr as aVector
val v : Vecotr = Vector()
val vA : aVector = aVector()
import 关键字不局限于导入类,您也可以使用它来导入其他声明:
- 顶级函数与属性
- 在对象声明中声明的函数和属性
- 枚举常量
类
Kotlin中的类遵循一个简单的结构。尽管与Java有一点细微的差别。你可以使用 try.kotlinlang.org 在不需要一个真正的项目和不需要部署到机器的前提下来测试一些简单的代码范例。
定义一个类
如果你想定义一个类,你只需要使用 class
关键字。
class Person {
}
它具有一个默认唯一的无参构造函数,如果我们想使用有参的参构造函数,只需要在类名后面写上它的参数:
class Person (name: String, var age: Int) {
init {
// init
}
}
而 init
方法是构造函数的函数体,可以用来执行一些初始化的工作。
在上面的代码时,我们并没有使用
;
结尾,当然你也可以使用分号结尾,但这不是必须的,官方也建议不使用分号,这是一个很好的实践,可以节约你很多时间。
访问性修饰符
Kotlin中这些修饰符是与C#中的使用是有些不同的。在 kotlin 中默认的修饰符是 public ,这节约了很多的时间和字符。而 kotlin 中具有以下几种访问性修饰符:
-
private 表示它只能被自己所的在文件可见,我们就不能在定义这个类之外的文件中使用它(但在同一个文件中的其它类可以访问,与C#中不同)。
-
protected 这个修饰符只能被用在类或者接口中的成员上,一个包成员不能被定义为 protected 。它可以被成员自己和继承它的成员可见(比如,类和它的子类)。
-
internal 如果是一个定义为 internal 的包成员的话,对所在的整个 module 可见。如果它是一个其它领域的成员,它就需要依赖那个领域的可见性了。比如,如果我们写了一个 private 类,那么它的 internal 修饰的函数的可见性就会限制与它所在的这个类的可见性。
-
public 这是默认的修饰符,成员在任何地方被修饰为 public ,很明显它只限制于它的领域。一个定义为 public 的成员被包含在一个 private 修饰的类中,这个成员在这个类以外也是不可见的。
什么是 module ?
根据Jetbrains的定义,一个 module 应该是一个单独的功能性的单位,它应该是可以被单独编译、运行、测试、debug的。根据我们项目不同的模块,可以在Android Studio中创建不同的 module 。在Eclipse中,这些 module 可以认为是在一个 workspace 中的不同的 project 。
构造函数
在 Kotlin 中类可以有一个主构造函数以及多个二级构造函数。主构造函数是类头的一部分:跟在类名后面(可以有可选的类型参数)。
class Person constructor(name: String) {
}
如果主构造函数没有注解或可见性说明,则 constructor 关键字可以省略:
class Person(name: String){
}
所有构造函数默认都是 public 的,它们的类是可见的,可以被其它地方使用,我们也可以把构造函数修改为 private :
class Person private constructor(name: String) {
}
同样,也可以在构造函数中声明属性的访问级别:
class A private constructor(private var a: Int) {
}
在构造函数中声明的参数,如果没有使用 var 修饰时,则表示为局部变量,而使用 var 时则是类的属性。
类也可以有二级构造函数,需要加前缀 constructor:
class Person(var name: String){
constructor(name: String, age: Int) : this(name) {
}
}
需要注意的时候的是,如果主构造函数带有参数,则二级构造函数必须显式的调用主构造函数。
创建一个实例
我们可以像使用普通函数那样使用构造函数创建类实例:
var persion1 = Persion("aaa");
var persion2 = Persion("aaa",10);
在 Kotlin 中并没有
new
关键字。
继承
默认任何类都是继承自 Any
,与 C# 中的 Object 类似。而在 Kotlin 中,所有的类默认是不可继承的,只有那些声明为 open
或者 abstract
的类才可以被继承:
open class Person(name: String, age: Int) {
}
class Student(name: String, age: Int, score: Int) : Persion(name, age) {
}
枚举
Kotlin也提供了枚举( enums )的实现:
enum class Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY }
枚举可以引用参数:
enum class Icon(val res: Int) {
UP(R.drawable.ic_up),
SEARCH(R.drawable.ic_search),
CAST(R.drawable.ic_cast)
}
val searchIconRes = Icon.SEARCH.res
枚举可以通过 String 匹配名字来获取,我们也可以获取包含所有枚举的 Array ,所以我们可以遍历它。
val search: Icon = Icon.valueOf("SEARCH")
val iconList: Array<Icon> = Icon.values()
而且每一个枚举都有一些函数来获取它的名字、声明的位置:
val searchName: String = Icon.SEARCH.name()
val searchPosition: Int = Icon.SEARCH.ordinal()
枚举还根据它的顺序实现了 Comparable 接口,所以可以很方便地把它们进行排序。
变量和属性
在 Kotlin 中,一切都是对象。没有像C#中那样的原始基本类型。这个是非常有用的,因为我们可以使用一致的方式来处理所有的可用的类型。
基本类型
当然,像integer,float或者boolean等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与C#非常相似的,但也有一些不同:
- 数字类型中不会自动转型。比如,你不能给 Double 变量分配一个 Int 。必须要做一个明确的类型转换
toDouble()
。 - 字符(Char)不能直接作为一个数字来处理。我们需要把他们转换为一个数字
'a'.toInt()
。 - 位运算也有一点不同,在C#中我们经常使用
|
和&
,但在 Kotlin 中则使用or
和and
关键字。
变量
变量分为 可变(var
) 和不可变(val
) 两种,val
类似于C#中的readonly。
一个不可变对象意味着它在实例化之后就不能再去改变它的状态了,如果大部分的对象都是可变的,那就意味着任何可以访问它这个对象的代码都可以去修改它,从而影响整个程序的其它地方。
不可变对象也可以说是线程安全的,因为它们无法去改变,也不需要去定义访问控制,因为所有线程访问到的对象都是同一个。
而变量的声明和Typescript
非常相似:
var a = "aaaaaaaaaa"
var b: String = "bbbbbbbbbb"
val c: = 10
当我们在声明中就为变量赋值时可以不用指定类型,因为 kotlin 会自动推断出它的类型,这样可以让代码更加清晰和快速修改。
属性
属性做的事情就是为字段加上 getter 和 setter,在C#中我们可以使用 public string a { get; set; }
的方式来声明一个属性,而在 kotlin 中,一个变量就是一个拥有默认getter和setter的属性,我们也可以自定义getter和setter:
public class Person {
var name: String = "zs"
get() = field.toUpperCase()
set(value) {
field = value
}
}
函数
定义
函数使用 fun
关键字来定义:
fun test(x: Int) {
}
如果没有指定返回值,则会返回一个 kotlin.Unit
,与C#中的 void
类似,但 kotlin.Unit
是一个真正的对象。而指定返回值也很简单:
fun test(x: Int) : Boolean {
return x > 0
}
而上面的代码还有一种更简单的方式,类似C#中的表达式方法体:
fun test(x: Int) : Boolean = x > 0
参数默认值
参数默认值在C#中比较常见,而在 Kotlin 中也支持了参数默认值:
fun test(x: Int, y: String = "default value") {
}
我们为第二个参数设置了一个默认值,这样,在调用的时候便可以不传第二个参数:
test(1)
test(1,"yyyyyyyy")
扩展函数
扩展函数数是指在一个类上增加一种新的行为,甚至我们没有这个类代码的访问权限。这是一个在缺少有用函数的类上扩展的方法。
在C#中,我们可以使用一个静态类中的静态方法,把要扩展的类型使用this修饰,做为第一个参数传进来,而 kotlin 中,更加简洁:
class Persion {
fun test1() {
}
}
class MyClass {
// 扩展Persion类
fun Persion.test2() {
}
fun test() {
var persion = Persion()
persion.test1()
// 调用扩展方法
persion.test2()
}
}
就像定义一个普通函数一样,只需要把函数名定义为要扩展的类型加上要扩展方法即可。
而扩展函数也可以是一个属性,可以通过相似的方法来扩展属性:
public var Persion.name: String
get() = getName()
set(v) = setName(v)
数据类
数据类是一种非常强大的类,它可以让我们避免创建C#中用于保存状态但又操作非常简单的POCO类模版代码。它们通常只提供了用于访问它们属性的简单的getter和setter。定义一个新的数据类非常简单:
data class UserDto(val name: String, val age: Int, val email: String, var date: Date)
额外的函数
通过数据类,我们可以方便地得到一些非常有用的函数:
- equals(): 它可以比较两个对象的属性来确保他们是相同的。
- hashCode(): 我们可以得到一个hash值,也是从属性中计算出来的。
- copy(): 你可以拷贝一个对象,可以根据你的需要去修改里面的属性。
- 一系列可以映射对象到变量中的函数。
映射对象到变量中
映射对象的每一个属性到一个变量中,这个过程是非常枯燥的。而在 kotlin 中则非常简洁:
var user = UserDto('zs', 18, Date())
val (name, age, email) = user
上面这个多声明会被编译成下面的代码:
val name = user.component1()
val age = user.component2()
val email = user.component3()
在主构造函数中有多少个参数,就会依次生成对应的component1,component2,component3……这些函数返回的就是对应字段的值。
这个特性背后的逻辑是非常强大的,它可以在很多情况下帮助我们简化代码。举个例子, Map 类含有一些扩展函数的实现,允许它在迭代时使用key和value:
for ((key, value) in map) {
Log.d("map", "key:$key, value:$value")
}