此页面描述了范围委派。假设您已阅读并理解之前的页面,构建定义 和 范围。
现在我们已经涵盖了范围的所有细节,我们可以详细解释 .value
查找。如果您是第一次阅读此页面,可以跳过此部分。
总结一下我们到目前为止所学的内容
Zero
。ThisBuild
。Test
扩展了 Runtime
,而 Runtime
扩展了 Compile
配置。${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 / bar
在 projX
中没有定义,但 sbt 仍然能够将 Test / bar
解析为另一个范围限定键,从而使 foo
初始化为 2
。
sbt 有一条定义明确的回退搜索路径,称为范围委派。此功能允许您在更通用的范围内设置一次值,使多个更具体的范围可以继承该值。
以下是范围委派的规则
Zero
,它是该范围的非任务范围版本。Zero
(与无范围配置轴相同)。ThisBuild
,然后是 Zero
。我们将在本页的其余部分中介绍每条规则。
换句话说,给定两个范围候选者,如果一个在子项目轴上具有更具体的价值,则它将始终获胜,而无论配置或任务范围如何。同样,如果子项目相同,则具有更具体配置值的子项目将始终获胜,而无论任务范围如何。我们将在后面的规则中看到如何定义更具体。
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
的值是什么?
"foo-2.11.11"
"foo-2.12.18"
答案是 "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"
。
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 / Zero
、projX / Runtime / Zero
,然后是 projX / Compile / Zero
中查找 bar
的范围限定。最后一个找到,即 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
的 organization
,它在 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
的值是什么?
"foo-2.12.2"
"foo-2.11.11"
答案是 foo-2.11.11
。范围限定为 projC / Zero / packageBin
的 scalaVersion
未定义。规则 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
会看到什么?
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
,因为它具有子项目轴 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 / scalacOptions
由 projD / Compile / scalacOptions
提供的。同样,“委派”下也列出了所有可能的委派候选者,按照优先级顺序排列!
projD
范围的范围将首先列出,然后是 ThisBuild
和 Zero
。Compile
范围的范围将首先列出,然后回退到 Zero
。console /
以及没有范围的列表。请注意,范围委托感觉类似于面向对象语言中的类继承,但两者之间存在区别。在像 Scala 这样的面向对象语言中,如果一个特质Shape
上有一个名为drawShape
的方法,它的子类可以覆盖该行为,即使drawShape
在Shape
特质中的其他方法中使用,这被称为动态分派。
然而,在 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
将返回什么?
"2.12.2_0.1.0"
"2.11.11_0.1.0"
答案是 2.12.2_0.1.0
。projE / 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
将显示什么?
"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,它将转到另一个范围键。让我们先去掉 +=
,并注释旧值的委托
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)
}
)