《Kotlin 程序设计》第七章 Kotlin 编译过程分析

简介: 第七章 Kotlin 编译过程分析正式上架:《Kotlin极简教程》Official on shelves: Kotlin Programming minimalist tutorial京东JD:https://item.

第七章 Kotlin 编译过程分析

正式上架:《Kotlin极简教程》Official on shelves: Kotlin Programming minimalist tutorial
京东JD:https://item.jd.com/12181725.html
天猫Tmall:https://detail.tmall.com/item.htm?id=558540170670

http://mp.weixin.qq.com/s/lEFRH523W7aNWUO1QE6ULQ

我们知道,Kotlin基于Java虚拟机(JVM),通过Kotlin编译器生成的JVM字节码与Java编译的字节码基本相同,也因此与Java可以完全兼容,并且语法更加简洁,让我对Kotlin的编译过程甚是好奇。一通Google之后,毫无收获,Kotlin作为一门新语言,绝大多数的资料都局限于它的用法和特性相关。幸好Kotlin所有源码都已开源,遂决定生啃之。
Kotlin源码传送门:https://github.com/JetBrains/kotlin
在具体讲Kotlin编译过程之前,我们先来看一张图。


上图是Java编译器的编译过程,正如它们俩完全兼容的特性一样,等分析完Kotlin的编译过程,你会发现,Kotlin和Java的编译过程也是很相似的。

  1. 编译入口
    整个Kotlin工程代码达到200多MB,面对如此巨大的项目,我们需要找一个入口来进行逐步深入。所以,我们从最简单直观的入手,来看一下Kotlin的编译命令:
    kotlinc Hello.kt

打开kotlinc
脚本文件看执行了什么,代码如下:
cygwin=false;case "uname" in CYGWIN*) cygwin=true ;;esac...declare -a kotlin_app//运行入口if [ -n "$KOTLIN_RUNNER" ];then java_args=("${java_args[@]}" "-Dkotlin.home=${KOTLIN_HOME}") kotlin_app=("${KOTLIN_HOME}/lib/kotlin-runner.jar" "org.jetbrains.kotlin.runner.Main")else//编译入口 [ -n "$KOTLIN_COMPILER" ] || KOTLIN_COMPILER=org.jetbrains.kotlin.cli.jvm.K2JVMCompiler java_args=("${java_args[@]}" "-noverify") kotlin_app=("${KOTLIN_HOME}/lib/kotlin-preloader.jar" "org.jetbrains.kotlin.preloading.Preloader" "-cp" "${KOTLIN_HOME}/lib/kotlin-compiler.jar" $KOTLIN_COMPILER)fi"${JAVACMD:=java}" $JAVA_OPTS "${java_args[@]}" -cp "${kotlin_app[@]}" "${kotlin_args[@]}"

从代码中找到了疑似编译部分的入口代码 org.jetbrains.kotlin.cli.jvm.K2JVMCompiler,
//org.jetbrains.kotlin.cli.jvm.K2JVMCompiler @JvmStatic fun main(args: Array<String>) { CLICompiler.doMain(K2JVMCompiler(), args) }

紧跟跳转,到CLICompiler.doMain()方法中。
//org.jetbrains.kotlin.cli.common.CLICompilerpublic static void doMain(@NotNull CLICompiler compiler, @NotNull String[] args) { System.setProperty("java.awt.headless", "true"); //执行编译 ExitCode exitCode = doMainNoExit(compiler, args); if (exitCode != OK) { System.exit(exitCode.getCode()); } } @SuppressWarnings("UseOfSystemOutOrSystemErr") @NotNull public static ExitCode doMainNoExit(@NotNull CLICompiler compiler, @NotNull String[] args) { try { //执行编译过程,返回退出码 return compiler.exec(System.err, args); } catch (CompileEnvironmentException e) { System.err.println(e.getMessage()); return INTERNAL_ERROR; } }

我们可以看到,编译器执行完编译过程以后,会返回一个退出码,返回OK即为编译成功,否则直接退出编译过程。好,知道了这些,我们继续往下看。跟着代码的跳转,跳转,又跳转,看到了关键的编译入口代码,泪流满面。
//org.jetbrains.kotlin.cli.jvm.K2JVMCompilerKotlinToJVMBytecodeCompiler.compileBunchOfSources(environment)

跳转进入看看发生了什么。
fun compileBunchOfSources(environment: KotlinCoreEnvironment): Boolean { val moduleVisibilityManager = ModuleVisibilityManager.SERVICE.getInstance(environment.project) val friendPaths = environment.configuration.getList(JVMConfigurationKeys.FRIEND_PATHS) for (path in friendPaths) { moduleVisibilityManager.addFriendPath(path) } if (!checkKotlinPackageUsage(environment, environment.getSourceFiles())) return false //词法、语法分析、语义分析、目标代码生成等过程 val generationState = analyzeAndGenerate(environment) ?: return false // 找到运行主类 val mainClass = findMainClass(generationState, environment.getSourceFiles()) try { //写入文件 writeOutput(environment.configuration, generationState.factory, mainClass) return true } finally { generationState.destroy() } }

  1. 编译过程
    Kotlin的整个编译过程大致有以下环节:
  2. 词法分析2. 语法分析3. 语义分析及中间代码生成4. 目标代码生成

其中,我们把词法分析、语法分析、语义分析及中间代码生成称之为编译器前段,将源程序翻译成中间代码;目标代码生成称之为编译器后端,负责将中间代码转换生成目标代码,与目标语言有关的细节尽可能放在了后端。
2.1 词法分析
词法分析是将源程序读入的字符序列,按照一定的规则转换成词法单元(Token)序列的过程。词法单元是语言中具有独立意义的最小单元,包括关键字、标识符、常数、运算符、界符等等。
来看看Kotlin中划分的Token。
//org.jetbrains.kotlin.lexer.KtTokenspublic interface KtTokens { //关键字的token KtKeywordToken PACKAGE_KEYWORD = KtKeywordToken.keyword("package"); KtKeywordToken AS_KEYWORD = KtKeywordToken.keyword("as"); KtKeywordToken TYPE_ALIAS_KEYWORD = KtKeywordToken.keyword("typealias"); KtKeywordToken CLASS_KEYWORD = KtKeywordToken.keyword("class"); KtKeywordToken THIS_KEYWORD = KtKeywordToken.keyword("this"); KtKeywordToken SUPER_KEYWORD = KtKeywordToken.keyword("super"); KtKeywordToken VAL_KEYWORD = KtKeywordToken.keyword("val"); KtKeywordToken VAR_KEYWORD = KtKeywordToken.keyword("var"); KtKeywordToken FUN_KEYWORD = KtKeywordToken.keyword("fun"); KtKeywordToken FOR_KEYWORD = KtKeywordToken.keyword("for"); KtKeywordToken NULL_KEYWORD = KtKeywordToken.keyword("null"); ... //标识符、运算符token KtSingleValueToken LBRACKET = new KtSingleValueToken("LBRACKET", "["); KtSingleValueToken RBRACKET = new KtSingleValueToken("RBRACKET", "]"); KtSingleValueToken LBRACE = new KtSingleValueToken("LBRACE", "{"); KtSingleValueToken RBRACE = new KtSingleValueToken("RBRACE", "}"); KtSingleValueToken LPAR = new KtSingleValueToken("LPAR", "("); KtSingleValueToken RPAR = new KtSingleValueToken("RPAR", ")"); KtSingleValueToken DOT = new KtSingleValueToken("DOT", "."); ... //修饰符token KtModifierKeywordToken ABSTRACT_KEYWORD = KtModifierKeywordToken.softKeywordModifier("abstract"); KtModifierKeywordToken ENUM_KEYWORD = KtModifierKeywordToken.softKeywordModifier("enum"); KtModifierKeywordToken OPEN_KEYWORD = KtModifierKeywordToken.softKeywordModifier("open"); KtModifierKeywordToken INNER_KEYWORD = KtModifierKeywordToken.softKeywordModifier("inner"); KtModifierKeywordToken OVERRIDE_KEYWORD = KtModifierKeywordToken.softKeywordModifier("override"); KtModifierKeywordToken PRIVATE_KEYWORD = KtModifierKeywordToken.softKeywordModifier("private"); KtModifierKeywordToken PUBLIC_KEYWORD = KtModifierKeywordToken.softKeywordModifier("public"); ...}

Kotlin中将所有Token按照进行了分类,同时进行了Token分组。
//关键字 KtModifierKeywordToken[] MODIFIER_KEYWORDS_ARRAY = new KtModifierKeywordToken[] { ABSTRACT_KEYWORD, ENUM_KEYWORD, OPEN_KEYWORD, INNER_KEYWORD, OVERRIDE_KEYWORD, PRIVATE_KEYWORD, PUBLIC_KEYWORD, INTERNAL_KEYWORD, PROTECTED_KEYWORD, OUT_KEYWORD, IN_KEYWORD, FINAL_KEYWORD, VARARG_KEYWORD, REIFIED_KEYWORD, COMPANION_KEYWORD, SEALED_KEYWORD, LATEINIT_KEYWORD, DATA_KEYWORD, INLINE_KEYWORD, NOINLINE_KEYWORD, TAILREC_KEYWORD, EXTERNAL_KEYWORD, ANNOTATION_KEYWORD, CROSSINLINE_KEYWORD, CONST_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, SUSPEND_KEYWORD, HEADER_KEYWORD, IMPL_KEYWORD }; //访问权限修饰符 TokenSet VISIBILITY_MODIFIERS = TokenSet.create(PRIVATE_KEYWORD, PUBLIC_KEYWORD, INTERNAL_KEYWORD, PROTECTED_KEYWORD); //操作符 TokenSet OPERATIONS = TokenSet.create(AS_KEYWORD, AS_SAFE, IS_KEYWORD, IN_KEYWORD, DOT, PLUSPLUS, MINUSMINUS, EXCLEXCL, MUL, PLUS, MINUS, EXCL, DIV, PERC, LT, GT, LTEQ, GTEQ, EQEQEQ, EXCLEQEQEQ, EQEQ, EXCLEQ, ANDAND, OROR, SAFE_ACCESS, ELVIS, RANGE, EQ, MULTEQ, DIVEQ, PERCEQ, PLUSEQ, MINUSEQ, NOT_IN, NOT_IS, IDENTIFIER);...

将所有的Kotlin词法单元一一枚举出来并分组以后,就要进行词法分析了。Kotlin使用了第三方开源的JFlex作为词法分析器,并没有自己实现(当然,重复造轮子就是一件很愚蠢的事情了:))。
2.1.1 定义JFlex词法分析配置文件Kotlin.flex
配置文件分为三个部分:

  • 用户代码:。 - 选项与声明:用来定制词法分析器,包括类名、父类、权限修饰符等等,以%开头作为标记 - 词法规则:包括一组正则表达式和动作行为,也就是当正则表达式匹配成功后要执行的代码。
    整个flex规范配置文件如下格式:
    用户代码%%选项与声明%%词法规则

具体可看Kotlin.flex详细配置文件。
2.1.2 词法分析器_JetLexer
JFlex会读取配置文件并生成一个词法分析器(扫描器),在Kotlin编译器中对应_JetLexer
/** * This class is a scanner generated by * <a href="http://www.jflex.de/">JFlex</a> 1.7.0-SNAPSHOT * from the specification file <tt>/Users/endermz/projects/kotlin/compiler/frontend/src/org/jetbrains/kotlin/lexer/Kotlin.flex</tt> */class _JetLexer implements FlexLexer { //返回当前词法的状态 public final int yystate() { return zzLexicalState; } //进入下一个词法 public final void yybegin(int newState) { zzLexicalState = newState; } //返回与当前正则表达式匹配的文本 public final CharSequence yytext() { return zzBuffer.subSequence(zzStartRead, zzMarkedPos); } //返回匹配文本位置的字符 public final char yycharat(int pos) { return zzBuffer.charAt(zzStartRead+pos); } //返回匹配文本区域的长度 public final int yylength() { return zzMarkedPos-zzStartRead; } //当词法扫描时错误上报 private void zzScanError(int errorCode) throws KotlinLexerException { String message; try { message = ZZ_ERROR_MSG[errorCode]; } catch (ArrayIndexOutOfBoundsException e) { message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; } throw new KotlinLexerException(message + "\n at '" + yytext() + "'\n" + zzBuffer); } //将制定数量的字符推送到输入流 public void yypushback(int number) throws KotlinLexerException { if ( number > yylength() ) zzScanError(ZZ_PUSHBACK_2BIG); zzMarkedPos -= number; } ...}

上述的方法以“yy”为前缀,表示它们是由JFlex自动生成的,避免与复制到这个类中的用户代码名字有冲突。
关于如法匹配输入流:
当对输入流进行词法分析时,词法分析器依据最长匹配规则来选择输入流的正规式,即所选择的正规式能最长的匹配当前输入流。如果同时有多个满足最长匹配的正规式,则生成的词法分析器将从中选择最先出现在词法规则描述中的正规式。在确定了起作用的正规式之后,将执行贵正规式所关联的动作。如果没有匹配的正规式,词法分析器将终止对输入流的分析并给出错误消息。
最后,KotlinLexer调用_JetLexer进行词法分析。
public class KotlinLexer extends FlexAdapter { public KotlinLexer() { super(new _JetLexer((Reader) null)); }}

2.2 语法分析
语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,语法分析器将判断源程序在结构上是否正确。在语法分析过程中,会生成语法树(ST)/抽象语法树(AST)。
Kotlin中定义了AST的数据结构,
//AST抽象语法树节点public interface ASTNode extends UserDataHolder { //节点类型 IElementType getElementType(); //节点文本 String getText(); //父节点 ASTNode getTreeParent(); //第一孩子节点 ASTNode getFirstChildNode(); //最后孩子节点 ASTNode getLastChildNode(); //所有孩子 ASTNode[] getChildren(@Nullable TokenSet filter); //移除孩子 void removeChild(@NotNull ASTNode child); ...}

Kotlin的语法分析使用了InteliJ平台的开发者项目,语法分析器继承使用了PsiParser。
/** * 自定义语言分析器的插件端,收到语法分析器lexer返回的令牌,并从中建立一个AST数 /public interface PsiParser { /* * 解析指定PSI构建器的内容,并返回具有制定类型的根元素的AST树。 * @param root AST树中根元素的类型 * @param builder 构建用于检索原始文件令牌并构建AST树的构建器 * @return 生成的AST树的根节点 */ @NotNull ASTNode parse(@NotNull IElementType root, @NotNull PsiBuilder builder);}

然后通过提供的PsiParser实现KotlinParser。
public class KotlinParser implements PsiParser { //语法分析生成AST树 public ASTNode parse(IElementType iElementType, PsiBuilder psiBuilder, PsiFile psiFile) { KotlinParsing ktParsing = KotlinParsing.createForTopLevel(new SemanticWhitespaceAwarePsiBuilderImpl(psiBuilder)); if (scriptDefinitionProvider != null && scriptDefinitionProvider.isScript(psiFile) || psiFile.getName().endsWith(KotlinParserDefinition.STD_SCRIPT_EXT)) { ktParsing.parseScript(); } else { ktParsing.parseFile(); } return psiBuilder.getTreeBuilt(); } //分析类型 public static ASTNode parseTypeCodeFragment(PsiBuilder psiBuilder) { KotlinParsing ktParsing = KotlinParsing.createForTopLevel(new SemanticWhitespaceAwarePsiBuilderImpl(psiBuilder)); ktParsing.parseTypeCodeFragment(); return psiBuilder.getTreeBuilt(); } //分析表达式 public static ASTNode parseExpressionCodeFragment(PsiBuilder psiBuilder) { KotlinParsing ktParsing = KotlinParsing.createForTopLevel(new SemanticWhitespaceAwarePsiBuilderImpl(psiBuilder)); ktParsing.parseExpressionCodeFragment(); return psiBuilder.getTreeBuilt(); } //分析块代码 public static ASTNode parseBlockCodeFragment(PsiBuilder psiBuilder) { KotlinParsing ktParsing = KotlinParsing.createForTopLevel(new SemanticWhitespaceAwarePsiBuilderImpl(psiBuilder)); ktParsing.parseBlockCodeFragment(); return psiBuilder.getTreeBuilt(); } //分析Lambda表达式 public static ASTNode parseLambdaExpression(PsiBuilder psiBuilder) { KotlinParsing ktParsing = KotlinParsing.createForTopLevel(new SemanticWhitespaceAwarePsiBuilderImpl(psiBuilder)); ktParsing.parseLambdaExpression(); return psiBuilder.getTreeBuilt(); }

PSI,即程序结构接口,定义了程序的结构。
PSI文件(PSI File)则能够将源代码文件内容表示为特定编程语言元素的层次结构。说的通俗一点,PSI文件可以把Java、XML等语言代码表示为层次结构(树)的形式。例如,在IntelliJ开源的项目来看,PsiJavaFile可表示为Java文件,XmlFile表示为XML文件。通过PSI文件,我们能够遍历迭代文件中的元素,从而生成AST,正也正是语法分析中所需要的。
KotlinParser语法分析器调用KotParsing进行语法分析,并生成AST抽象语法树。
关于如何生成一个简单表达式的AST树,可以参考下图:



2.3 语义分析及中间代码生成
语义分析的任务是检查抽象语法树AST的上下文相关属性,即检查源代码是否符合该编程语言的规范,比如变量类型定义是否正确,运算符是否匹配等等。
举个例子:
var a: Int = 1var b: String = "hello"var c: Boolean = falsec = a * bprintln(c)

上述代码在语法分析阶段显然是符合语言编程结构的,但是却会出现编译失败的情况,因为a、b、c类型各不相同且不能相互转换,不能进行运算操作。这就是语义分析所要做的事情。
在Kotlin编译器中,语义分析的工作位于org.jetbrains.kotlin.resolve模块下。



该模块包含了所有的的上下文相关属性的检查,包括对表达式语句、常量、智能转换等上下文相关属性检查。
语义分析器进行了上下文相关属性的检查之后,会生成中间代码,位于org.jetbrains.kotlin.ir模块中。



如图所示,Psi2IrTranslator文件将AST抽象语法树转换成了IR中间表示形式(即中间代码)。
class Psi2IrTranslator(val configuration: Psi2IrConfiguration = Psi2IrConfiguration()) { interface PostprocessingStep { fun postprocess(context: GeneratorContext, irElement: IrElement) } private val postprocessingSteps = SmartList<PostprocessingStep>() fun add(step: PostprocessingStep) { postprocessingSteps.add(step) } fun generateModule(moduleDescriptor: ModuleDescriptor, ktFiles: Collection<KtFile>, bindingContext: BindingContext): IrModuleFragment { val context = createGeneratorContext(moduleDescriptor, bindingContext) return generateModuleFragment(context, ktFiles) } fun createGeneratorContext(moduleDescriptor: ModuleDescriptor, bindingContext: BindingContext) = GeneratorContext(configuration, moduleDescriptor, bindingContext) fun generateModuleFragment(context: GeneratorContext, ktFiles: Collection<KtFile>): IrModuleFragment { val irModule = ModuleGenerator(context).generateModuleFragment(ktFiles) postprocess(context, irModule) return irModule } private fun postprocess(context: GeneratorContext, irElement: IrElement) { insertImplicitCasts(context.builtIns, irElement) postprocessingSteps.forEach { it.postprocess(context, irElement) } }}

2.4 目标代码生成
目标代码生成的任务,顾名思义,是将中间代码转换为目标代码,即JVM字节码,位于org.jetbrains.kotlin.codegen模块中。
目标代码生成入口:
public class KotlinCodegenFacade { public static void doGenerateFiles( @NotNull Collection<KtFile> files, @NotNull GenerationState state, @NotNull CompilationErrorHandler errorHandler ) { ... for (KtFile file : files) { ... Set<FqName> obsoleteMultifileClasses = new HashSet<FqName>(state.getObsoleteMultifileClasses()); for (FqName multifileClassFqName : Sets.union(filesInMultifileClasses.keySet(), obsoleteMultifileClasses)) { doCheckCancelled(state); //目标代码类生成 generateMultifileClass(state, multifileClassFqName, filesInMultifileClasses.get(multifileClassFqName), errorHandler); } Set<FqName> packagesWithObsoleteParts = new HashSet<FqName>(state.getPackagesWithObsoleteParts()); for (FqName packageFqName : Sets.union(packagesWithObsoleteParts, filesInPackages.keySet())) { doCheckCancelled(state); //目标代码类包生成 generatePackage(state, packageFqName, filesInPackages.get(packageFqName), errorHandler); } doCheckCancelled(state); //生成结束 state.getFactory().done(); }}

在代码类生成的过程中,又包括生成类名、类体、字段、函数方法等环节,相关的生成类有ClassBodyCodegen、ClassFunctionCodegen、MemberCodegen、ExpressionCodegen、PropertyCodegen等。
Kotlin与Java不同的编译过程主要在于目标代码生成环节,Kotlin做了更多的工作。举个例子:
在Kotlin中,如果我们定义如下代码:
var a: Int = 1;

会等价于Java中
public Int a = 1;public Int getA(){ return a;}public void setA(int a) { this.a = a;}

那么,在Kotlin中是怎么实现的呢,我们来看PropertyCodegen关于属性的生成部分。
public class PropertyCodegen { private void gen( @Nullable KtProperty declaration, // 属性声明 @NotNull PropertyDescriptor descriptor, //描述,包括权限修饰符、注解、类型等。 @Nullable KtPropertyAccessor getter, // 决定是否生成getter @Nullable KtPropertyAccessor setter //决定是否生成setter ) { assert kind == OwnerKind.PACKAGE || kind == OwnerKind.IMPLEMENTATION || kind == OwnerKind.DEFAULT_IMPLS : "Generating property with a wrong kind (" + kind + "): " + descriptor; //生成注解信息 genBackingFieldAndAnnotations(declaration, descriptor, false); //根据注解和权限修饰符等信息判断是否自动生成Getter代码 if (isAccessorNeeded(declaration, descriptor, getter)) { generateGetter(declaration, descriptor, getter); } //根据注解和权限修饰符等信息判断是否自动生成Setter代码 if (isAccessorNeeded(declaration, descriptor, setter)) { generateSetter(declaration, descriptor, setter); } }}

可以看到,Kotlin在目标代码生成环节做了更多的处理,在该环节实现了自动生成Getter、Setter的代码。
总结
Kotlin的编译过程分析完了,当然很多细节的东西并没有深入研究,并且内容太大,不是一篇文章可以说的详尽的。
那么,分析了这么多,我们得到了什么有用的信息?
Kotlin编译器在编译前端(即词法分析、语法分析、语义分析、中间代码生成)并没有做让人感到惊讶的事情,和Java是基本一致的。与Java相比,所与众不同,也最重要的细节在编译后端(目标代码生成)环节。Kotlin编译器在目标代码生成环节做了很多类似于Java封装的事情,比如自动生成Getter/Setter代码的生成、Companion转变成静态类、修改类属性为final不可继承等等工作。可以说,大部分Kotlin的特性都在这个环节处理产生。可以这么说,Kotlin将我们本来在代码层做的一些封装工作转移到了编译后端阶段,以使得我们可以更加简洁的使用Kotlin语言。

相关文章
|
6月前
|
Java Kotlin
java调用kotlin代码编译报错“找不到符号”的问题
java调用kotlin代码编译报错“找不到符号”的问题
283 10
|
11月前
|
数据采集 JSON 安全
简明指南:使用Kotlin和Fuel库构建JD.com分析
Kotlin是一门基于JVM的静态类型编程语言,以其简洁、高效、安全、跨平台等特点而著称。这门语言不仅适用于Android开发,还可用于服务器端和Web应用程序的开发。Fuel库作为一款轻量级的Kotlin HTTP客户端,提供了一套简洁易用的DSL(领域特定语言),使得发送HTTP请求和处理响应变得异常便捷。除此之外,Fuel还支持协程、异步操作、JSON处理以及文件上传等多种功能,以满足各种网络编程需求。
简明指南:使用Kotlin和Fuel库构建JD.com分析
|
6月前
|
Java 编译器 API
告别KAPT!使用 KSP 为 Kotlin 编译提速
告别KAPT!使用 KSP 为 Kotlin 编译提速
372 0
|
Java Unix 编译器
开心档-软件开发入门之​Kotlin 使用命令行编译​
开心档-软件开发入门之​Kotlin 使用命令行编译​
46 0
|
Java Unix 编译器
开心档-软件开发入门之​Kotlin 使用命令行编译​
开心档-软件开发入门之​Kotlin 使用命令行编译​
|
Android开发 Kotlin
【错误记录】Kotlin 编译报错 ( Type mismatch: inferred type is String? but String was expected )
【错误记录】Kotlin 编译报错 ( Type mismatch: inferred type is String? but String was expected )
3147 0
【错误记录】Kotlin 编译报错 ( Type mismatch: inferred type is String? but String was expected )
|
Java Android开发 Kotlin
kotlin查看编译后的Java代码
kotlin查看编译后的Java代码
|
算法 Java Android开发
抽丝剥茧聊Kotlin协程之协程异常处理机制分析
抽丝剥茧聊Kotlin协程之协程异常处理机制分析
抽丝剥茧聊Kotlin协程之协程异常处理机制分析
|
Java Android开发 Kotlin
【错误记录】记录 Android 命令行执行 Java 程序中出现的错误 ( dx 打包 PC 可执行文件报错 | dalvik 命令执行 kotlin 编译的 dex 文件报错 )
【错误记录】记录 Android 命令行执行 Java 程序中出现的错误 ( dx 打包 PC 可执行文件报错 | dalvik 命令执行 kotlin 编译的 dex 文件报错 )
438 0
【错误记录】记录 Android 命令行执行 Java 程序中出现的错误 ( dx 打包 PC 可执行文件报错 | dalvik 命令执行 kotlin 编译的 dex 文件报错 )
|
Android开发 Kotlin
【错误记录】Kotlin 编译报错 ( Not nullable value required to call an ‘iterator()‘ method on for-loop range )
【错误记录】Kotlin 编译报错 ( Not nullable value required to call an ‘iterator()‘ method on for-loop range )
267 0
【错误记录】Kotlin 编译报错 ( Not nullable value required to call an ‘iterator()‘ method on for-loop range )