Kotlin 之 DSL,java 面试写代码
=================================================================
入职新公司后发现 kotlin 的语法知识跟不上,因为公司开发团队的习惯问题,原先的公司还是 java+kt 混编开发的模式,许多同事基本不用 kt,一些不常规的语法应用很少接触使用,还存在使用错误的情况,所以现在要补一些 ktolin 语法使用的知识,而 DSL 就是第一个点
================================================================================
先上一小段可爱的代码:
ConstraintLayout {
layout_width = wrap_content
layout_height = wrap_content
background_res = R.drawable.ic1
ImageView {
layout_width = 22
layout_height = 22
top_toTopOf = parent_id
end_toEndOf = parent_id
margin_top = 9
margin_end = 5
padding = 5
scaleType = scale_fit_xy
src = R.drawable.ic2
}
TextView {
layout_id = "tv1"
layout_width = wrap_content
layout_height = wrap_content
textColor = "#ff3f4658"
textSize = 16f
textRes = R.string.tv1
center_horizontal = true
top_toBottomOf = "iv"
margin_top = 3
}
TextView {
layout_id = "tv2"
layout_width = wrap_content
layout_height = wrap_content
textColor = "#ffffff"
textSize = 12f
center_horizontal = true
top_toBottomOf = "tv1"
margin_top = 8
text = "deo"
}
}
土包子的我第一次看到这样的定义布局方式并没有感觉有啥新奇,不就是动态设置控件嘛,并没有什么高级的地方,反而觉得不如 XML 直观且无法预览,但一仔细看就不对了,按照常规的写法应该是从父布局开始逐个初始化对象,然后通过 layouparmars 设置布局属性添加到相应容器中,这种写法是什么情况,貌似直接嵌套还直接定义了控件属性,和 xml 的语法很像但貌似更简便(XML 也可以看做是 DSL)。此时只有一个字可以形容
通过偷偷摸摸的百度后知道这就是传说中的 DSL 构建的布局方式,这个东西是有了解过的,但没深入的看过(看完就忘),kt 的部分语法感觉过于难以阅读所以一直抗拒(其实是菜看不懂),靠着 apply、let、with 苟延残喘的使用着,还感觉自己 kt 用的很 nice。
这么陌生且高大上的词汇看的第一眼就感觉很难,自然反应的 Google 一下:
给出的解释就是*领域专用语言***。 词汇太专业,依然不知道是啥。。。查的时候还看到另一个词:GPL ,有必要了解一下,General Purpose Language 翻译过来就是通用语言,如 Java、OC、C 等等,这个好理解脑子一下就知道是啥了。这些语言使用很广泛,像 java 能 写 android 也能写后端,业务代码都是用这些语言写的。
对比一下 GPL,DSL 的针对性更强,只适用于特定的功能方向,就像文言文和白话文,一个用于作文记事,一个用于交际用语,举个可能不恰当的栗子,墓志铭就有特定的格式,第一他只能写给死人,第二有固定的格式,其他的像啥三言、五言、七言的,不是很准确但就是那个意思吧。
===================================================================
简单的了解过后感觉 DSL 更像是一种自己构建出来快捷的语法格式或者说结构,像最上面的布局代码其实最后做的工作应该和普通的写法(控件都最后都要初始化)是一样的,但对于项目来说大量频繁的重写就会省力很多且代码格式更好,当然好处不止于此。很多东西都见过也用过只是不知道原来这么 D 伊奥。
第一个众所周知的 HTML 就是典型的 DSL,首先她不算是编程语言,它定义了特殊的网页结构标示不同的含义,还一个特点他不能像 java 那样写出复杂的逻辑,它们都是命令式的,瞬间清醒,此时 DSL 和 GPL 的区别通透了。
其他的一些我们接触过的包括 Gradle、SQL,按照上面的特点,SQL 语句能写逻辑吗?Gradle 语法能写复杂逻辑吗?
他们的语句还有些共同点,仔细的回忆一下 SQL、Gradle 语句,是不是容易忘记,容易不认识,容易写错,可能一年没写过一条 SQL 的人大把抓,但他们都比较有特定的结构且只能用于实现特定功能,因为语法和常规的语言编写格式不同所以容易忘记。SQL 编写语句管理数据,Gradle 配置项目用于构建,HTML 用于显示网页文本内容,而不像 Java 或者其他编程语言一样,没有特定的限制,想在哪用在哪用。
============================================================================
要实现开头的布局方式需要怎么做呢?因为 SDK 也没定义这种规范,kt 也没有直接提供,所以只能内部创造,手动满足。
在 kotlin 中实现 DSL 构建要靠两样东西:
.扩展函数;
.带接收者的 Lambda 表达式;
先说扩展函数,这个还是比较简单的。
fun String.customOperation(append:String) :String{
return this+append
}
上面写了个 String 的扩展函数,自定义了一个操作,对接受者拼接了 append 字符串,在其他地方可以把 customOperation 当做系统的 api 直接调用。扩展函数的作用也一目鸟然,可以封装频率使用高的操作代码,可以像使用原生 API 一样使用扩展方法。
再看一下字节码:
// access flags 0x19
public final static customOperation(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
@Lorg/jetbrains/annotation
s/NotNull;() // invisible
// annotable parameter count: 2 (visible)
// annotable parameter count: 2 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1
L0
ALOAD 0
LDC "customOperation"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
ALOAD 1
LDC "append"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 11 L1
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
L2
LOCALVARIABLE customOperation Ljava/lang/String; L0 L2 0
LOCALVARIABLE append Ljava/lang/String; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
@Lkotlin/Metadata;(mv={1, 4, 0}, bv={1, 0, 3}, k=2, d1={"\u0000\n\n\u0000\n\u0002\u0010\u000e\n\u0002\u0008\u0002\u001a\u0012\u0010\u0000\u001a\u00020\u0001*\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0001\u00a8\u0006\u0003"}, d2={"customOperation", "", "append", "My_Application.app"})
// compiled from: Extends.kt
}
static 圈一下,一目了然,扩展方法被编译成静态方法,(Ljava/lang/String;Ljava/lang/String;) 圈一下,咦?怎么两个 String 呢?
带接受者的 Lambda 是啥样子,首先函数式编程 Lambda 是 kt 的一大特色(简洁避免语法噪音),但高阶函数看起来就很累,阅读性感觉不太友好,过于简化(主要还是菜),rxjava、协程一起用的时候就更懵逼了,lowB 的代码不香吗?
要知道啥是高阶函数,简单无脑的理解就是函数嵌套函数,在 kt 中 lambda 是可以当做参数进行传递的
fun createString(build:(StringBuilder)->Unit):String {
val stringBuilder=StringBuilder()
build(stringBuilder)
return stringBuilder.toString()
}
如果想要通过 StringBuild 去创建一个自定义的拼接字符串可以用上面的方法,比如:
这就是 lambda 作为参数传递的一个简单栗子,但是在调用 createString 的时候,在表达式中需要显示的调用 it,这个 it 就是 StringBuilder,而这个表达式的调用者是期望或者我们已经指定了就是一个 StringBuilder,所以可以避免这种显示调用的现象,在声明表达式的时候为它指定调用者,所以修改 createString 传入的表达式:
fun createString(build:StringBuilder.()->Unit):String {
val stringBuilder=StringBuilder()
build(stringBuilder)
return stringBuilder.toString()
}
这个时候可以看到调用函数的地方报错了
可以看到编译的提示,表达式的参数提示由 it 变成了 this,这个时候我们直接可以使用 StringBuilder 的方法而不需要显示的使用 it
在上面的方法中 build:StringBuilder.()->Unit 就是一个带接受者的 Lambda 表达式,它的声明语法格式是:
调用者类型.(参数类型)->返回值类型
最上面的 ConstraintLayout 布局其实就是基于 DSL 构建的动态布局,哪具体如何实现的呢?就拿 ConstraintLayout 来说,
首先要定义一种声明方式来初始化对象,所以可以写一个基于 Context 的扩展函数
fun Context.ConstraintLayout(): ConstraintLayout {
val constraintLayout =
if (style != null) ConstraintLayout(
ContextThemeWrapper(this, style)
) else ConstraintLayout(this)
return constraintLayout
}
这种布局容器都可以这么声明,但对于宽高、背景、方向等等的一些属性也需要处理,对于每个布局都要设置的宽高属性如何定义呢?还是利用扩展函数来实现
inline var View.layout_width: Number
get() {
return 0
}
set(value) {
val w = if (value.dp > 0) value.dp else value.toInt()
val h = layoutParams?.height ?: 0
updateLayoutParams<ViewGroup.LayoutParams> {
width = w
height = h
}
}
这里已经不是扩展函数了 ,应该说是扩展属性,layout_width 是自定义的属性,这里和 XML 中的属性定义一样的。同理对于高度可以这么写:
inline var View.layout_height: Number
get() {
return 0
}
set(value) {
val w = layoutParams?.width ?: 0
val h = if (value.dp > 0) value.dp else value.toInt()
updateLayoutParams<ViewGroup.LayoutParams> {
width = w
height = h
}
评论