如何让代码更易于高亮
TL;DR · AI 摘要
优化代码高亮复杂度可显著提升IDE性能,包括更快的响应速度、更低的CPU和内存消耗。
核心要点
- 将代码拆分为模块可限制绑定范围并加速增量编译,从而提高高亮效率。
- 关注高亮复杂度与算法复杂度同样重要,忽略它可能导致资源过度消耗。
- 保持类和方法小而专注,有助于降低认知复杂度和高亮复杂度。
结构提纲
按章节快速跳转。
- §引言
介绍代码高亮复杂度的概念及其对IDE性能的影响。
解释为何某些代码难以分析,并提出通过微调代码提升高亮效率。
模块化代码能减少依赖范围,提升编译和高亮性能。
模块化改进了IDE搜索效率和用户体验,同时支持部分项目编译。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Code Highlighting-Friendly
- Highlighting Complexity
- Algorithmic vs Cognitive
- Separate Code into Modules
- Limit Binding Scopes
- Parallel Compilation
金句 / Highlights
值得收藏与分享的关键句。
有些代码本质上难以分析。
细微的代码调整可以显著提高高亮效率。
模块限制了绑定范围并引入了明确的依赖图。
IntelliJ IDEA 和 Android Studio 的 Scala 插件
如何让代码高亮友好
2026年5月7日
本文介绍了 _高亮复杂性_ 的概念,并提供了使代码更易于高亮的技巧,从而使高亮更快、更高效。
代码风格不仅仅是风格问题——它会影响物理世界!高亮友好的代码的好处包括:
- 更好的响应速度
- 优化的 CPU 使用
- 高效的内存使用
- 更低的系统温度
- 更安静的运行
- 更长的电池寿命
虽然单子(monads)是炸玉米饼,但你不应该在笔记本电脑上煎鸡蛋!
考虑高亮复杂性
想象一下你编写了以下函数来通过简单的递归计算斐波那契数列:
def fib(n: Int): Int =
if (n <= 1) n
else fib(n - 1) + fib(n - 2)可以预见的是,这个函数运行得很慢,但你不会因此责怪 Scala。问题更为根本,且与编程语言无关。然而,这并不意味着该函数不能变得更快。有一种方法可以调整代码,使其输出完全相同的序列,但效率更高。
同样的道理也适用于代码高亮。如果高亮很慢,IDE 并不总是罪魁祸首。有些代码本质上难以分析。 然而,这并不意味着高亮不能快速完成。对代码进行微小调整可以使高亮显著更高效,即使代码本身基本保持不变。
到目前为止还不错。然而,虽然 _算法复杂性_ 是“计算机科学入门”,开发人员很少考虑 _高亮复杂性_。(这两者不同:代码可能运行缓慢但容易高亮,或者运行迅速但难以高亮。)即使你学习编译器构造,主要内容也不涉及性能,而涉及性能的部分通常指的是编译器而非源代码。此外,批量编译代码与编辑代码并不相同。
遵循软件工程的最佳实践通常可以加速高亮。这也是一般情况下有用的:保持类和方法的小巧和专注,优先选择清晰性而非巧妙性等。然而,这些原则主要关注 _认知复杂性_。与算法复杂性不同,认知复杂性往往与高亮复杂性相关。不过,它们并不相同,有时可能会有显著差异。
在编写代码时,你也应该考虑高亮复杂性。 如果忽略算法复杂性,你的代码性能会很差;如果忽略认知复杂性,你的代码将难以理解;如果忽略高亮复杂性,你的代码将需要很长时间才能编译或高亮,并在此过程中消耗大量资源。
好的代码应该在所有方面都表现良好。 幸运的是,使代码高亮友好的原则简单且易于在实践中应用。(大多数技巧不仅适用于 Scala,对其他语言也很有用。)
将代码拆分为模块
大多数 Scala 程序员会将代码划分为包,但较少有人将代码划分为模块。原因是一样的。
与 C 语言不同,Scala 支持包,大多数 Scala 项目自然会使用它们。然而,模块是 IDE 和构建工具的概念,而不是编程语言的概念,因此使用得较少。即使是 Java 平台模块系统,也更多地关注编译后的类和 JAR 文件,而不是源代码。
模块限制了绑定的作用域,并引入了一个显式的依赖图——否则,任何源文件原则上都可以依赖于任何其他源文件。这限制了增量编译和分析的作用域,从而加快了编译速度,减少了峰值资源消耗,并允许模块并行编译。
同样,模块也能提高高亮的性能——IDE 可以更高效地搜索实体和失效缓存。此外,这还能改善用户体验,使自动补全和自动导入更加相关,减少干扰。另一个好处是,当你在一个模块中运行应用程序或单元测试时,只需编译(或重新编译)项目的部分代码(即使其他模块无法干净地编译)。
包通常是模块的自然边界。如果你的项目只有一个模块,或者某些模块过大,考虑将一个或多个包提取为单独的模块。由于这种重构不影响包本身,因此应该是向后兼容的。此外,你仍然可以将类打包成单个 JAR 文件——重构针对的是源代码,而不一定是字节码。
注意,你必须使用真正的模块——使用多个目录或多个源根目录并不是一回事。(有关 sbt 的多项目构建,请参阅 多项目构建。)
将类放在单独的文件中
Scala 编译器不限制你可以向源文件中添加多少个类(或如何命名该文件)。这可能是有用的,但你不应过度使用这一功能。
如果你只修改源文件中的一个类,Scala 编译器无法单独编译该类——它必须编译整个源文件。IDE 通常也是如此:你在一个编辑器标签中打开的是文件而不是类,这会分析整个文件。(不过,你可以使用 增量高亮 来克服这一限制。)
此外,当每个类都有一个专门命名的文件时,即使没有 IDE,也更容易找到类并在项目中导航。你应该像将包放入对应的目录一样,将类放入对应的文件中。
另一个原因是导入语句。每个类都需要自己的导入集合,而在单个文件中定义多个类会合并这些导入并使它们变得通用。这可能会减慢引用解析的速度。(如果存在大量导入以及依赖于许多其他导入的导入实体,可能会出现组合爆炸。)
如果你注意到一个文件中有许多相对较大的类,考虑将类提取到单独的源文件中。这样做很容易,并且不会影响向后兼容性。(显然,伴生类和密封类层次结构应保留在同一个文件中。)
在包中定义类而不是对象中
在 Scala 中,包和 object 是相似的,甚至还有 package object!这使得可以将类放在 object 中而不是 package 中。然而,有一些很好的理由避免这样做。
首先,由于每个 object 都包含在一个源文件中,因此 object 中的多个类意味着文件中的多个类,正如我们已经看到的,这不是理想的。
其次,这不仅影响源文件,还影响编译后的代码。虽然每个类都会被编译成单独的 JVM .class 文件,就像它们是在 package 中定义的一样,但 object 只有一个 _大纲_——无论是 pickles 还是 TASTy。结果是,即使只需要访问其中一个类,编译器和 IDE 仍需要处理多个类。
因此,通常应该 在 `package` 中定义类而不是在 `object` 中。将 object 留给方法、变量和 type。(在 Scala 3 中,顶级定义也可以位于 package 中。)
偏向小类和小方法
是的,是的,你已经知道了这一点。但这里有一个转折。当你通常想到“小”时,你往往想到“简单”。例如,如果一个类只包含几个带有描述性名称的方法,这个类看起来很简单,你不需要分析这些方法的代码来理解它们的作用。
然而,这种奢侈并不适用于编译器或 IDE。如果你打开文件,整个内容都将被分析,如果方法(以及相应的类)很大,分析将消耗时间和资源。
即使类和方法很简单,也请考虑将大类和方法拆分为更小的部分。对于高亮显示,“代码行数”很重要;即使是一个类或方法,如果它非常大,也可能太多。
这也适用于生成的源代码:如果一个源文件是生成的并且其他源文件依赖于它,你不需要查看那段代码,但 IDE 和编译器仍然需要。在生成代码时,将输出划分为更小的部分——文件、类和方法;不要把所有内容混成一个整体。
依赖接口而不是类
一般来说,“针对接口编程”是一个好习惯,这也可以帮助高亮显示。
假设有一个大类,其中包含几个组成其 API 的方法。即使你只访问 API,读取源文件仍需要解析整个类,包括所有的实现细节。即使你明确指定了类型,解析相应的引用仍需要处理许多导入。
因此,如果一个类非常大,请考虑提取一个接口而不是直接引用该类。
避免通配符导入
使用命名导入而不是通配符导入是一种众所周知的最佳实践。它使代码更具可读性——你可以清楚地看到符号来自哪里。它还使代码更加健壮。(否则,如果库添加了一个与另一个导入类冲突的类,代码可能就无法编译了。)而且代码更简洁——自动补全只会显示实际使用的相关符号。
此外,命名导入可以加速代码分析。在解析标识符时,每个通配符导入都必须被检查,并且导入表达式可能反过来依赖于上方的通配符导入。可能还会有从对象中的导入,而这些对象本身又依赖于其他地方的导入。所有这些都不限于正在高亮显示的文件。即使你的代码仅依赖于其他文件中的签名,由于类型注解中的路径不是绝对的,分析仍然需要处理那些文件中的导入。
通配符导入对隐式值尤其成问题。因为隐式值是隐式的,并且可能需要其他隐式值,搜索它们可能会带来巨大的计算开销。如果隐式值是通过通配符导入的,那么它们的使用和导入都是隐式的。这进一步复杂化了任务——分析不仅需要找到某个模糊的实体,还需要在不明确的范围内查找。
因此,优先使用具体导入而不是通配符导入。将现有的通配符转换为命名导入。在 Scala 2 中,考虑按名称导入隐式值。尽管 Scala 3 中的 given 导入有所改进,但它们本质上仍然是通配符导入,因此依赖于良好的库设计。为了安全起见,优先使用按类型的导入而不是普通的 `given` 导入。(如果你正在设计一个库,请将隐式值定义在单独的包或对象中。)
优先使用导入而不是混入
可以使用继承而不是导入。即使在 Java 中我们也能看到这一点:每个 TestCase 同时也是 Assert,因此你可以直接访问诸如 assertEquals 的方法而无需显式导入它们。这看似方便,但实际上这相当于强制进行了通配符导入,并伴随着所有通常的缺点。更好的做法是选择性地 import Assert.assertEquals(或者作为选项的 import Assert.*)。
此外,通过子类化或混入 trait 的方式比普通的通配符导入更慢。分析时必须考虑继承、线性化以及重载和覆盖等问题。而且如果你修改了 trait,使用它的类也需要重新编译。
如果某些定义实际上是静态的,请将它们放在一个 `object` 而不是一个 `trait` 中,以便客户端导入而不是继承它们。
声明类和方法为 private
有许多充分的理由来最小化类和方法的可访问性:区分 API 和实现、维护源代码和二进制兼容性、防止自动补全功能中的混乱以及减少认知负担。
较少为人所知的是,尽可能声明类和方法为 `private` 可以提高编译和高亮显示的性能。增量编译器在确定 API 时不会包含 private 成员,因此不需要存储和比较它们。在解析引用的过程中,IDE 可以更快地跳过不可访问的元素。当你写“Foo”时,你已经知道指的是哪个 Foo。然而,你可能会惊讶于解析一个引用通常涉及多少计算。声明不合适的 Foo 为不可访问有助于加快分析速度。
Scala 插件可以通过自动检测可以设为 private 的声明来提供帮助。
指定公共或复杂定义的类型
每个非局部定义要么应为 private,要么应具有类型注解。对客户端可见的定义构成了一个 API。API 是抽象的边界,因此必须明确;客户端不应该需要研究实现(右侧)来理解签名(左侧)。与实现不同,API 必须稳定且不能依赖于右侧的内容。类型注解使 API 明确且稳定。
类型注解极大地帮助了增量计算。当签名稳定时,在代码修改后需要重新编译的类更少。同样,当你在 IDE 中编辑代码时,可以重用更多缓存,从而加快高亮显示并减少资源消耗。
因此,最好 始终显式指定非私有成员的类型。注意,即使存在覆盖关系也应指定类型,因为推断出的类型可能更具体,至少在 Scala 2 中是这样。(例如,如果超类方法返回 Seq[Int],而子类方法只是 = List(1),那么后者的类型将是 List[Int],这可能会影响直接使用子类的客户端。)你也应该 为受保护的成员指定类型,而不仅仅是公共成员——子类也是客户端。(例外情况是,当右侧既简单又稳定时,例如字面量,可以省略类型。不过,明确写出类型通常对人类和编译器都有好处。)
此外,即使是私有和局部定义,显式类型也可能带来好处。虽然增量编译器会重新编译整个文件,但 IDE 可以更渐进地失效缓存,并缩小范围。因此,如果私有成员较复杂,请添加类型注解——这可以使代码编辑更高效。同时,为复杂的局部变量指定类型。(有时你可能需要先提取方法或引入变量才能指定类型。)
Scala 插件中的 _Code Style | Type Annotation_ 需要为公共和受保护的成员指定类型注解——这些注解会通过重构和代码生成自动添加,并由相应的检查验证。然而,对于简单表达式有例外,且不要求为私有或局部定义指定类型,无论其复杂性如何。你可以调整这些设置以更加严格,确保安全。
优先使用标准语言特性而非宏
宏的概念可能看起来很有吸引力——你在编译时而不是运行时进行计算。然而,“编译时”同时也是“高亮显示时”,这无论你是使用编译器还是 IDE 编辑代码都是成立的……除非你总是一次性完成所有工作而不借助任何辅助工具。因此,宏可能会干扰代码的编写和编辑,使反馈变慢并消耗更多资源。请注意,这不仅适用于需要功能标志的 _定义_ 宏,还适用于不需要功能标志的 _使用_ 宏。
宏很少真正必要。以 Lisp 为例:其语法非常有限,语言是动态的,因此无论如何都不会进行静态分析。然而,Scala 本身已经是一种非常富有表现力的静态类型语言,标准语言特性足以应对大多数任务。在这种情况下,宏只会让静态分析以及理解代码变得更加困难。因此,编写代码时,应首先使用标准语言特性:类型参数、隐式参数等。宏应该是最后的选择,而不是首选方案。
这可以进一步概括:不要仅仅因为“你能”就使用复杂的语言特性,只有在确实需要时才使用;优先选择解决问题的最简单的解决方案。关于这个主题的更多细节,请参阅 Martin Odersky 的 Lean Scala。
将这些原则应用于 AI 生成的代码
即使你使用 AI 生成了 100% 的代码,你仍然需要阅读这些代码。(对吧?)因此,生成适合高亮显示的代码依然非常重要——代码是在数据中心生成的,但高亮显示是在 _你的_ 机器上完成的。这也有助于增量编译,减少使用代理时的系统负载。此外,它还能防止上下文填充(当模型加载无关信息时),从而提高准确性并降低成本。
你可以做的第一件事是通过示例引导 AI,因为模型倾向于传播现有的约定和编码风格。在新项目中,你可以明确地向 `AGENTS.md` 添加建议。最后但同样重要的是,无论代码是由人类还是 AI 编写的,你都可以随时重构代码。
总结
话虽如此,你的 IDE 性能也_很重要_。我们一直在努力改进 IntelliJ IDEA 和 Scala 插件的性能,并且有一些提高性能的技巧,你可以在实践中应用。然而,正如没有任何数量的编译器优化可以修复带有天真递归的例子一样,高亮显示有时也需要你的协助。
与所有事情一样,高亮复杂性并不是唯一的因素;你需要平衡不同的考虑。但通常情况下,两者之间并没有矛盾:清晰的代码可以改善高亮复杂性,而改善高亮复杂性也会使代码更清晰。无论如何,始终考虑高亮复杂性并掌握解决方案是有用的。
如需更多详细信息,请参阅 YouTrack 中相应的ticket。它还列出了可以帮助你更轻松应用重构的功能。如果你觉得它们有用,请为这些 ticket 投票,以便我们知道有需求。
如果有任何问题,请随时在 Discord 上向我们提问。
祝开发愉快!
JetBrains 的 Scala 团队
[](http://blog.jetbrains.com/scala/2026/05/07/how-to-make-code-highlighting-friendly/#)
- 考虑高亮复杂性
- 将代码分离为模块
- 将类放在单独的文件中
- 在包中定义类而不是对象
- 优先选择小型类和方法
- 依赖接口而不是类
- 避免通配符导入
- 优先选择导入而不是混入
- 声明类和方法为私有
- 指定公共或复杂定义的类型
- 优先选择标准语言特性而非宏
- 将这些原则应用于 AI 生成的代码
- 总结