在 Foundation 框架中的度量值和單位

做者:Ole Begemann,原文連接,原文日期:2016-07-28
譯者:粉紅星雲;校對:saitjr;定稿:CMBgit

文章更新日誌:github

  • 2016/06/30 增長了一個「不足之處」小節,主要關於語法冗長。還有不多一部份內容的重寫。swift

  • 2016/08/02 把代碼更新到 Xcode 8 beta 4 版本的。安全

這個系列的其餘文章:session

  1. 在 Foundation 框架中的度量值和單位(本篇文章)app

  2. 乘法和除法框架

  3. 改良ide

  4. 幽靈類型 (Phantom Types) 動畫

在 iOS 10 和 macOS 10.12 裏的 Foundation 框架,新出了一系列將度量單位模型化的類型,咱們在現實中真實使用的度量單位,好比:1 公里,21 攝氏度。若是你還沒了解過這個,看看 WWDC session 238 吧,這裏概述講的挺好的。編碼

<!-- more -->

介紹

這個例子向你展現了下用法。讓咱們重新建一個我上次騎行的距離的常量開始。

let distance = Measurement(value: 106.4, unit: UnitLength.kilometers)
// → 106.4 km

這度量值(Measurement,在 swift 中是一個值類型)包含了數量(106.4)和度量單位(公里)。咱們也能夠本身定義一個單位,可是在 Foundation 框架中已經有了一堆常見的物理量(physical quantities)。目前已存在 21 種已定義單位類型。他們都是抽象類(Dimension)的子類,而且類名也是以 Unit 開頭的。好比:UnitAccelerationUnitMass,和 UnitTemperature 等等。咱們在這裏用的是 UnitLength

每個單位類提供了類屬性來描述其相關的各類單位。好比有米,公里,英里和光年。咱們能夠這麼寫,來把咱們原來在公里的度量值轉換爲其餘單位:

let distanceInMeters = distance.converted(to: .meters)
// → 106400 m
let distanceInMiles = distance.converted(to: .miles)
// → 66.1140591795394 mi
let distanceInFurlongs = distance.converted(to: .furlongs)
// → 528.911158832419 fur

UnitLength 自帶 22 個預約義好的的單位屬性,從皮米到光年都有。若是沒有你須要的單位,新建自定義的也十分簡單。只要給這個類擴展一個靜態的屬性,屬性包含描述新單位的標誌和它轉換爲本類型的基本單位的換算因素就好了。後面這部分是使用 UnitConverter 這個類搞定的。基本單位能夠是其餘同類型預約義的單位。它必定是已經在文檔裏的而且一般與(但不必定是)國際單位制對應的基本單位。對於 UnitLength 來講,基本單位就是米(.meters)。

extension UnitLength {
    static var leagues: UnitLength {
        // 1 league = 5556 meters
        return UnitLength(symbol: "leagues", 
            converter: UnitConverterLinear(coefficient: 5556))
    }
}

let distanceInLeagues = distance.converted(to: .leagues)
// → 19.150467962563 leagues

(我更傾向使用靜態存儲常量而不是一個計算屬性,可是在 NSObject 的子類擴展中,不怎麼支持存儲屬性。瞭解更多詳見 SR-993 。)

咱們也可使用標量值乘上度量值,或給度量值作加減。在須要時,單位的轉換是自動處理的:

let doubleDistance = distance * 2
// → 212.8 km
let distance2 = distance + Measurement(value: 5, unit: UnitLength.kilometers)
// → 111.4 km
let distance3 = distance + Measurement(value: 10, unit: UnitLength.miles)
// → 122493.4 m

注意到上個例子,當咱們添加一個公里和一個英里的度量值時,框架把他們全轉換成米( UnitLength 的基本單位)才相加的。原始單位的信息丟失了。而在先前的例子中都沒有發生過,那是由於以前是兩個相同單位的度量值(公里)。

優勢

安全

目前爲止運做良好。並且比咱們一般的使用簡單的浮點數字來作度量值、使用變量名來編碼單位,像 distanceInKilometerstemperatureInCelsius 等要好多了。不只預防了溝通上的誤解,更嚴謹的類型也讓編譯器能夠來幫忙檢查咱們的邏輯:錯誤的將長度單位添加到溫度單位類中這樣的事情再也不可能,由於這樣代碼就編譯不起來了。

更富有表現力的 API

在將來,採用新類型的 API(不管是蘋果原生,仍是第三方),會變得更加有表現力和自動文檔化。

假設有一個旋轉圖片的方法。如今可能要用 Double 來接收 angle 參數,並且做者要寫明這個方法是接收弧度制仍是角度值的參數,調用 API 的開發者也須要注意不要傳錯參數。在有單位的新世界裏,角度參數的類型必定會是 UnitAngle,同時解放了 API 的做者和調用者。不只採用了最爲明瞭的處理方式,而且排除了轉換錯誤產生的 bug。

一樣,一個動畫 API 再也不須要文檔解釋 duration 參數。參數的單位簡單明瞭的是 UnitDuration 類型。

MeasurementFormatter

最後,還附帶了一個 MeasurementFormatter 類。它能將度量值換算爲本地化的值,更加地域化(好比使用英里,而不是千米),數字格式和符號都參與換算。

let formatter = MeasurementFormatter()
let ?? = Locale(identifier: "de_DE")
formatter.locale = ??
formatter.string(from: distance) // "106,4 km"

let ?? = Locale(identifier: "en_US")
formatter.locale = ??
formatter.string(from: distance) // "66.114 mi"

let ?? = Locale(identifier: "zh_Hans_CN")
formatter.locale = ??
formatter.string(from: distance) // "106.4千米"

不足之處

新 API 有個不討喜的點,太過冗長。 Measurement(value: 5, unit: UnitLength.kilometers) 這句代碼的讀寫性都不好。雖然要找到既簡潔,又能清晰表達的方法命名很難,但這個方法也有些太過冗長了。

有種較爲極端的初始方式: let d = 5.kilometers。這個閱讀性超好,可是仍是有一個缺點——污染了通用的整型和浮點的命名空間。有點像這種表達:5.measure.kilometers

去掉參數標誌對初始化方法來講已是一個很大的進步了。let d = Measurement(5, UnitLength.kilometers) 更好理解。如今很喜歡給每個單位類型添加一個別名,從而擺脫掉 UnitLength 的前綴,像下面這樣:

typealias Length = Measurement<UnitLength>
let d = Length(5, .kilometers)

typealias Duration = Measurement<UnitDuration>
let t = Duration(10, .seconds)

這些加到你本身的項目中仍是挺容易的,只須要蘋果出一個更加標準的語法。

單位類之間的關係

咱們已經見過相同類型的度量值的相加了,若是我須要計算在單車騎行中的平均速度呢?速度等於距離除於時間,咱們新建一個騎行時間的度量值而後能夠作這個計算:

// 8h 6m 17s
let time = Measurement(value: 8, unit: UnitDuration.hours)
    + Measurement(value: 6, unit: UnitDuration.minutes)
    + Measurement(value: 17, unit: UnitDuration.seconds)
let speed = distance / time
// error: binary operator '/' cannot be applied to operands of type 'Measurement<UnitLength>' and 'Measurement<UnitDuration>'

這個除法運算會產生一個編譯錯誤。發現蘋果(可能在第一個版本的時候更明智些)斷開了類型之間的關聯。因此咱們不能用 UnitLength 來除以 UnitDuration,最後獲得一個 UnitSpeed 類型。不過手動添加很簡單。咱們只須要提供一個對應的除法運算符 / 的重載方法:

func / (lhs: Measurement<UnitLength>, rhs: Measurement<UnitDuration>) -> Measurement<UnitSpeed> {
    let quantity = lhs.converted(to: .meters).value / rhs.converted(to: .seconds).value
    let resultUnit = UnitSpeed.metersPerSecond
    return Measurement(value: quantity, unit: resultUnit)
}

在執行運算的時候,咱們把長度值轉換爲米的單位,持續時間用秒的單位,而且返回值的單位是米 / 秒。如今編譯器可開心了:

let speed = distance / time
 // → 3.64670802344312 m/s
 speed.converted(to: .kilometersPerHour)
 // → 13.1281383818845 km/h

能更加優雅一些嗎?

這種作法挺好的,可是有點受限。咱們須要給各類反向運算提供一個額外的重載方法,好比:距離 = 速度 × 時間、時間 = 距離 / 速度。若是咱們還想表達其餘的關係,好比:電阻 = 電壓 / 電流,咱們要所有再寫一遍。若是能夠一次性陳述表達各類關係,以後使用的時候自動就能用這個關係的話是否是超級厲害。我在下一篇文章中將會向你介紹這個。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索