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 的相依性。但是,儘管 projX 中未定義 Test / bar,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 命令會列出委派 

您可能想要快速查看正在發生什麼。這就是 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 為範圍的範圍,然後是 ThisBuildZero
  • 在子專案內,首先列出在組態軸上以 Compile 為範圍的範圍,然後回退到 Zero
  • 最後,任務軸範圍列出了給定任務範圍 console / 和沒有任務範圍的範圍。

.value 查詢 vs 動態調度 

  • 規則 5:委派的範圍索引鍵及其相依設定/任務會在不攜帶原始內容的情況下進行評估。

請注意,範圍委派感覺類似於物件導向語言中的類別繼承,但存在差異。在像 Scala 這樣的物件導向語言中,如果 trait Shape 上有一個名為 drawShape 的方法,即使 drawShapeShape trait 中的其他方法使用,其子類別也可以覆寫該行為,這稱為動態調度。

然而,在 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)
    }
  )