Kotlin 使用技巧
Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin
但受Java语言的影响,我们使用Kotlin编写的代码时,依然受Java语言特性的影响,而Kotlin推崇的高级语言特性,并没有被大家熟知
Kotlin 的高级函数可以让代码可读性更强,更加简洁,值得学习并在项目中实践
下面将一起学习常用的Kotlin技巧,以及使用中应当注意的性能问题
拓展函数
扩展函数表示在即使不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数
示例一
有个字符串变量,由TextView显示,如果字符串为空,则TextView隐藏
var name: String? = null
if(name.isNullOrEmpty()) {
textView.isVisible = false
} else {
textView.isVisible = true
textView.text = name
}
使用拓展函数,就可以使用更加面向对象的方式来实现,拓展函数如下
fun TextView.setVisibleText(title: String?) {
if (title.isNullOrEmpty()) {
isVisible = false
return
}
isVisible = true
text = title
}
使用拓展函数后,只需要一行代码
var name: String? = null
textView.setText(name) //只需要一行代码
看上去就好像是TextView类自带了这个方法一样
示例二
再以Activity间常见的参数传递为例, 使用拓展函数,实现一行代码传递参数
- 添加拓展函数
inline fun <reified T : Any> Activity.intent(
key: String,
crossinline defaultValue: () -> T
) = lazy(LazyThreadSafetyMode.NONE) {
val value = intent?.extras?.get(key)
if (value is T) value else defaultValue()
}
inline fun <reified T : Any> Activity.intent(
key: String
) = lazy(LazyThreadSafetyMode.NONE) {
intent?.extras?.get(key)
}
inline fun <reified T : Activity> Context.startActivity(vararg params: Pair<String, Any>) {
startActivity<T>{
params
}
}
inline fun <reified T : Any> Context.startActivity(params: () -> Array<out Pair<String, Any>>) {
startActivity(makeIntent(this, T::class.java, params))
}
inline fun makeIntent(
context: Context,
targetClass: Class<*>,
params: () -> Array<out Pair<String, Any>>
): Intent = Intent(context, targetClass).apply {
val arry = params()
for ((_, value) in arry.withIndex()) {
makeParams(value)
}
}
inline fun Intent.makeParams(it: Pair<String, Any>) {
val value = it.second
// from anko
when (value) {
is Int -> putExtra(it.first, value)
is Long -> putExtra(it.first, value)
is CharSequence -> putExtra(it.first, value)
is String -> putExtra(it.first, value)
is Float -> putExtra(it.first, value)
is Double -> putExtra(it.first, value)
is Char -> putExtra(it.first, value)
is Short -> putExtra(it.first, value)
is Boolean -> putExtra(it.first, value)
is java.io.Serializable -> putExtra(it.first, value)
is Bundle -> putExtra(it.first, value)
is Parcelable -> putExtra(it.first, value)
is Array<*> -> when {
value.isArrayOf<CharSequence>() -> putExtra(it.first, value)
value.isArrayOf<String>() -> putExtra(it.first, value)
value.isArrayOf<Parcelable>() -> putExtra(it.first, value)
else -> throw IllegalArgumentException("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
}
is IntArray -> putExtra(it.first, value)
is LongArray -> putExtra(it.first, value)
is FloatArray -> putExtra(it.first, value)
is DoubleArray -> putExtra(it.first, value)
is CharArray -> putExtra(it.first, value)
is ShortArray -> putExtra(it.first, value)
is BooleanArray -> putExtra(it.first, value)
else -> throw IllegalArgumentException("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
}
- Activity之间传递参数
activity.startActivity<HomeActivity> { USER_NAME to "Kotlin" }
activity.startActivity<HomeActivity> { arrayOf( USER_NAME to "Kotlin" ) }
- 获取Activity传递的参数
class HomeActivity : Activity() {
// 方式一: 不带默认值
private val userName by intent<String>(USER_NAME)
// 方式二:带默认值:如果获取失败,返回一个默认值
private val userName by intent<String>(USER_NAME) { "admin" }
}
委托
关于Kotlin 委托属性,可以参考Delegated properties
主要包含属性委托和类委托,关于类委托的使用,可以参考我之前文章 Kotlin委托
Lazy 懒加载
如果你的类中存在很多全局变量实例,尽管你清楚的知道他们不会为空,但为了保证他们能够满足Kotlin的空指针检查语法标准,你也不得不做许多的非空判断保护才行,例如下面的例子:
class MainActivity:AppCompatActivity(),View.OnClickListener{
private var adapter:MultiTypeAdapter? = null
override fun onCreate(savedInstanceState : Bundle?){
...
adapter = MultiTypeAdapter()
...
}
override fun onClick(v:View?){
...
adapter?.notifyItemInserted(msgList.size - 1)
}
}
这里我们将adapter设置成了全局变量,但他的初始化工作是在onCreate()方法执行的,因此我们不得不把他设置为空,同时声明为MultiTypeAdapter?。虽然我们会确保自己已经初始化了,且能确保onClick()必然会在onCreate之后执行,但我们必须在onClick()方法里对adapter进行判空才行
那么可以使用Lazy特性,在使用时初始化,并只有首次访问时才会初始化代码
private val adapter: MultiTypeAdapter by lazy {
MultiTypeAdapter()
}
lazy的3种模式:
-
LazyThreadSafetyMode.SYNCHRONIZED
-
LazyThreadSafetyMode.PUBLICATION
-
LazyThreadSafetyMode.NONE
注意
LazyThreadSafetyMode.SYNCHRONIZED是默认模式,多线程时可以保证线程安全,但存在double-check lock开销; 如果已知在单线程工作环境,则建议使用LazyThreadSafetyMode.NONE模式,减少同步锁开销。
属性委托
属性委托的定义,请参考文档,利用属性委托的特性,可以做些什么呢?
举个例子,ViewBinding在项目中使用频率非常高,是个非常方便的视图管理类,当在Fragment 中使用时,官方推荐在
onDestroyView()时,将viewBinding引用清理
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
虽然清理的代码量不大,但对每一个Fragment都要执行这种重复操作,有没有更好的方式去管理viewBinding的释放呢?
直接上代码
fun <T: ViewBinding> Fragment.viewLifecycle(): ReadWriteProperty<Fragment, T> =
object : ReadWriteProperty<Fragment, T>, LifecycleEventObserver {
init {
//监听onStateChanged的变化
this@viewLifecycle.viewLifecycleOwnerLiveData.observe(this@viewLifecycle, {
it.lifecycle.removeObserver(this)
it.lifecycle.addObserver(this)
})
}
//viewBinding setValue时,将值委托给binding保存
private var binding: T? = null
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
binding = value
}
//getValue时,返回binding
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return binding!!
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
//Fragment DestroyView 时清理
if (event == Lifecycle.Event.ON_DESTROY) {
binding = null
}
}
}
代码主要有三个部分组成
- 使用Kotlin拓展函数,为Fragment添加新的功能
- 使用属性委托, 将viewBinding的值委托给拓展函数处理
- 监听Fragment的Lifecycle的状态变化,当Fragment调用onDestroyView时,将viewBinding清理
在Fragment的performDestroyView方法中,会在onDestroyView时更新状态为ON_DESTROY
void performDestroyView() {
mChildFragmentManager.dispatchDestroyView();
if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()
.isAtLeast(Lifecycle.State.CREATED)) {
//更新LifeCycle.Event
mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
mState = CREATED;
mCalled = false;
//销毁View
onDestroyView();
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onDestroyView()");
}
// Handles the detach/reattach case where the view hierarchy
// is destroyed and recreated and an additional call to
// onLoadFinished may be needed to ensure the new view
// hierarchy is populated from data from the Loaders
LoaderManager.getInstance(this).markForRedelivery();
mPerformedCreateView = false;
}
在Fragment中定义ViewBinding时,将属性委托给拓展函数即可
class HomeFragment : Fragment() {
private var binding: FragmentHomeBinding by viewLifecycle()
}
在ViewBinding 初始化时,会自动将属性委托给viewLifecycle,而Fragment DestroyView时,viewLifecycle自动释放viewBing实例, 无须手动处理
Sealed Class、Sealed Interface
关于Sealed Class及Sealed Interface,请参考文档
我们先来看Sealed Class,稍后对比Sealed Class和Sealed Interface的区别
Sealed Class
关于Sealed Class,我们可以和枚举类比来学习
首先来看枚举的特点
- 限制枚举每个类型只允许有一个实例
- 限制所有枚举常量使用相同的类型的值
枚举使用
enum class Color(val value: Int) {
Red(1),
Green(2),
Blue(3);
}
在枚举 Color 中定义了三个常量 Red 、Green 、Blue,但是它们只能使用 Int 类型的值,不能使用其他类型的值
Sealed Class 的特性
- Sealed Class 密封类约束的子类只是一个类型(子类可以是任意的类, 数据类、Kotlin 对象、普通的类,甚至也可以是另一个 Sealed)
- 从某种意义上说,Sealed Classes 是枚举类的扩展
- 枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例
那么Sealed Class 的特性能在实际项目中带来什么好处呢?
Sealed Class特性的应用
- 示例一
定义
sealed class HttpResult<out T> {
data class Success<out T>(val value: T) : HttpResult<T>()
data class Failure(val throwable: Throwable?) : HttpResult<Nothing>()
}
使用
when (result) {
is HttpResult.Failure -> {
// 进行失败提示
}
is HttpResult.Success -> {
// 进行成功处理
}
}
因为Sealed Class的子类可以是任意类型,可以将子类中的数据进行处理
- 示例二
结合上面拓展函数的特性,我们可以给View 封装的一系列操作
sealed class UiOp{
class MoveToTop(val px: Float): UiOp()
class ScaleSize(val size: Float): UiOp()
object Hide: UiOp()
}
fun View.Op(uiOp: UiOp) {
when(uiOp) {
UiOp.Hide -> {
visibility = View.GONE
}
is UiOp.MoveToTop ->{
translationX = uiOp.px
}
is UiOp.ScaleSize -> {
scaleX = uiOp.size
scaleY = uiOp.size
}
}
}
Sealed Class 和 Enum Class 相比,有如下优点
-
子类不受类型限制,可以是任意类
-
拓展性强,增加子类不需要修改父类的结构
-
子类不包含无关字段,只根据自身需要添加
Enum Class 和 Sealed Class 使用选择
-
不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适
-
其他情况下使用 Sealed Classes,在一定程度上可以使用 Sealed Classes 代替枚举
Sealed Interface 与 Sealed Class 区别
既然有了密封类,为什么还需要密封接口呢?主要为了弥补以下不足:
- 嵌套的枚举
我们知道,枚举类是final Class, 无法被继承
但有时候,我们又需要枚举能实现嵌套以处理更复杂的分类逻辑。此时密封接口就成了唯一选择
sealed interface Language
enum class HighLevelLang : Language {
Java, Kotlin, CPP
}
enum class MachineLang : Language {
ARM, X86
}
object AssemblyLang : Language
通过密封接口,相当于实现了''嵌套'的枚举
使用when进行分类处理
when (lang) {
is Machine ->
when (lang) {
MachineLang.ARM -> TODO()
MachineLang.X86 -> TODO()
}
is HighLevel ->
when (lang) {
HighLevelLang.CPP -> TODO()
HighLevelLang.Java -> TODO()
HighLevelLang.Kotlin -> TODO()
}
else -> TODO()
}
-
多继承的密封类
比如,当我们像下面这样定义密封类时
sealed class JvmLang { object Java : JvmLang() object Kotlin : JvmLang() object Groovy : JvmLang() } sealed class CompiledLang { object Java : CompiledLang() object Kotlin : CompiledLang() object Groovy : CompiledLang() object Cpp : CompiledLang() }Java不能同时继承自CompiledLang与JvmLang,所以无法在两个密封类中复用,需要重复定义。此时可能有人会说,密封类是可以被继承的,可以让
JvmLang继承CompiledLangsealed class JvmLang : CompiledLang object Java : JvmLang() object Kotlin : JvmLang() object Groovy : JvmLang() object Cpp : CompiledLang()如上,
Java同时是CompiledLang和JvmLang的子类,且没有违反单继承结构。但这只是因为
Java的语言特性还不够“复杂”罢了。Groovy除了是一个编译性语言,同时具有解释性语言的特性,可以同时归类为CompiledLang和InterpretedLang, 此时单继承结构很难维系,需要解除接口实现多继承:sealed interface CompiledLang sealed interface InterpretedLang sealed interface FunctionalLang sealed interface JvmLang : CompiledLang object Java : JvmLang object Kotlin : JvmLang, FunctionalLang object Groovy : JvmLang, FunctionalLang, InterpretedLang object JavaScript: InterpretedLang object Cpp : CompiledLang, FunctionalLang //编程语言的市场份额 fun shareOfCompiledLang(lang: CompiledLang) = when(lang) { Java -> TODO() Kotlin -> TODO() Groovy -> TODO() Cpp -> TODO() } fun shareOfInterpretedLang(lang: InterpretedLang) = when(lang) { JavaScript -> TODO() Groovy -> TODO() }
高阶函数(inline/noline/crossline)
inline
首先来看下kotlin里的高阶函数定义:如果一个函数接收另一个函数作为参数,或返回类型是一个函数类型,那么该函数被称为是高阶函数。
例如
fun plus(name: String): String {
return "hello$name"
}
//高阶函数
fun setName(name: String, operation: ((name: String) -> String)): String {
return operation(name)
}
fun test() {
val newName = setName("kotlin") {
it
}
println(newName)
}
上面的例子setName是一个高阶函数,该函数的一个参数operation: ((name: String) -> String)是lambda表达式。
转换为Java代码:
public final class TestKotlin {
@NotNull
public final String plus(@NotNull String name) {
return "hello" + name;
}
@NotNull
public final String setName(@NotNull String name, @NotNull Function1 operation) {
return (String)operation.invoke(name);
}
public final void test() {
String newName = this.setName("kotlin", (Function1)null.INSTANCE);
}
}
字节码:
public final setName(Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/String;
L0
ALOAD 1
LDC "name"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
ALOAD 2
LDC "operation"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 14 L1
ALOAD 2
ALOAD 1
INVOKEINTERFACE kotlin/jvm/functions/Function1.invoke (Ljava/lang/Object;)Ljava/lang/Object; (itf)
CHECKCAST java/lang/String
ARETURN
L2
LOCALVARIABLE this Lcom/mx/live/TestKotlin; L0 L2 0
LOCALVARIABLE name Ljava/lang/String; L0 L2 1
LOCALVARIABLE operation Lkotlin/jvm/functions/Function1; L0 L2 2
MAXSTACK = 2
MAXLOCALS = 3
// access flags 0x11
public final test()V
L0
LINENUMBER 18 L0
ALOAD 0
LDC "kotlin"
GETSTATIC com/mx/live/TestKotlin$test$newName$1.INSTANCE : Lcom/mx/live/TestKotlin$test$newName$1;
CHECKCAST kotlin/jvm/functions/Function1
INVOKEVIRTUAL com/mx/live/TestKotlin.setName (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/String;
ASTORE 1
L1
LINENUMBER 21 L1
RETURN
L2
LOCALVARIABLE newName Ljava/lang/String; L1 L2 1
LOCALVARIABLE this Lcom/mx/live/TestKotlin; L0 L2 0
MAXSTACK = 3
MAXLOCALS = 2
通过字节码我们发现两个问题:
- 每个调用高阶函数的地方,lambda表达式都会分别产生一个继承自
kotlin/jvm/functions/Function1的函数对象。 (注意:如果不希望每次调用lambda表达式的地方都创建对象,可以将lambda表达式赋值给一个变量,然后每次引用该变量) - 从上面的字节码可以看出,
lambda表达式调用存在装箱和拆箱开销。 基本调用过程:入参String ,Function1.invoke(Object) : Object,出参Object -> String
如何解决这种性能问题呢?kotlin提供了inline内联函数
//高阶函数
inline fun setName(name: String, operation: ((name: String) -> String)): String {
return operation(name)
}
转换为Java代码:
public final class TestKotlin {
@NotNull
public final String plus(@NotNull String name) {
return "hello" + name;
}
@NotNull
public final String setName(@NotNull String name, @NotNull Function1 operation) {
int $i$f$setName = 0;
return (String)operation.invoke(name);
}
public final void test() {
String name$iv = "kotlin";
int $i$f$setName = false;
int var6 = false;
}
}
字节码:
public final test()V
L0
LINENUMBER 18 L0
ALOAD 0
ASTORE 2
LDC "kotlin"
ASTORE 3
LOCALVARIABLE it Ljava/lang/String; L3 L6 5
LOCALVARIABLE $i$a$-setName-TestKotlin$test$newName$1 I L4 L6 6
LOCALVARIABLE this_$iv Lcom/mx/live/TestKotlin; L1 L7 2
LOCALVARIABLE name$iv Ljava/lang/String; L1 L7 3
LOCALVARIABLE $i$f$setName I L2 L7 4
LOCALVARIABLE newName Ljava/lang/String; L8 L9 1
LOCALVARIABLE this Lcom/mx/live/TestKotlin; L0 L9 0
结论:如果高阶函数为inline内联函数,则lambda表达式不会生成kotlin/jvm/functions/Function1函数对象,会直接执行lambda函数体。
inline 本质上是编译时直接将函数内容直接复制粘贴到调用处
我们知道函数调用最终是通过JVM操作数栈的栈帧完成的,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程,使用了inline关键字理论上可以减少一个栈帧层级。
noline
当高阶函数被inline标记时,使用noinline可以使函数参数不被内联
//highFuc被inline修饰,而函数参数block0()使用了noinline修饰
inline fun test(noinline block0: () -> Unit, block1: () -> Unit) {
block0() //不会内联
block1()
}
crossinline
高阶函数使用inline时,会带来一个弊端
例如,在高阶函数的Lambda表达式使用全局return是不允许的
fun testInline() {
test {
return //错误,不允许在非内联函数中直接return
}
}
fun test(block: () -> Unit) {
println("before")
block()
println("after")
}
但是可以写成return@test
fun testInline() {
test {
return@test //允许,局部返回
}
}
fun test(block: () -> Unit) {
println("before")
block()
println("after")
}
其中return是全局返回,会影响Lambda之后的执行流程;而return@test是局部返回,不会影响Lambda之后的执行流程
如果我就想全局返回,那么可以通过inline来进行声明
fun testInline() {
test {
return //允许,全局返回
}
}
inline fun test(block: () -> Unit) {
println("before")
block()
println("after")
}
因为test通过inline声明为内联函数,所以调用方可以直接使用return进行全局返回,执行testInline()的结果:
before
这时候有种场景,既想使用inline 优化高阶函数,又不想调用方在lambda 中使用return 打断整个执行流程,那么就可以使用crossinline
fun testInline() {
test {
return //允许,全局返回
}
}
crossinline fun test(block: () -> Unit) {
println("before")
block()
println("after")
}
crossinline关键字就像一个契约,它用于保证内联函数的Lambda表达式中一定不会使用return全局返回
使用建议
- 避免在每个地方都直接使用lambda表达式,可以将lambda表达式赋值给一个变量,然后每次引用该变量,这样既可以避免重复创建函数对象,也可以避免重复装箱拆箱开销。
- inline:编译时直接将函数内容直接复制粘贴到调用处。
- noinline:当函数被inline标记时,使用noinline可以使函数参数不被内联。
- crossinline: 允许内联函数里的函数类型参数可以被间接调用,但是不能在Lambda表达式中使用全局return返回
Sequence 和 Iterator
Kotlin 提供了一种容器类型——序列(Sequence)。 序列提供与 Iterable 相同的函数,但实现另一种方法来进行多步骤集合处理
Sequence 和 Iterator 从代码结构上来看,它们非常的相似
interface Iterable<out T> {
operator fun iterator(): Iterator<T>
}
interface Sequence<out T> {
operator fun iterator(): Iterator<T>
}
它们处理集合操作时有两部分区别
内存
-
Sequences(序列)
在 Sequences 处理过程中,每一个中间操作(
map/filter/take等)不会进行任何计算,它们只会返回一个新的 Sequence,经过一系列中间操作之后,会在末端操作toList或count等等方法中进行最终的运算如何区分是中间操作还是末端操作,看方法的返回类型,中间操作返回的是 Sequence,末端操作返回的是一个具体的类型( List、int、Unit 等等
-
Iterator(迭代器)
在 Iterator 处理过程中,每一次的操作都是对整个数据进行操作,需要开辟新的内存来存储中间结果,将结果传递给下一个操作
执行顺序
-
Sequences(序列)
Sequences 在中间操作处理时,让每个元素按照执行完所有中间操作后,下一个元素再执行,再执行最终运算
-
舒服点
Iterator 则是在执行中间操作时,所有元素完成某个中间操作后,创建新集合,然后再统一执行下一个操作,最后执行结果运算
代码对比
val words1 = "The quick brown fox jumps over the lazy dog".split(" ")
// 将列表转换为序列
val wordsSequence = words1.asSequence()
val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)
println("Lengths of first 4 words longer than 3 chars")
// 末端操作:以列表形式获取结果。
println(lengthsSequence.toList())
println("----------------------")
val words2 = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words2.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)
println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)
输出结果:
Lengths of first 4 words1 longer than 3 chars
filter: The
filter: quick
length: 5
filter: brown
length: 5
filter: fox
filter: jumps
length: 5
filter: over
length: 4
[5, 5, 5, 4]
----------------------
filter: The
filter: quick
filter: brown
filter: fox
filter: jumps
filter: over
filter: the
filter: lazy
filter: dog
length: 5
length: 5
length: 5
length: 4
length: 4
Lengths of first 4 words2 longer than 3 chars:
[5, 5, 5, 4]
序列(Sequences) 的秘诀在于它们是共享同一个迭代器(iterator) ---序列允许 filter操作 转换一个元素后,然后立马可以将这个元素传递给 filter操作 ,而不是像集合(lists) 一样等待所有的元素都循环完成了filter操作后,用一个新的集合存储起来,然后又遍历循环从新的集合取出元素完成map操作
使用选择
Sequences
链接多个操作符
处理集合时性能损耗的最大原因是循环。集合元素迭代的次数越少性能越好
使用first{...}或者last{...}操作符
当使用接收一个预判断的条件 first or last 方法时候,使用序列(Sequences)会产生一个小的性能提升,如果将它与其他操作符结合使用,它的性能将会得到更大的提升
Iterable
量级比较小的集合
访问索引元素
例如List是有索引的,这就是为什么按索引访问项目非常快并且具有恒定时间复杂度的原因。在另一方面,Sequences 则必须逐项进行,直到它们到达目标项目
区分和使用 run, with, let, also, apply
run, with, let, also, apply 如何区分并使用呢,从以下三个方面来区分。
- 是否是扩展函数。
- 作用域函数的参数(this、it)。
- 作用域函数的返回值(最后一行、调用本身)
是否是拓展函数
with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回
val list = listOf("Apple","Banana","Orange","Pear","Grape")
val result = with(StringBuilder()){
append("Start eating fruits \n")
for(fruit in list){
append(fruit).append("\n")
}
append("ate all fruits")
toString()
}
println(result)
而run, let, also, apply 属于拓展函数,需要由对象调用,例如run函数
val list = listOf("Apple","Banana","Orange","Pear","Grape")
val result = StringBuilder().run{
append("Start eating fruits \n")
for(fruit in list){
append(fruit).append("\n")
}
append("ate all fruits")
toString()
}
println(result)
作用域函数的参数(this、it)
以run函数 和 let函数为例,其他同理
var viewBinding: ViewBinding? = null
//run函数参数是 this,可以省略不写,不可修改
viewBinding?.run {
tvCount?.text = "0"
tvTitle.text = "title"
}
//let函数 默认参数 it, 可以修改
viewBinding?.let {
it.tvCount?.text = "0"
it.tvTitle.text = "title"
}
//let函数 默认参数 it, 可以修改
viewBinding?.let { binding ->
binding.tvCount?.text = "0"
binding.tvTitle.text = "title"
}
在上面的例子中可以看出run函数中的 this 参数可以省略,调用更加的简洁。但是 let 函数中参数 it 允许我们自定义参数名字,使可读性更强
作用域函数的返回值(最后一行、调用本身)
我们来对比 let 和 also ,它们接受的参数都是 it, 但是它们的返回值是不同的,let 返回最后一行,also 返回调用本身。
var name = "kotlin"
// 返回调用本身
name = name.also {
"also test"
}
println("name = ${name}") // name = kotlin
// 返回的最后一行
name = name.let {
"let test"
}
println("name = ${name}") // name = let test
所以我们可以使用also的特性, 自我操作后,将对象传递给下一个作用域函数使用
apply函数
apply 函数综合类上面三种函数的特性, 是作用域函数,函数参数是this,并返回对象调用对象本身,就不举例说明
| 函数 | 是否是扩展函数 | 函数参数(this、it) | 返回值(调用本身、最后一行) |
|---|---|---|---|
| with | 不是 | this | 最后一行 |
| run | 是 | this | 最后一行 |
| let | 是 | it | 最后一行 |
| also | 是 | it | 调用本身 |
| apply | 是 | this | 调用本身 |
使用 require 或者 check 函数作为条件检查
// 传统的做法
if (!this::context.isInitialized) {
throw IllegalArgumentException("context is null")
}
// 使用 require 去检查
require(!this::context.isInitialized) {
"context must not be null"
}
// 使用 checkNotNull 检查
val name: String? = null
checkNotNull(name){
"name must not be null"
}