【问题标题】:extend generic Array<T> to adopt protocol扩展泛型 Array<T> 以采用协议
【发布时间】:2015-12-26 21:57:19
【问题描述】:

假设我已经定义了这样一个协议:

protocol EuclideanPoint {
    func distance(other: Self) -> Double
    func dimension() -> UInt
}

现在我想扩展 [Float] 和 [Double] 以采用该协议。

但是下面的代码:

extension [Float]: EuclideanPoint {
    func distance(other: [Float]) {
        return Double(zip(self, other).map{a, b in pow(a-b,2)}.reduce(0, combine: +))
    }
    func dimension() {
        return UInt(self.count)
    }
}

由于错误而无效

错误:必须在非专用泛型类型“Array”上声明受约束的扩展,并使用“where”子句指定的约束

我发现了类似的问题(如this),但建议的解决方案是使用extension CollectionType where Generator.Element == S { ... },但在这种情况下会导致错误:

错误:协议 'CollectionType' 只能用作通用约束,因为它具有 Self 或关联的类型要求

有什么解决办法吗?

编辑:

使用建议的解决方案:

protocol DoubleConvertibleType {
    var doubleValue: Double { get }
}

extension Double : DoubleConvertibleType { var doubleValue: Double { return self         } }
extension Float  : DoubleConvertibleType { var doubleValue: Double { return Double(self) } }
extension CGFloat: DoubleConvertibleType { var doubleValue: Double { return Double(self) } }

extension Array where Element : DoubleConvertibleType {
    func distance(other: Array) -> Double {
        return Double(zip(self, other).map{ pow($0.0.doubleValue - $0.1.doubleValue, 2) }.reduce(0, combine: +))
    }

    func dimension() -> UInt {
        return UInt(self.count)
    }
}

为[Double] 和[Float] 提供.distance() 和.dimension() 方法。然而,[Double] 或 [Float] 不能用来代替符合 EuclideanPoint 协议所需的东西,从而产生错误:

错误:类型“[Double]”不符合协议“EuclideanPoint”

【问题讨论】:

标签: swift generics


【解决方案1】:

已编辑


以下解决方案有点通用,符合协议EuclidianPoint,并基于两个假设:

  • 我们可以在 EuclideanPoint 协议中为方法 distance 的蓝图包含一个泛型类型约束,并且我们将使用泛型而不是 Self ([T])。但是,我们将(在编译时)确定 [T] 与 Self 的类型相同(这里是 [Double]、[Float] 或 [Int] 类型的 Self),并且 确定 [T] 符合协议EuclidianPoint。

  • 1234563 /em>。 Swift 中的这些.map、.reduce 等功能确实简洁实用,但在许多应用程序中只是对幕后 for 循环的包装,因此您不会在手动命令中失去任何性能风格。事实上,.reduce 在减少数组的同时由于重复的数组复制分配而被认为是非常非可选的(我不会在这里更多地讨论......)。无论如何,也许您可​​以利用我的示例并将其调整回更实用的范式。

我们从一个自定义类型协议开始,MyTypes,它将充当我们想要包含在泛型中的类型的接口。我们还添加了稍微更新的EuiclidianPoint 协议,其中我们使用协议MyTypes 作为distance (...) 函数蓝图中使用的通用T 的类型限制。

/* Used as type constraint for Generator.Element */
protocol MyTypes {
    func -(lhs: Self, rhs: Self) -> Self
    func +=(inout lhs: Self, rhs: Self)
}

extension Int : MyTypes { }
extension Double : MyTypes { }
extension Float : MyTypes { }
    /* Extend with the types you wish to be covered by the generic ... */

/* Used as extension to Array : blueprints for extension method
to Array where Generator.Element are constrainted to MyTypes */
protocol EuclideanPoint {
    func distance<T: MyTypes> (other: [T]) -> Double?
    func dimension() -> UInt
}

请注意,我已将 distance 的 Double 返回更改为可选;您可以随意处理,但如果self 和other 数组的长度不同,或者Self 和[T] 类型不同,则需要显示不符合项——我会在这里使用nil。

我们现在可以通过EuclidianPoint 协议实现我们对Array 的扩展:

/* Array extension by EuclideanPoint protocol */
extension Array : EuclideanPoint {

    func distance<T: MyTypes> (other: [T]) -> Double? {
        /* [T] is Self? proceed, otherwise return nil */
        if let a = self.first {
            if a is T && self.count == other.count {
                var mySum: Double = 0.0
                for (i, sElement) in self.enumerate() {
                    mySum += pow(((sElement as! T) - other[i]) as! Double, 2)
                }
                return sqrt(mySum)
            }
        }
        return nil
    }

    func dimension() -> UInt {
        return UInt(self.count)
    }
}

请注意,在 distance 函数的内部 if 子句中,我们使用显式向下转换为 T,但由于我们已断言 Self 的元素属于 T 类型,因此没关系。

不管怎样,有了这个,我们就完成了,我们可以测试我们的“通用”数组扩展,我们注意到它现在也符合您的协议EuclidianPoint。

/* Tests and Examples */
let arr1d : [Double] = [3.0, 4.0, 0.0]
let arr2d : [Double] = [-3.0, -4.0, 0.0]
let arr3d : [Double] = [-3.0, -4.0]

let arr1f : [Float] = [-3.0, -4.0, 0.0]

let arr1i = [1, 2, 3]

let _a = arr1d.dimension() // 3, OK
let _b = arr1d.distance(arr2d) // 10, OK (A->B dist)
let _c = arr1d.distance(arr1f) // nil (Incomp. types)
let _d = arr1d.distance(arr3d) // nil (Incomp. sizes)
let _e = arr1i.distance(arr1d) // nil (Incomp. types)

    /* for use in function calls: generic array parameters constrained to
       those that conform to protocol 'EuclidianPoint', as requested     */
func bar<T: MyTypes, U: protocol<EuclideanPoint, _ArrayType> where U.Generator.Element == T> (arr1: U, _ arr2: U) -> Double? {

    // ...

    return arr1.distance(Array(arr2))
        /* We'll need to explicitly tell the distance function
           here that we're sending an array, by initializing an 
           array using the Array(..) initializer                */
}
let myDist = bar(arr1d, arr2d) // 10, OK

好的!


我的第一个答案还剩下一个注释:

泛型 Array 到协议的扩展实际上是最近才在这里提出的:

共识是您不能以您可能期望的“整洁迅速”的方式将数组通用扩展为协议。然而,有一些解决方法可以模仿这种行为,一种是我在上面使用过的。如果您对另一种方法感兴趣,我建议您查看此线程。

【讨论】:

  • 你不能它会抛出一个错误 = type 'Element' constrained to non-protocol type 'Double'
  • 您可以使用扩展数组 where Element: FloatingPointType 或自定义协议
  • @LeoDabus 请注意,FloatingPointType 出于某种原因不包含基本的二元运算符,因此您需要在附加协议中(或类似的方式)向这些运算符添加蓝图
  • 你也可以添加一个doubleValue属性
  • @LeoDabus 啊,我不知道,谢谢!关于您的第一个答案:是的,我要快速解决这个问题,在我的 iPad 上写,现在在笔记本电脑上 :) 将修复
【解决方案2】:

您可以扩展SequenceType 而不是Array

extension SequenceType where Generator.Element == Float {
//
}

【讨论】:

  • SequenceType 没有计数属性。 EuclideanPoint 协议需要类似的属性
  • @fferri @Adam : (self as! [Float]).count ? self 现在有计数了
【解决方案3】:

前言:正如@difri 在评论中正确提到的那样,当同时使用通用约束时,我们还不能创建符合协议的扩展。已经有几个雷达打开了 - 搜索 “带约束的类型扩展不能有继承子句” 会得到几个。

实际答案:在@LeoDabus awesome answer 和我的实验基础上,我得出以下结论:

protocol DoubleConvertibleType {
    var doubleValue: Double { get }
}

extension Double : DoubleConvertibleType { var doubleValue: Double { return self         } }
extension Float  : DoubleConvertibleType { var doubleValue: Double { return Double(self) } }
extension CGFloat: DoubleConvertibleType { var doubleValue: Double { return Double(self) } }

extension Array where Element : DoubleConvertibleType {
    func distance(other: Array) -> Double {
        return Double(zip(self, other).map{ pow($0.0.doubleValue - $0.1.doubleValue, 2) }.reduce(0, combine: +))
    }

    func dimension() -> UInt {
        return UInt(self.count)
    }
}

测试它

let arr1 = [1.5, 2, 3]
let arr2 = [5.5, 2, 3]
let arrD = arr1.distance(arr2)

有些正确的打印

16

要获得正确答案(至少我会怀疑),您必须将 distance 包装成 sqrt:

return sqrt(Double(zip(self, other).map{ pow($0.0.doubleValue - $0.1.doubleValue,2) }.reduce(0, combine: +)))

然后正确打印

4

【讨论】:

  • 这解决了问题细节中描述的问题(而且做得很巧妙!),但我认为值得一提的是它并没有解决问题标题中提出的问题:we不能(以简洁的方式)将泛型类型扩展到协议。请注意,上面的解决方案删除了​​等式的协议EuclideanPoint。好的答案,加 1,但只是为了让人们不要点击这里并认为这解决了更棘手的问题“通用 Array 符合协议”!
  • @mescalinum 我一直在摆弄它,现在试图避免为此目的创建一个特殊协议,但似乎这是不可避免的。
  • @dfri 你是绝对正确的,完全忘记我们试图遵守的实际协议:D hmmm
  • 我认为这可能适用于问题海报(因为他提出了相同的答案),并且应该被接受。但是,如果可能的话,请在您的答案中添加它只是这样做的:解决特定问题,而不是解决协议骗子的通用数组!那么,如果有人想全力以赴真正攻击后者,他们可能应该参考以下线程:stackoverflow.com/questions/34467606/…
  • @dfri 这样做是因为这是一个绝对有效的观点 - 谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多