This page was translated mostly with Google Translate. Please send a pull request to improve it.
此页面描述 scope 委托。 假定您已经阅读并理解了先前的页面 .sbt 构建定义 和 scopes。
既然我们已经涵盖了 scope 界定的所有细节,我们就可以详细解释 .value 查找。 如果您是第一次阅读此页面,则可以跳过本节。
总结到目前为止我们已经学到的东西:
Zero。
ThisBuild。
Test 扩展了 Runtime,而 Runtime 扩展了 Compile configuration。
${current subproject} / Zero / Zero。
/ 运算符确定 key 的 scope。
现在,假设我们具有以下构建定义:
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 的 setting 主体内部,声明了对 scoped key Test / bar 的依赖。
但是,尽管在 projX 中未定义 Test / bar,sbt 仍然能够将 Test / bar
解析为另一个 scoped key,导致 foo 初始化为 2。
sbt 具有定义明确的后备搜索路径,称为 scope 委托。 此功能使您可以在更广泛的 scope 内设置一次值,从而允许多个更特定的 scope 继承该值。
以下是 scope 委托的规则:
Zero (这是 scope 的非 task scope 版本)。
Zero(与无作用域的 configuration 轴相同)。
ThisBuild,然后为 Zero。
我们将在本页面的其余部分中查看每个规则。
换句话说,给定两个作用域候选者,如果一个在 subproject 轴上具有更特定的值,则无论 configuration 或 task scope 如何,它将始终获胜。 同样,如果 subproject 相同,则无论 task scope 如何,具有更具体 configuration 值的子项目将始终获胜。 我们将看到更多定义更具体的规则。
Zero (这是 scope 的非 task scope 版本)。
对于给定 key,sbt 将如何生成委托 scope,这里有一个具体规则。 记住,我们试图显示给定任意 (xxx / yyy).value 的搜索路径。
练习题 A: 给出以下构建定义:
lazy val projA = (project in file("a"))
  .settings(
    name := {
      "foo-" + (packageBin / scalaVersion).value
    },
    scalaVersion := "2.11.11"
  )
projA / name 的值是什么?
"foo-2.11.11"
"foo-2.12.18"
答案是 "foo-2.11.11"。
在 .settings(...) 内部,scalaVersion 的 scope 将自动设置为 projA / Zero / Zero,
因此 packageBin / scalaVersion 变为 projA / Zero / packageBin / scalaVersion。
该特定 scoped key 是未定义的。
通过使用规则2,sbt 将把 task 轴替换 Zero 作为 projA / Zero / Zero (或 projA / scalaVersion)。该 scoped key 定义为 "2.11.11"。
Zero(与无作用域的 configuration 轴相同)。
我们前面看到的例子是 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
  )
如果我们再次写出完整 scope,projX / Test / Zero 。
还记得 Test 扩展了 Runtime ,Runtime 扩展了 Compile 。
Test / bar 是未定义的,但是由于规则3,sbt 将查找 scope 为 projX / Test / Zero,
projX / Runtime / Zero,然后 projX / Compile / Zero。
找到最后一个,即 Compile / bar。
ThisBuild,然后为 Zero。
练习题 B: 给出以下构建定义:
ThisBuild / organization := "com.example"
lazy val projB = (project in file("b"))
  .settings(
    name := "abc-" + organization.value,
    organization := "org.tempuri"
  )
projB / name 的值是什么?
"abc-com.example"
"abc-org.tempuri"
答案是 abc-org.tempuri。
因此,根据规则4,第一个搜索路径是具有 projB / Zero / Zero scope 的 organization,在 projB 中定义为 "org.tempuri"。
它的优先级高于构建级别 setting 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 值是什么?
"foo-2.12.2"
"foo-2.11.11"
答案是 foo-2.11.11。
scope 为 projC / Zero / packageBin 的 scalaVersion 未定义。
scalaVersion scoped to projC / Zero / packageBin is undefined.
规则2找到 projC / Zero / Zero。 规则4找到 ThisBuild / Zero / packageBin。
在这种情况下,规则1决定在 subproject 轴上赢得更具体的价值,这是定义为 "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 您会看到什么?
List()
List(-Ywarn-unused-import)
答案是 List(-Ywarn-unused-import)。
规则2找到 projD / Compile / Zero,
规则3找到 projD / Zero / console,
规则4找到 ThisBuild / Zero / Zero。
规则1选择 projD / Compile / Zero 因为它具有 subproject 轴 projD,并且 configuration 轴的优先级高于 task 轴。
接下来, 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
请注意,“Provided by” 如何显示 projD / Compile / console / scalacOptions 提供了 projD / Compile / scalacOptions。
同样在 “Delegates” (委托),按优先顺序列出了所有可能的委托候选人!
projD scope 的所有 scope,然后列出 ThisBuild 和 Zero。
Compile scope 的 scope,然后退回到 Zero。
console / 的所有 scope,然后列出没有 task scope console / 的所有 scope。
请注意,scope 委托感觉类似于面向对象语言中的类继承,但是有区别。
在像 Scala 这样的 OO语言中,如果在 trait Shape 上有一个名为 drawShape 的 method,则即使 Shape trait 中的其他 method 使用了 drawShape,其子类也可以覆盖行为,这称为动态调度。
但是,在 sbt 中,scope 委托可以将 scope 委托给更通用的 scope,例如将 project-level 的 setting 委托给 build-level setting,但是该 build-level setting 不能引用 project-level setting。
练习题 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 返回什么?
"2.12.2_0.1.0"
"2.11.11_0.1.0"
答案是 2.12.2_0.1.0。
projE / version 委托 ThisBuild / version,
它取决于 ThisBuild / scalaVersion。
因此,build-level setting 应主要限于简单的值分配。
练习题 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 显示什么?
"bippy-D4"
"bippy-D2-D4"
"bippy-D0-D3-D4"
答案是 "bippy-D0-D3-D4"。
这是 Paul Phillips 最初创建的练习的变体。
这是所有规则的很好展示,因为 someKey += "x" 扩展为
someKey := {
  val old = someKey.value
  old :+ "x"
}
检索旧值将导致委托,并且由于规则5,它将转到另一个 scoped key。
让我们先摆脱 +=,然后为旧值注释委托:
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)
    }
  )