全民 Kotlin:你没有玩过的全新玩法

本文章已授权鸿洋微信公众号转载

目录

  • 空安全

  • 方法支持添加默认参数

  • 方法上面的参数不可变

  • 类方法扩展

  • 函数变量

  • 内联函数

  • 委托机制

    • 类委托

    • 属性委托

    • 懒委托

  • 高阶函数

    • let 函数

    • with 函数

    • run 函数

    • apply 函数

    • also 函数

  • 运算符重载

空安全

  • 在 Java 不用强制我们处理空对象,所以常常会导致 NullPointerException 空指针出现,现在 Kotlin 对空对象进行了限定,必须在编译时处理对象是否为空的情况,不然会导致编译不通过

  • 在对象不可空的情况下,可以直接使用这个对象

fun getText() : String {
    return "text"
}
val text = getText()
print(text.length)
  • 在对象可空的情况下,必须要判断对象是否为空
fun getText() : String? {
    return null
}
val text = getText()
if (text != null) {
    print(text.length)
}
// 如果不想判断是否为空,可以直接这样,如果 text 对象为空,则会报空指针异常,一般情况下不推荐这样使用
val text = getText()
print(text!!.length)
// 还有一种更好的处理方式,如果 text 对象为空则不会报错,但是 text.length 的结果会等于 null
val text = getText()
print(text?.length)

方法支持添加默认参数

  • 在 Java 方法上,我们可能会为了扩展某个方法而进行多次重载
public void toast(String text) {
    toast(this, text, Toast.LENGTH_SHORT);
}

public void toast(Context context, String text) {
    toast(context, text, Toast.LENGTH_SHORT);
}

public void toast(Context context, String text, int time) {
    Toast.makeText(context, text, time).show();
}
toast("弹个吐司");
toast(this, "弹个吐司");
toast(this, "弹个吐司", Toast.LENGTH_LONG);
  • 但是在 Kotlin 上面,我们无需进行重载,可以直接在方法上面直接定义参数的默认值
fun toast(context : Context = this, text : String, time : Int = Toast.LENGTH_SHORT) {
    Toast.makeText(context, text, time).show()
}
toast(text = "弹个吐司")
toast(this, "弹个吐司")
toast(this, "弹个吐司", Toast.LENGTH_LONG)

方法上面的参数不可变

  • 在 Java 方法上面,我们可以随意修改方法上面参数的赋值,但是到了 Kotlin 这里是不行的,Kotlin 方法参数上面的变量是 val (对应 Java 的 final)类型的,那么这个时候我们有两种解决方案:

  • 第一种,在方法里面定义一个一模一样的变量,具体写法如下:

class XxxView : View {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var widthMeasureSpec: Int = widthMeasureSpec
        var heightMeasureSpec: Int = heightMeasureSpec
        
        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }

        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }
        
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
    }
}
  • 但是编译器会报警告,提示我们出现了重复变量,但是仍可正常编译和运行,所以不推荐这种写法

  • 第二种,在方法里面定义一个不同名称的变量,具体写法如下:

class XxxView : View {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var finalWidthMeasureSpec: Int = widthMeasureSpec
        var finalHeightMeasureSpec: Int = heightMeasureSpec

        if (MeasureSpec.getMode(finalWidthMeasureSpec) == MeasureSpec.AT_MOST) {
            finalWidthMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }

        if (MeasureSpec.getMode(finalHeightMeasureSpec) == MeasureSpec.AT_MOST) {
            finalHeightMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }
        
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
    }
}
  • 其实就在原来的基础上加一个 final 前缀,这样不仅解决了编译器警告的问题,还解决了我们还要重新想一个名称来给变量命名的烦恼。

  • 那么肯定有人会问了,有没有办法像 Java 一样改呢?关于这个问题我也纠结了一阵子,但是查阅了很多文档和资料,最终发现并没有办法,所以只能妥协了,毕竟这个世界上没有什么事物是完美的。

类方法扩展

  • 可以在不用继承的情况下对扩展原有类的方法,例如对 String 类进行扩展方法
fun String.handle() : String {
    return this + "Android轮子哥"
}
// 需要注意,handle 方法在哪个类中被定义,这种扩展只能在那个类里面才能使用
print("HJQ = ".handle())
HJQ = Android轮子哥

函数变量

  • 在 Kotlin 语法中函数是可以作为变量进行传递的
var result = fun(number1 : Int, number2 : Int) : Int {
    return number1 + number2
}
  • 使用这个函数变量
println(result(1, 2))

内联函数

  • 有人可能会问了,内联函数是虾米?我举个栗子,用 Kotlin 编写以下代码
class Demo {

    fun test() {
        showToast("666666")
    }

    /**
     * 这个就是我们今天的主角:内联函数了,用 inline 关键字来修饰
     */
    private inline fun showToast(message: String) {
        ToastUtils.show(message)
    }
}
  • 经过反编译之后,会变成以下代码:
/* compiled from: Demo.kt */
public final class Demo {

    public final void test() {
        ToastUtils.show("666666");
    }
}
  • 看到这里相信大家应该知道内联函数的用法和作用了,内联函数就是在编译的时候将所有调用 inline 函数的代码直接替换成方法里面的代码,那么大家可能有疑问了,这样做有什么实际好处呢?它其实提升了代码的性能,这跟基本数据类型的常量会在编译的过程中被优化一样,但是如果 inline 函数被许多处地方调用,并且 inline 函数的实现代码比较多的情况下,也会相应导致代码量增加。

  • 另外上面的代码示例中,编译器在 inline 关键字上面有一个代码警告,原话是这样的:

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

内联对性能的预期影响是微不足道的。内联最适用于参数为函数类型的函数

  • 大致的意思是,上面的代码示例中,内联函数能起到的性能优化是微不足道的,它比较适合带有 lambda 参数的函数,根据这个提示,将上面的代码示例修改成下面这样就不会报代码警告了:
class Demo {

    fun test() {
    
        showToast({
            println("测试输出了")
        }, "7777777")
    }

    private inline fun showToast(function: () -> Unit, message: String) {
        function.invoke()
        ToastUtils.show(message)
    }
}
  • 有人可能会好奇了,这样就能有很大的性能提升?有什么判断依据呢?接下来让我们做一组实验,加 inline 和不加 inline 反编译出来的代码有什么区别,先来看加 inline 之后反编译出来的代码长啥样
/* compiled from: Demo.kt */
public final class Demo {

    public final void test() {
        System.out.println("\u6d4b\u8bd5\u8f93\u51fa\u4e86");
        ToastUtils.show("7777777");
    }
}
  • 一切都在预料之中,那么不加 inline 反编译出来又是什么效果呢?
/* compiled from: Demo.kt */
public final class Demo {

    public final void test() {
        showToast(1.INSTANCE, "7777777");
    }

    private final void showToast(Function0<Unit> function, String message) {
        function.invoke();
        ToastUtils.show(message);
    }
}
/* compiled from: Demo.kt */
final class Demo$test$1 extends Lambda implements Function0<Unit> {
    public static final Demo$test$1 INSTANCE = new Demo$test$1();

    Demo$test$1() {
        super(0);
    }

    public final void invoke() {
        System.out.println("\u6d4b\u8bd5\u8f93\u51fa\u4e86");
    }
}
  • 很明显,不加 inline 会导致多生成一个内部类,这个是 lambda 函数多出来的类,并且里面的示例还是静态,这无疑会增加内存消耗,另外这样还有另外一个好处,就是能少一层方法栈的调用。

  • 除了 inline (内联)这个关键字,还有另外一个关键字:noinline(禁止内联),大家可能到这里就摸不着头脑了,这个有啥用?我不在方法上面写 inline 不就是不会内联了么?那么这个关键字是有什么作用呢?其实这个关键字不是修饰在方法上面的,而是修饰 在 lambda 参数上面的,假设一个 inline 函数上面有多个 lambda 参数,那么我只想对某个 lambda 参数内联,其他 lambda 参数不内联的情况下,就可以使用这个关键字来对不需要进行内联的 lambda 参数进行修饰,大体用法如下:

private inline fun showToast(function1: () -> Unit, noinline function2: () -> Unit, message: String) {
    function1.invoke()
    function2.invoke()
    ToastUtils.show(message)
}

密封类

  • 大家看到这个词语的时候,第一反应是,密封类是啥子?大家应该用过枚举吧?先说枚举类的几个弊端,第一个枚举值赋值是固定的(一旦赋值之后就不可变),第二个枚举值的类型是固定的(类型只能是自己),密封类的出现正是为了解决这两个问题,具体用法如下:
sealed class Result {

    // 定义请求成功
    data class SUCCESS(val data: String) : Result()

    // 定义请求失败
    data class FAIL(val throwable: Throwable) : Result()
}
val result = if (AppConfig.isDebug()) {
    Result.SUCCESS("模拟后台返回数据")
} else Result.FAIL(IllegalStateException("模拟请求失败了"))

when (result) {
    is Result.SUCCESS -> {
        println(result.data)
    }
    is Result.FAIL -> {
        println(result.throwable)
    }
}
  • 从这里可以看到,密封类和枚举类很类似,但是比枚举类更加强大,枚举可以是任意类型(SUCCESS 类型或者 FAIL 类型),一个枚举值可以有很多种结果(FAIL 类中的 throwable 参数是外层传入的,而不是像枚举一样只能固定在内部)。

委托机制

类委托
  • 先让我们来看一段代码
// 定义日志策略接口
interface ILogStrategy {

    fun log(message: String)
}
// 实现一个默认的日志策略类
class LogStrategyImpl : ILogStrategy {

    override fun log(message: String) {
        Log.i("测试输出", message)
    }
}
// 创建一个日志代理类
class LogStrategyProxy(strategy: ILogStrategy) : ILogStrategy by strategy
  • 看到这里大家可能有一些疑惑

    • ILogStrategy by strategy 是虾米操作?

    • LogStrategyProxy 这个类不去实现接口方法难道不会导致编译不通过么?

  • 关于这两个问题,我觉得都可以用同一个解释,LogStrategyProxy 之所以不用实现 ILogStrategy 的 log 方法,是因为在 ILogStrategy 接口后面加了 by strategy,而 strategy 对象就是 LogStrategyProxy 构造函数中的变量,意思是让这个接口的具体实现由 strategy 对象帮我实现就可以了,我(LogStrategyProxy 类)不需要再实现一遍了,这样是不是跟 Java 中的静态代理很像?只不过在 Kotlin 类委托特性上面编译器帮我们自动生成接口方法的代码,你可以把它想象下面这样的代码:

class LogStrategyProxy(val strategy: ILogStrategy) : ILogStrategy {

    override fun log(message: String) {
        strategy.log(message)
    }
}
  • 有人肯定会问了:口说无凭,我凭什么相信你就是这样的代码?

  • 这是个好问题,我提供一下反编译之后的代码,大家看一下就能明白了:

public final class LogStrategyProxy implements ILogStrategy {

    private final /* synthetic */ ILogStrategy $$delegate_0;
    
    public LogStrategyProxy(@NotNull ILogStrategy strategy) {
        Intrinsics.checkNotNullParameter(strategy, "strategy");
        this.$$delegate_0 = strategy;
    }

    public void log(@NotNull String message) {
        Intrinsics.checkNotNullParameter(message, "message");
        this.$$delegate_0.log(message);
    }
}
  • 是不是就立马顿悟了?调用的话也很简单,代码如下:
val logStrategyImpl = LogStrategyImpl()

LogStrategyProxy(logStrategyImpl).log("666666")
  • 最后让我们看看输出的日志:
测试输出: 666666
  • 这个我突然有一个大胆的想法,在使用类委托的情况下,再去重写它的接口方法呢?例如下面的:
class LogStrategyProxy(strategy: ILogStrategy) : ILogStrategy by strategy {

    override fun log(message: String) {
        println("测试输出 " + message)
    }
}
  • 关于这个问题我已经做过实践了,是木有问题的,大家放心大胆搞。
属性委托
  • 看过了上面的类委托,想必大家对委托有一定的了解了,那么属性委托是什么呢?简单来讲,类委托是为了帮我们减少一些实现代码,而属性委托是为了帮我们控制变量的 Get、Set 的操作了,废话不多说,下面演示一下用法,下面先创建一个委托类
class XxxDelegate {

    // 先给它一个默认值
    private var currentValue: String = "666666"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("测试字段名为 ${property.name} 的变量被访问了,当前值为 $currentValue")
        return currentValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        currentValue = newValue
        println("测试字段名为 ${property.name} 的变量被赋值了,当前值为 $currentValue" + ",新的值 $newValue")
    }
}
  • 使用代码示例如下:
var temp: String by XxxDelegate()
println("测试输出 " + temp)
temp = "55555"
println("测试输出 " + temp)
  • 具体日志输出如下:
System.out: 测试字段名为 temp 的变量被访问了,当前值为 666666
System.out: 测试输出 666666
System.out: 测试字段名为 temp 的变量被赋值了,当前值为 55555,新的值 55555
System.out: 测试字段名为 temp 的变量被访问了,当前值为 55555
System.out: 测试输出 55555
  • 看到这里你是否明白了,这个 XxxDelegate 类里面只有两个方法,一个是 getValue,另外一个是 setValue,从方法命名上我们已经能大致得出它的作用了,这里就不再多讲解了,var temp: String by XxxDelegate() 表示这个 temp 对象的创建会全权委托给 XxxDelegate 这个类来做。
懒委托
  • 什么是懒委托呢?大家知道单例模式中的懒汉式吧?这个跟它差不多,只不过我们不需要写静态方法和锁机制了,只需要像下面这样写:
val temp: String by lazy {
    println("测试变量初始化了")
    return@lazy "666666"
}
  • 调用代码如下:
println("测试开始")
println("测试第一次输出 " + temp)
println("测试第二次输出 " + temp)
println("测试结束")
  • 输出日志如下:
System.out: 测试开始
System.out: 测试变量初始化了
System.out: 测试第一次输出 666666
System.out: 测试第二次输出 666666
System.out: 测试结束
  • 是不是真的跟懒汉式差不多?只不过这种写法简化了很多,另外在日常开发中我们可以用它来做 findViewById 是最适合不过的。
private val viewPager: ViewPager? by lazy { findViewById(R.id.vp_home_pager) }
  • 另外懒委托还提供了几种懒加载模式供我们选择,

    • LazyThreadSafetyMode.SYNCHRONIZED:同步模式,确保只有单个线程可以初始化实例,这种模式下初始化时线程安全的,当 by lazy 没有指定模式的时候,就是默认用的这种模式。

    • LazyThreadSafetyMode.PUBLICATION:并发模式,在多线程下允许并发初始化,但是只有第一个返回的值作为实例,这种模式下是线程安全的,和 LazyThreadSafetyMode.SYNCHRONIZED 最大区别是,这种模式在多线程并发访问下初始化效率是最高的,本质上面是用空间换时间,哪个的线程执行快就让哪个先返回结果,其他线程执行的结果抛弃掉。

    • LazyThreadSafetyMode.NONE:普通模式,这种模式不会使用锁来限制多线程访问,所以是线程不安全的,所以请勿在多线程并发的情况下使用。

  • 具体使用的方式也很简单,如下:

val temp: String by lazy(LazyThreadSafetyMode.NONE) {
    println("测试变量初始化了")
    return@lazy "666666"
}
  • 另外有一点需要注意,使用懒委托的变量必须声明为 val(不可变的),因为它只能被赋值一次。

高阶函数

  • 高阶函数是 Kotlin 用于简化一些代码的写法,提升代码可读性而产生的,其中有 let、with、run、apply、also 五个常用函数
let 函数
  • 在函数块内可以通过 it 指代该对象。返回值为函数块的最后一行或指定 return 表达式

  • 一般写法

fun main() {
    val text = "Android轮子哥"
    println(text.length)
    val result = 1000
    println(result)
}
  • let 写法
fun main() {
    val result = "Android轮子哥".let {
        println(it.length)
        1000
    }
    println(result)
}
  • 最常用的场景就是使用let函数处理需要针对一个可 null 的对象统一做判空处理
videoPlayer?.setVideoView(activity.course_video_view)
videoPlayer?.setControllerView(activity.course_video_controller_view)
videoPlayer?.setCurtainView(activity.course_video_curtain_view)
videoPlayer?.let {
   it.setVideoView(activity.course_video_view)
   it.setControllerView(activity.course_video_controller_view)
   it.setCurtainView(activity.course_video_curtain_view)
}
  • 又或者是需要去明确一个变量所处特定的作用域范围内可以使用
with 函数
  • 前面的几个函数使用方式略有不同,因为它不是以扩展的形式存在的。它是将某对象作为函数的参数,在函数块内可以通过 this 指代该对象,返回值为函数块的最后一行或指定 return 表达式

  • 定义 Person 类

class Person(var name : String, var age : Int)
  • 一般写法
fun main() {
    var person = Person("Android轮子哥", 100)
    println(person.name + person.age)
    var result = 1000
    println(result)
}
  • with 写法
fun main() {
    var result = with(Person("Android轮子哥", 100)) {
        println(name + age)
        1000
    }
    println(result)
}
  • 适用于调用同一个类的多个方法时,可以省去类名重复,直接调用类的方法即可,经常用于 Android 中 RecyclerView.onBinderViewHolder 中,数据 model 的属性映射到 UI 上
override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    holder.nameView.text = "姓名:${item.name}"
    holder.ageView.text = "年龄:${item.age}"
}

override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    with(item){
        holder.nameView.text = "姓名:$name"
        holder.ageView.text = "年龄:$age"
    }
}
run 函数
  • 实际上可以说是 let 和 with 两个函数的结合体,run 函数只接收一个 lambda 函数为参数,以闭包形式返回,返回值为最后一行的值或者指定的 return 的表达式

  • 一般写法

var person = Person("Android轮子哥", 100)
println(person.name + "+" + person.age)
var result = 1000
println(result)
  • run 写法
var person = Person("Android轮子哥", 100)
var result = person.run {
    println("$name + $age")
    1000
}
println(result)
  • 适用于 let,with 函数任何场景。因为 run 函数是let,with两个函数结合体,准确来说它弥补了 let 函数在函数体内必须使用 it 参数替代对象,在 run 函数中可以像 with 函数一样可以省略,直接访问实例的公有属性和方法,另一方面它弥补了 with 函数传入对象判空问题,在 run 函数中可以像l et 函数一样做判空处理,这里还是借助 onBindViewHolder 案例进行简化
override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    holder.nameView.text = "姓名:${item.name}"
    holder.ageView.text = "年龄:${item.age}"
}

override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    item?.run {
        holder.nameView.text = "姓名:$name"
        holder.ageView.text = "年龄:$age"
    }
}
apply 函数
  • 从结构上来看 apply 函数和 run 函数很像,唯一不同点就是它们各自返回的值不一样,run 函数是以闭包形式返回最后一行代码的值,而 apply 函数的返回的是传入对象的本身

  • 一般写法

val person = Person("Android轮子哥", 100)
person.name = "HJQ"
person.age = 50
  • apply 写法
val person = Person("Android轮子哥", 100).apply {
    name = "HJQ"
    age = 50
}
  • 整体作用功能和 run 函数很像,唯一不同点就是它返回的值是对象本身,而 run 函数是一个闭包形式返回,返回的是最后一行的值。正是基于这一点差异它的适用场景稍微与 run 函数有点不一样。apply 一般用于一个对象实例初始化的时候,需要对对象中的属性进行赋值。或者动态 inflate 出一个 XML 的 View 的时候需要给 View 绑定数据也会用到,这种情景非常常见。特别是在我们开发中会有一些数据 model 向 View model 转化实例化的过程中需要用到
mRootView = View.inflate(activity, R.layout.example_view, null)
mRootView.tv_cancel.paint.isFakeBoldText = true
mRootView.tv_confirm.paint.isFakeBoldText = true
mRootView.seek_bar.max = 10
mRootView.seek_bar.progress = 0
  • 使用 apply 函数后的代码是这样的
mRootView = View.inflate(activity, R.layout.example_view, null).apply {
   tv_cancel.paint.isFakeBoldText = true
   tv_confirm.paint.isFakeBoldText = true
   seek_bar.max = 10
   seek_bar.progress = 0
}
  • 多层级判空问题
if (sectionMetaData == null || sectionMetaData.questionnaire == null || sectionMetaData.section == null) {
    return;
}
if (sectionMetaData.questionnaire.userProject != null) {
    renderAnalysis();
    return;
}
if (sectionMetaData.section != null && !sectionMetaData.section.sectionArticles.isEmpty()) {
    fetchQuestionData();
    return;
}
  • kotlin 的 apply 函数优化
sectionMetaData?.apply {

    // sectionMetaData 对象不为空的时候操作sectionMetaData

}?.questionnaire?.apply {

    // questionnaire 对象不为空的时候操作questionnaire

}?.section?.apply {

    // section 对象不为空的时候操作section

}?.sectionArticle?.apply {

    // sectionArticle 对象不为空的时候操作sectionArticle

}
also 函数
  • also 函数的结构实际上和 let 很像唯一的区别就是返回值的不一样,let 是以闭包的形式返回,返回函数体内最后一行的值,如果最后一行为空就返回一个 Unit 类型的默认值。而 also 函数返回的则是传入对象的本身
fun main() {
    val result = "Android轮子哥".let {
        println(it.length)
        1000
    }
    println(result) // 打印:1000
}
fun main() {
    val result = "Android轮子哥".also {
        println(it.length)
    }
    println(result) // 打印:Android轮子哥
}
  • 适用于 let 函数的任何场景,also 函数和 let 很像,只是唯一的不同点就是 let 函数最后的返回值是最后一行的返回值而 also 函数的返回值是返回当前的这个对象。一般可用于多个高阶函数链式调用

运算符重载

  • 在 Kotlin 中使用运算符最终也会调用对象对应的方法,我们可以通过重写这些方法使得这个对象支持运算符,这里不再演示代码
运算符 调用方法
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
运算符 调用方法
a++ a.inc()
a-- a.dec()
运算符 调用方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b), a.mod(b) (deprecated)
a..b a.rangeTo(b)
运算符 调用方法
a in b b.contains(a)
a !in b !b.contains(a)
运算符 调用方法
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)
运算符 调用方法
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)
运算符 调用方法
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b), a.modAssign(b) (deprecated)
运算符 调用方法
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))
运算符 调用方法
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

下一篇:全民 Kotlin:协程特别篇

另外推荐一个 Kotlin 语言编写的开源项目,大家感兴趣可以看看:AndroidProject-Kotlin

Android 技术讨论 Q 群:10047167

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容