1. 范围委派 (.value 查找)

范围委派 (.value 查找) 

此页面描述了范围委派。假设您已阅读并理解之前的页面,构建定义范围

现在我们已经涵盖了范围的所有细节,我们可以详细解释 .value 查找。如果您是第一次阅读此页面,可以跳过此部分。

总结一下我们到目前为止所学的内容

  • 范围是三个轴上的组件元组:子项目轴、配置轴和任务轴。
  • 对于任何范围轴,都有一个特殊的范围组件 Zero
  • 对于**子项目轴**,只有一个特殊的范围组件 ThisBuild
  • Test 扩展了 Runtime,而 Runtime 扩展了 Compile 配置。
  • 默认情况下,放置在 build.sbt 中的键的范围为 ${current subproject} / Zero / Zero
  • 键可以使用 / 运算符进行范围限定。

现在让我们假设我们有以下构建定义

lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")

lazy val projX = (project in file("x"))
  .settings(
    foo := {
      (Test / bar).value + 1
    },
    Compile / bar := 1
  )

foo 的设置主体中,声明了对范围限定键 Test / bar 的依赖关系。但是,尽管 Test / barprojX 中没有定义,但 sbt 仍然能够将 Test / bar 解析为另一个范围限定键,从而使 foo 初始化为 2

sbt 有一条定义明确的回退搜索路径,称为范围委派。此功能允许您在更通用的范围内设置一次值,使多个更具体的范围可以继承该值。

范围委派规则 

以下是范围委派的规则

  • 规则 1:范围轴具有以下优先级:子项目轴、配置轴,然后是任务轴。
  • 规则 2:给定一个范围,委派范围将通过以下顺序替换任务轴来搜索:给定的任务范围,然后是 Zero,它是该范围的非任务范围版本。
  • 规则 3:给定一个范围,委派范围将通过以下顺序替换配置轴来搜索:给定的配置、其父级、它们的父级等等,然后是 Zero(与无范围配置轴相同)。
  • 规则 4:给定一个范围,委派范围将通过以下顺序替换子项目轴来搜索:给定的子项目、ThisBuild,然后是 Zero
  • 规则 5:委派的范围限定键及其依赖设置/任务将在不携带原始上下文的情况下进行评估。

我们将在本页的其余部分中介绍每条规则。

规则 1:范围轴优先级 

  • 规则 1:范围轴具有以下优先级:子项目轴、配置轴,然后是任务轴。

换句话说,给定两个范围候选者,如果一个在子项目轴上具有更具体的价值,则它将始终获胜,而无论配置或任务范围如何。同样,如果子项目相同,则具有更具体配置值的子项目将始终获胜,而无论任务范围如何。我们将在后面的规则中看到如何定义更具体

规则 2:任务轴委派 

  • 规则 2:给定一个范围,委派范围将通过以下顺序**替换**任务轴来搜索:给定的任务范围,然后是 Zero,它是该范围的非任务范围版本。

这里有一个具体的规则,说明 sbt 将如何根据给定的键生成委派范围。请记住,我们试图展示给定任意 (xxx / yyy).value 的搜索路径。

**练习 A**:给定以下构建定义

lazy val projA = (project in file("a"))
  .settings(
    name := {
      "foo-" + (packageBin / scalaVersion).value
    },
    scalaVersion := "2.11.11"
  )

projA / name 的值是什么?

  1. "foo-2.11.11"
  2. "foo-2.12.18"
  3. 其他值?

答案是 "foo-2.11.11"。在 .settings(...) 中,scalaVersion 会自动范围限定为 projA / Zero / Zero,因此 packageBin / scalaVersion 变为 projA / Zero / packageBin / scalaVersion。该特定范围限定键未定义。通过使用规则 2,sbt 将替换任务轴为 Zero,即 projA / Zero / Zero(或 projA / scalaVersion)。该范围限定键定义为 "2.11.11"

规则 3:配置轴搜索路径 

  • 规则 3:给定一个范围,委派范围将通过以下顺序替换配置轴来搜索:给定的配置、其父级、它们的父级等等,然后是 Zero(与无范围配置轴相同)。

该规则的示例是之前提到的 projX

lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")

lazy val projX = (project in file("x"))
  .settings(
    foo := {
      (Test / bar).value + 1
    },
    Compile / bar := 1
  )

如果我们再次写出完整的范围,它就是 projX / Test / Zero。还要记住 Test 扩展了 Runtime,而 Runtime 扩展了 Compile

Test / bar 未定义,但根据规则 3,sbt 将在 projX / Test / ZeroprojX / Runtime / Zero,然后是 projX / Compile / Zero 中查找 bar 的范围限定。最后一个找到,即 Compile / bar

规则 4:子项目轴搜索路径 

  • 规则 4:给定一个范围,委派范围将通过以下顺序替换子项目轴来搜索:给定的子项目、ThisBuild,然后是 Zero

**练习 B**:给定以下构建定义

ThisBuild / organization := "com.example"

lazy val projB = (project in file("b"))
  .settings(
    name := "abc-" + organization.value,
    organization := "org.tempuri"
  )

projB / name 的值是什么?

  1. "abc-com.example"
  2. "abc-org.tempuri"
  3. 其他值?

答案是 abc-org.tempuri。因此,根据规则 4,第一个搜索路径是范围限定为 projB / Zero / Zeroorganization,它在 projB 中定义为 "org.tempuri"。它比构建级设置 ThisBuild / organization 具有更高的优先级。

范围轴优先级,再次 

**练习 C**:给定以下构建定义

ThisBuild / packageBin / scalaVersion := "2.12.2"

lazy val projC = (project in file("c"))
  .settings(
    name := {
      "foo-" + (packageBin / scalaVersion).value
    },
    scalaVersion := "2.11.11"
  )

projC / name 的值是什么?

  1. "foo-2.12.2"
  2. "foo-2.11.11"
  3. 其他值?

答案是 foo-2.11.11。范围限定为 projC / Zero / packageBinscalaVersion 未定义。规则 2 找到 projC / Zero / Zero。规则 4 找到 ThisBuild / Zero / packageBin。在这种情况下,规则 1 规定,子项目轴上更具体的价值将获胜,即定义为 "2.11.11"projC / Zero / Zero

**练习 D**:给定以下构建定义

ThisBuild / scalacOptions += "-Ywarn-unused-import"

lazy val projD = (project in file("d"))
  .settings(
    test := {
      println((Compile / console / scalacOptions).value)
    },
    console / scalacOptions -= "-Ywarn-unused-import",
    Compile / scalacOptions := scalacOptions.value // added by sbt
  )

如果您运行 projD/test 会看到什么?

  1. List()
  2. List(-Ywarn-unused-import)
  3. 其他值?

答案是 List(-Ywarn-unused-import)。规则 2 找到 projD / Compile / Zero,规则 3 找到 projD / Zero / console,规则 4 找到 ThisBuild / Zero / Zero。规则 1 选择 projD / Compile / Zero,因为它具有子项目轴 projD,而配置轴的优先级高于任务轴。

接下来,Compile / scalacOptions 指的是 scalacOptions.value,我们接下来需要找到 projD / Zero / Zero 的委派。规则 4 找到 ThisBuild / Zero / Zero,因此它解析为 List(-Ywarn-unused-import)

检查命令列出委派 

您可能想快速查看一下发生了什么。这时可以使用 inspect 命令。

sbt:projd> inspect projD / Compile / console / scalacOptions
[info] Task: scala.collection.Seq[java.lang.String]
[info] Description:
[info]  Options for the Scala compiler.
[info] Provided by:
[info]  ProjectRef(uri("file:/tmp/projd/"), "projD") / Compile / scalacOptions
[info] Defined at:
[info]  /tmp/projd/build.sbt:9
[info] Reverse dependencies:
[info]  projD / test
[info]  projD / Compile / console
[info] Delegates:
[info]  projD / Compile / console / scalacOptions
[info]  projD / Compile / scalacOptions
[info]  projD / console / scalacOptions
[info]  projD / scalacOptions
[info]  ThisBuild / Compile / console / scalacOptions
[info]  ThisBuild / Compile / scalacOptions
[info]  ThisBuild / console / scalacOptions
[info]  ThisBuild / scalacOptions
[info]  Zero / Compile / console / scalacOptions
[info]  Zero / Compile / scalacOptions
[info]  Zero / console / scalacOptions
[info]  Global / scalacOptions

请注意,“提供者”是如何显示 projD / Compile / console / scalacOptionsprojD / Compile / scalacOptions 提供的。同样,“委派”下也列出了所有可能的委派候选者,按照优先级顺序排列!

  • 所有在子项目轴上具有 projD 范围的范围将首先列出,然后是 ThisBuildZero
  • 在子项目中,配置轴上具有 Compile 范围的范围将首先列出,然后回退到 Zero
  • 最后,任务轴范围列表列出了给定的任务范围console /以及没有范围的列表。

.值查找 vs 动态分派 

  • 规则 5:委派的范围限定键及其依赖设置/任务将在不携带原始上下文的情况下进行评估。

请注意,范围委托感觉类似于面向对象语言中的类继承,但两者之间存在区别。在像 Scala 这样的面向对象语言中,如果一个特质Shape上有一个名为drawShape的方法,它的子类可以覆盖该行为,即使drawShapeShape特质中的其他方法中使用,这被称为动态分派。

然而,在 sbt 中,范围委托可以将一个范围委托给更一般的范围,例如将项目级设置委托给构建级设置,但该构建级设置不能引用项目级设置。

练习 E:给定以下构建定义

lazy val root = (project in file("."))
  .settings(
    inThisBuild(List(
      organization := "com.example",
      scalaVersion := "2.12.2",
      version      := scalaVersion.value + "_0.1.0"
    )),
    name := "Hello"
  )

lazy val projE = (project in file("e"))
  .settings(
    scalaVersion := "2.11.11"
  )

projE / version 将返回什么?

  1. "2.12.2_0.1.0"
  2. "2.11.11_0.1.0"
  3. 其他值?

答案是 2.12.2_0.1.0projE / version 委托给 ThisBuild / version,后者依赖于 ThisBuild / scalaVersion。出于这个原因,构建级设置应该主要限于简单的值分配。

练习 F:给定以下构建定义

ThisBuild / scalacOptions += "-D0"
scalacOptions += "-D1"

lazy val projF = (project in file("f"))
  .settings(
    compile / scalacOptions += "-D2",
    Compile / scalacOptions += "-D3",
    Compile / compile / scalacOptions += "-D4",
    test := {
      println("bippy" + (Compile / compile / scalacOptions).value.mkString)
    }
  )

projF / test 将显示什么?

  1. "bippy-D4"
  2. "bippy-D2-D4"
  3. "bippy-D0-D3-D4"
  4. 其他值?

答案是 "bippy-D0-D3-D4"。这是一种由 Paul Phillips 创作的练习的变体。

这是一个很好的演示所有规则的示例,因为 someKey += "x" 会扩展为

someKey := {
  val old = someKey.value
  old :+ "x"
}

检索旧值会导致委托,并且根据规则 5,它将转到另一个范围键。让我们先去掉 +=,并注释旧值的委托

ThisBuild / scalacOptions := {
  // Global / scalacOptions <- Rule 4
  val old = (ThisBuild / scalacOptions).value
  old :+ "-D0"
}

scalacOptions := {
  // ThisBuild / scalacOptions <- Rule 4
  val old = scalacOptions.value
  old :+ "-D1"
}

lazy val projF = (project in file("f"))
  .settings(
    compile / scalacOptions := {
      // ThisBuild / scalacOptions <- Rules 2 and 4
      val old = (compile / scalacOptions).value
      old :+ "-D2"
    },
    Compile / scalacOptions := {
      // ThisBuild / scalacOptions <- Rules 3 and 4
      val old = (Compile / scalacOptions).value
      old :+ "-D3"
    },
    Compile / compile / scalacOptions := {
      // projF / Compile / scalacOptions <- Rules 1 and 2
      val old = (Compile / compile / scalacOptions).value
      old :+ "-D4"
    },
    test := {
      println("bippy" + (Compile / compile / scalacOptions).value.mkString)
    }
  )

这将变成

ThisBuild / scalacOptions := {
  Nil :+ "-D0"
}

scalacOptions := {
  List("-D0") :+ "-D1"
}

lazy val projF = (project in file("f"))
  .settings(
    compile / scalacOptions := List("-D0") :+ "-D2",
    Compile / scalacOptions := List("-D0") :+ "-D3",
    Compile / compile / scalacOptions := List("-D0", "-D3") :+ "-D4",
    test := {
      println("bippy" + (Compile / compile / scalacOptions).value.mkString)
    }
  )