T
traeai
登录
返回首页
The JetBrains Blog

如何让代码更易于高亮

8.5Score
如何让代码更易于高亮

TL;DR · AI 摘要

优化代码高亮复杂度可显著提升IDE性能,包括更快的响应速度、更低的CPU和内存消耗。

核心要点

  • 将代码拆分为模块可限制绑定范围并加速增量编译,从而提高高亮效率。
  • 关注高亮复杂度与算法复杂度同样重要,忽略它可能导致资源过度消耗。
  • 保持类和方法小而专注,有助于降低认知复杂度和高亮复杂度。

结构提纲

按章节快速跳转。

  1. 介绍代码高亮复杂度的概念及其对IDE性能的影响。

  2. 解释为何某些代码难以分析,并提出通过微调代码提升高亮效率。

  3. 模块化代码能减少依赖范围,提升编译和高亮性能。

  4. 模块化改进了IDE搜索效率和用户体验,同时支持部分项目编译。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Code Highlighting-Friendly
    • Highlighting Complexity
      • Algorithmic vs Cognitive
    • Separate Code into Modules
      • Limit Binding Scopes
      • Parallel Compilation

金句 / Highlights

值得收藏与分享的关键句。

#Scala#IntelliJ IDEA#代码优化#性能调优
打开原文
Image 1: Scala logo

IntelliJ IDEA 和 Android Studio 的 Scala 插件

ScalaScala 编程

如何让代码高亮友好

Image 2: Pavel Fatin

2026年5月7日

本文介绍了 _高亮复杂性_ 的概念,并提供了使代码更易于高亮的技巧,从而使高亮更快、更高效。

代码风格不仅仅是风格问题——它会影响物理世界!高亮友好的代码的好处包括:

  1. 更好的响应速度
  2. 优化的 CPU 使用
  3. 高效的内存使用
  4. 更低的系统温度
  5. 更安静的运行
  6. 更长的电池寿命

虽然单子(monads)是炸玉米饼,但你不应该在笔记本电脑上煎鸡蛋!

考虑高亮复杂性

想象一下你编写了以下函数来通过简单的递归计算斐波那契数列:

scala
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/#)

  1. 考虑高亮复杂性
  2. 将代码分离为模块
  3. 将类放在单独的文件中
  4. 在包中定义类而不是对象
  5. 优先选择小型类和方法
  6. 依赖接口而不是类
  7. 避免通配符导入
  8. 优先选择导入而不是混入
  9. 声明类和方法为私有
  10. 指定公共或复杂定义的类型
  11. 优先选择标准语言特性而非宏
  12. 将这些原则应用于 AI 生成的代码
  13. 总结

发现更多

AI 可能会生成不准确的信息,请核实重要内容

如何让代码更易于高亮 | The JetBrains Blog | traeai