作者:Ole Begemann,原文链接,原文日期:2016/07/29
译者:钟颖;校对:小铁匠Linus;定稿:CMB

更新:
2016-08-02 已将代码更新至 Xcode 8 Beta 4

本系列其他文章:

(1) Measurements 和 Units 概览

(2) 乘法和除法

(3) 内容提炼(本文)

(4) 幽灵类型

Swift 中 Measurements 和 Units 的系列文章中,仍然有一些收尾工作要做。如果你还没有看过之前的文章的话,可以在第一部分中找到 Foundation 框架中 Measurements 和 Units 接口的大致介绍,并在第二部分中看到我如何扩展该系统用于类型安全的乘法和除法。

在计算过程中保持单位

在我们之前的文章里面,乘法和除法在计算之前总是会将单位转换到各自的默认单位,正如我们在 UnitProduct 协议的 defaultUnitMapping() 方法定义的那样。为了让结果在各种计算场景下都正确,无论传入参数的单位是什么。

到目前为止,我们仍然使用一个默认的单位映射作为计算结果的单位。比如说,UnitSpeedUnitDurationUnitLength 的映射是 (.metersPerSecond, .seconds, .meters)。这意味着 72 千米每 2 小时 将会在计算前被转换成 72000 米每 7200 秒。然后我们会将计算结果封装成 Measurement<UnitVelocity> 并且返回,它的单位将会是米每秒。

从实现的角度来说这是一个最简单的方案,但是可以想象的是,方法的调用者会更希望在计算中尽可能保持单位一致。如果我输入米和秒,则尽可能的返回米每秒,但是如果我输入千米和小时,则希望返回千米每小时。

首选单位映射

我们可以在协议里面添加一个新的方法来实现这个目标,这个方法会通过优先级排序返回一个单位映射的集合。在计算的时候我们通过遍历这个集合,为当前的计算找出最合适的单位映射。如果没有匹配到,则回退到默认的单位映射。我把这个方法叫做 preferredUnitMappings()。完整的协议看起来像是这样:


protocol UnitProduct {associatedtype Factor1: Dimensionassociatedtype Factor2: Dimensionassociatedtype Product: Dimensionstatic func defaultUnitMapping() -> (Factor1, Factor2, Product)static func preferredUnitMappings() -> [(Factor1, Factor2, Product)]
}

我们需要提供一个返回空数组的默认实现,这样的话如果协议的实现者不需要这个功能,他也可以选择忽略这个方法。


extension UnitProduct {// 默认实现static func preferredUnitMappings() -> [(Factor1, Factor2, Product)] {return []}
}

接下来,我们需要提供一系列的便捷方法,他们的功能是为乘法或者除法的参数匹配出合适的单位映射。这个方法需要三个重载的形式,取决于 Factor1Factor2Product 三个参数是哪两个组成一对。他们的工作原理是相同的:返回在 preferredUnitMappings 列表里面同时匹配两个参数的第一个结果。如果没有匹配的话,返回默认的单位映射。我们使用 Swift 3 里面 Sequence 的新方法 first(where:) 来实现这个功能:


extension UnitProduct {static func unitMapping(factor1: Factor1, factor2: Factor2) -> (Factor1, Factor2, Product) {let match = preferredUnitMappings().first { (f1, f2, _) inf1 == factor1 && f2 == factor2}return match ?? defaultUnitMapping()}static func unitMapping(product: Product, factor2: Factor2) -> (Factor1, Factor2, Product) {let match = preferredUnitMappings().first { (_, f2, p) inp == product && f2 == factor2}return match ?? defaultUnitMapping()}static func unitMapping(product: Product, factor1: Factor1) -> (Factor1, Factor2, Product) {let match = preferredUnitMappings().first { (f1, _, p) inp == product && f1 == factor1}return match ?? defaultUnitMapping()}
}

在计算中使用首选单位映射

最后,我们可以修改乘法和除法来使用这个新功能。计算过程本身没有任何变化,我们还是通过默认的单位进行计算。但是在返回结果之前,我们将其转换到我们首选的单位映射。如下是除法的代码(实现另一个重载方法的实现是类似的):


/// UnitProduct / Factor2 = Factor1
public func / <UnitType: Dimension> (lhs: Measurement<UnitType>, rhs: Measurement<UnitType.Factor2>)-> Measurement<UnitType.Factor1> where UnitType: UnitProduct, UnitType == UnitType.Product {// 使用默认单位进行计算let (resultUnit, rightUnit, leftUnit) = UnitType.defaultUnitMapping()let value = lhs.converted(to: leftUnit).value / rhs.converted(to: rightUnit).valuelet result = Measurement(value: value, unit: resultUnit)// 转换到首选的单位let (desiredUnit, _, _) = UnitType.unitMapping(product: lhs.unit, factor2: rhs.unit)return result.converted(to: desiredUnit)
}

一切准备就绪,我们可以为 UnitLength 实现 preferredUnitMappings() 这个方法,他实现了 UnitProduct 这个协议:


extension UnitLength {static func preferredUnitMappings() -> [(UnitSpeed, UnitDuration, UnitLength)] {return [(.kilometersPerHour, .hours, .kilometers),(.milesPerHour, .hours, .miles),(.knots, .hours, .nauticalMiles)]}
}

现在,计算过程中匹配到合适单位的将会得到保留(会带有一点舍入误差):


Measurement(value: 72, unit: UnitLength.kilometers) / Measurement(value: 2, unit: UnitDuration.hours)
// → 35.999971200023 km/h
Measurement(value: 10, unit: UnitLength.miles) / Measurement(value: 1, unit: UnitDuration.hours)
// → 9.99997514515231 mph
Measurement(value: 25, unit: UnitLength.nauticalMiles) / Measurement(value: 2, unit: UnitDuration.hours)
// → 12.5000107991454 kn

这是一个好主意吗?

我不太确定这个方案是不是真的是一个好想法。他使代码变得相当的复杂,但是收益可以说是很小的。而且在每一次计算时,枚举首选单位列表会使代码变得慢一点点1,这可能在循环中会是个问题。但像我们在这里做的这种简单的计算应该尽可能的快。

乘方的问题

如果你使用过 UnitProduct 这个协议的话,可能发现它并不能应用于物理量的乘方,也就是说,Factor1Factor2 类型相同的情况。面积 = 长度 × 长度 是一个很好的例子:


extension UnitArea: UnitProduct {typealias Factor1 = UnitLengthtypealias Factor2 = UnitLengthtypealias Product = UnitAreastatic func defaultUnitMapping() -> (UnitLength, UnitLength, UnitArea) {return (.meters, .meters, .squareMeters)}
}

如果我们尝试执行两个长度的乘法时,编译器会因为 * 运算符的歧义而报错。


let width = Measurement(value: 4, unit: UnitLength.meters)
let height = Measurement(value: 6, unit: UnitLength.meters)
let area: Measurement<UnitArea> = width * height
// error: Ambiguous use of operator '*'

原因是我们有两个乘法重载运算符,一个是 (Factor1, Factor2) -> Product 另一个是 (Factor2, Factor1) -> Product。当 Factor1Factor2 类型相同的时候,这两个重载方法的类型是一模一样的,编译器不知道应该调用哪个,所以就会报错。(在我们的场景中,两个方法都是对的,他们能算出同一个结果,但是编译器并不知道这一点)

最好的解决方案是,我们能够给其中一个方法添加一个通用的约束,类似于 Factor1 != Factor2,可以让类型检查在参数类型相同的时候将其区分开来。像这样:


func * <UnitType: Dimension> (...) -> ...where UnitType: UnitProduct, UnitType == UnitType.Product, UnitType.Factor1 != UnitType.Factor2
// error: Expected ':' or '==' to indicate a conformance or same-type requirement

遗憾的是,Swift 并不支持这样的语法,Swift 的 where 语句只能包含 :==

单独给乘方的协议

我们通过引入一个单独的协议,UnitSquare 协议,来解决这个问题,用于定义乘方关系。这个协议只需要两个 associated 类型,FactorProduct


protocol UnitSquare {associatedtype Factor: Dimensionassociatedtype Product: Dimensionstatic func defaultUnitMapping() -> (Factor, Factor, Product)static func preferredUnitMappings() -> [(Factor, Factor, Product)]
}

我们在这里就不展开其具体实现了,因为这个协议和 UnitProduct 很大程度上是相同的。(他的乘法和除法重载都只需要一个,相反的是 UnitProduct 需要两个。)

如果我们将 UnitArea 遵循 UnitSquare,那么这些计算就能符合我们的预期:


extension UnitArea: UnitSquare {typealias Factor = UnitLengthtypealias Product = UnitAreastatic func defaultUnitMapping() -> (UnitLength, UnitLength, UnitArea) {return (.meters, .meters, .squareMeters)}
}let width = Measurement(value: 4, unit: UnitLength.meters)
let height = Measurement(value: 6, unit: UnitLength.meters)
let area: Measurement<UnitArea> = width * height
// → 24.0 m²
area / width
// → 6.0 m
area / height
// → 4.0 m

自身相除

谜题的最后一部分,我想应该是实现两个相同类型的除法,比如说 6 米 / 4 米 = 1.5。计算结果应该是一个没有单位的量(换句话说是一个 Double 类型),并且对所有的 Dimension 类型都是可以有效的。

支持这一特性十分简单,我们只需要再增加一个重载的除法。可以描述为:输入两个相同 Dimension 的量,返回一个 Double 值。我们通过将两个量都转换成基本类型来实现这个除法:


func / <UnitType: Dimension> (lhs: Measurement<UnitType>, rhs: Measurement<UnitType>) -> Double {return lhs.converted(to: UnitType.baseUnit()).value / rhs.converted(to: UnitType.baseUnit()).value
}let ratio = height / width
// → 1.5

代码

我将在本系列文章中讨论到的所有代码放到了一个叫 Ampere 的库里面,你可以在 GitHub 上面找到。这个工作正在进展中,我还没有将其变成一个“真正的”库,类似于加入版本控制以及支持 CocoaPods,因为我不知道社区会不会对这些内容感兴趣。所以,让我知道你的想法吧!

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg。


  1. 在当前的实现中,单位映射的列表甚至没有被缓存下来,每次计算的时候都会重新创建一次。所以这毫无疑问是可以优化的,但不管怎样都会比不需要进行一次查询要慢。 ↩

Measurements 和 Units,第三部分相关推荐

  1. github上可供新手阅读和玩耍的java项目有哪些??

    扫垃圾 ,Java 爱好者, 前沿技术思考者 刘巍然-学酥 等 217 人赞同 一个聊天程序范例: tinystruct2.0/smalltalk.java at master · m0ver/tin ...

  2. vw实现移动端自适应页面

    一.设备支持情况 测试网站:https://caniuse.com/#search=vw css3test:https://airen.github.io/css3test/,https://gith ...

  3. 图片的描述生成任务、使用迁移学习实现图片的描述生成过程、CNN编码器+RNN解码器(GRU)的模型架构、BahdanauAttention注意力机制、解码器端的Attention注意力机制

    日萌社 人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新) Encoder编码器-Decoder解码器框架 + Atten ...

  4. influxdb基础(五)——数据的备份与恢复(influxd backup/influxd restore)

    文章目录 一.前言 二.备份和恢复元数据 1.备份元数据 2.恢复元数据 三.备份和恢复DB数据 1.备份DB数据 2.恢复DB数据 四.总结 一.前言 influxdb中有两种数据可以备份,一种是元 ...

  5. turf:计算面积、折线长度和两点距离

    一.面积 官网 API // 面积计算 var polygon = turf.polygon([[[116.24616622924805, 40.0312614440918],[116.2368965 ...

  6. word标尺灰色_如何在Microsoft Word中使用标尺

    word标尺灰色 Word's rulers let you control the margins of your page and the indentation of paragraphs. T ...

  7. word标尺灰色_如何在Microsoft Word 2013中更改标尺测量单位

    word标尺灰色 In Word, you can select one of several units of measurement for the ruler. You may be worki ...

  8. word标尺灰色_如何在Microsoft Word中更改标尺测量单位

    word标尺灰色 In Word, you can select one of several units of measurement for the ruler. This is useful w ...

  9. Pascal面试考试题库和答案(命令式和过程式编程语言学习资料)

    命令式和过程式编程语言Pascal面试考试题库和答案 1.Pascal是什么? Pascal 是一种命令式和过程式编程语言.它由 Niklaus Wirth 于 1970 年开发.它遵循结构化编程和数 ...

最新文章

  1. php 开发商城 注意,php开发微商城要注意什么
  2. m_Orchestrate learning system---二十四、thinkphp里面的ajax如何使用
  3. django学习(1)-----项目组成
  4. dart系列之:dart优秀的秘诀-隔离机制
  5. java 多线程面试题
  6. python中实现多路分支的最佳控制结构是_哪个选项是实现多路分支的最佳控制结构? (1.3分)_学小易找答案...
  7. linux的/dev内容介绍
  8. Ubuntu卸载图形界面
  9. 防淘宝关闭二维码案例
  10. 深入解读Linux进程调度系列(5)——调度的入口
  11. Hadoop权威指南读书笔记(2) — Yarn简介及Capacity Fair Scheduler
  12. pythonqt5教程从零开始_pyQt5 QtDesigner 简易入门教程
  13. JAVA实训心得体会(精选4篇)
  14. Jetson 系列——nvidia jetson nano设置声卡
  15. esxi安装系统ndows,ESXI 安装 Windows Server 2012过程
  16. RA8835驱动320240液晶模块例程
  17. 19电子设计速成实战宝典pdf_ALTIUMDESIGNER19(中文版)电子设计速成实战宝典
  18. html显示文件夹图片,Html读取本地文件夹下图片并显示
  19. JavaScript就这么回事(好收藏,哪天忘了可以查一查)
  20. Neural Machine Translation by Jointly Learning to Align and Translate论文及代码助解

热门文章

  1. 2013 8.26   总结记录下,别忘了哈
  2. 洛谷P2672 推销员
  3. 家用机器人风口来临,但巨头围猎背后的前景不容乐观
  4. Nginx反向代理以及负载均衡配置
  5. 英特尔发现Spectre和Meltdown 补丁对性能影响程度为0-21%
  6. 十万浙企上云 阿里云崛起的最大征候?
  7. JQuery 进入页面默认给已赋值的复选框打钩
  8. Centos环境下实现DNS的智能解析
  9. 错误代码为0xC000218,0x00000051的蓝屏错误,或是提示 System32/config missing之类的注册表错误...
  10. Oracle内部错误ORA-07445:[_memcmp()+88] [SIGSEGV]一例