Measurements 和 Units,第三部分
更新: 本系列其他文章: (1) Measurements 和 Units 概览 (2) 乘法和除法 (3) 内容提炼(本文) (4) 幽灵类型 Swift 中 Measurements 和 Units 的系列文章中,仍然有一些收尾工作要做。如果你还没有看过之前的文章的话,可以在第一部分中找到 Foundation 框架中 Measurements 和 Units 接口的大致介绍,并在第二部分中看到我如何扩展该系统用于类型安全的乘法和除法。 在计算过程中保持单位在我们之前的文章里面,乘法和除法在计算之前总是会将单位转换到各自的默认单位,正如我们在 到目前为止,我们仍然使用一个默认的单位映射作为计算结果的单位。比如说, 从实现的角度来说这是一个最简单的方案,但是可以想象的是,方法的调用者会更希望在计算中尽可能保持单位一致。如果我输入米和秒,则尽可能的返回米每秒,但是如果我输入千米和小时,则希望返回千米每小时。 首选单位映射我们可以在协议里面添加一个新的方法来实现这个目标,这个方法会通过优先级排序返回一个单位映射的集合。在计算的时候我们通过遍历这个集合,为当前的计算找出最合适的单位映射。如果没有匹配到,则回退到默认的单位映射。我把这个方法叫做 protocol UnitProduct { associatedtype Factor1: Dimension associatedtype Factor2: Dimension associatedtype Product: Dimension static func defaultUnitMapping() -> (Factor1,Factor2,Product) static func preferredUnitMappings() -> [(Factor1,Product)] } 我们需要提供一个返回空数组的默认实现,这样的话如果协议的实现者不需要这个功能,他也可以选择忽略这个方法。 extension UnitProduct { // 默认实现 static func preferredUnitMappings() -> [(Factor1,Product)] { return [] } } 接下来,我们需要提供一系列的便捷方法,他们的功能是为乘法或者除法的参数匹配出合适的单位映射。这个方法需要三个重载的形式,取决于 extension UnitProduct { static func unitMapping(factor1: Factor1,factor2: Factor2) -> (Factor1,Product) { let match = preferredUnitMappings().first { (f1,f2,_) in f1 == factor1 && f2 == factor2 } return match ?? defaultUnitMapping() } static func unitMapping(product: Product,Product) { let match = preferredUnitMappings().first { (_,p) in p == product && f2 == factor2 } return match ?? defaultUnitMapping() } static func unitMapping(product: Product,factor1: Factor1) -> (Factor1,_,p) in p == 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).value let result = Measurement(value: value,unit: resultUnit) // 转换到首选的单位 let (desiredUnit,_) = UnitType.unitMapping(product: lhs.unit,factor2: rhs.unit) return result.converted(to: desiredUnit) } 一切准备就绪,我们可以为 extension UnitLength { static func preferredUnitMappings() -> [(UnitSpeed,UnitDuration,UnitLength)] { return [ (.kilometersPerHour,.hours,.kilometers),(.milesPerHour,.miles),(.knots,.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,这可能在循环中会是个问题。但像我们在这里做的这种简单的计算应该尽可能的快。 乘方的问题如果你使用过 extension UnitArea: UnitProduct { typealias Factor1 = UnitLength typealias Factor2 = UnitLength typealias Product = UnitArea static 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 '*' 原因是我们有两个乘法重载运算符,一个是 最好的解决方案是,我们能够给其中一个方法添加一个通用的约束,类似于 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 语句只能包含 单独给乘方的协议我们通过引入一个单独的协议, protocol UnitSquare { associatedtype Factor: Dimension associatedtype Product: Dimension static func defaultUnitMapping() -> (Factor,Factor,Product) static func preferredUnitMappings() -> [(Factor,Product)] } 我们在这里就不展开其具体实现了,因为这个协议和 如果我们将 extension UnitArea: UnitSquare { typealias Factor = UnitLength typealias Product = UnitArea static func defaultUnitMapping() -> (UnitLength,.squareMeters) } } let width = Measurement(value: 4,unit: UnitLength.meters) let area: Measurement<UnitArea> = width * height // → 24.0 m2 area / width // → 6.0 m area / height // → 4.0 m 自身相除谜题的最后一部分,我想应该是实现两个相同类型的除法,比如说 6 米 / 4 米 = 1.5。计算结果应该是一个没有单位的量(换句话说是一个 支持这一特性十分简单,我们只需要再增加一个重载的除法。可以描述为:输入两个相同 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 代码我将在本系列文章中讨论到的所有代码放到了一个叫
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |