在 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

相关文章
相关标签/搜索