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

作者:Ole Begemann,原文链接,原文日期:2016-07-28
译者:粉红星云;校对:saitjr;定稿:CMB

文章更新日志:

  • 2016/06/30 增加了一个“不足之处”小节,主要关于语法冗长。还有很少一部分内容的重写。
  • 2016/08/02 把代码更新到 Xcode 8 beta 4 版本的。

这个系列的其他文章:

  1. 在 Foundation 框架中的度量值和单位(本篇文章)
  2. 乘法和除法
  3. 改良
  4. 幽灵类型 (Phantom Types)

在 iOS 10 �和 macOS 10.12 里的 Foundation 框架,新出了�一系列将度量单位模型化的类型,我们在现实中真实使用的度量单位,比如:1 千米,21 摄氏度。如果你还没了解过这个,看看 WWDC session 238 吧,这里概述讲的挺好的。

介绍

这个例子向你展示了下用法。让我们从新建一个我上次骑行的距离的常量开始。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容