具有自我类型要求的协议的类型擦除

时间:2019-04-04 20:40:47

标签: swift

我有一个名为Shape的协议,符合Comparable。最终,即使它们不是同一子类型,我仍然希望创建一个符合该协议的数组。

我创建了一些符合Shape的类,即TriangleSquareRectangle。我想做的是定义另一个名为Drawing的类,它可以接受任何Shape的数组。

//: Playground - noun: a place where people can play
import Foundation
import UIKit

protocol Shape: Comparable {
    var area: Double { get }
}

extension Shape {
    static func < (lhs: Self, rhs: Self) -> Bool {
        return lhs.area < rhs.area
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        return lhs.area == rhs.area
    }
}

class Triangle: Shape {
    let base: Double
    let height: Double

    var area: Double { get { return base * height / 2 } }

    init(base: Double, height: Double) {
        self.base = base
        self.height = height
    }
}

class Rectangle: Shape {
    let firstSide: Double
    let secondSide: Double

    var area: Double { get { return firstSide * secondSide } }

    init(firstSide: Double, secondSide: Double) {
        self.firstSide = firstSide
        self.secondSide = secondSide
    }
}

class Square: Rectangle {
    init(side:  Double) {
        super.init(firstSide: side, secondSide: side)
    }
}

class Drawing {
    //Protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements
    let shapes: [Shape]
    init(shapes: [Shape]) {
        self.shapes = shapes
    }
}

但是,当我尝试使用Shape作为数组类型时,出现以下错误Protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements。 如何声明包含任何形状的数组?

1 个答案:

答案 0 :(得分:7)

您几乎遇到了设计协议时可能遇到的每个基本错误,但是它们都是极其常见的错误,不足为奇。每个人在开始时都以这种方式进行操作,并且需要一段时间才能使您的头脑处于正确的位置。我已经思考这个问题将近四年了,并给出talks这个问题,但我仍然搞砸了,因为我犯了同样的错误,所以必须备份并重新设计协议。

协议不能代替抽象类。有两种完全不同的协议:简单协议和PAT(具有关联类型的协议)。

一个简单的协议代表一个具体的接口,可以用某些方式使用其他语言中的抽象类,但是最好将其视为一系列要求。就是说,您可以将一个简单的协议视为一种类型(它实际上已成为一种存在的协议,但是非常接近)。

PAT是用于约束其他类型的工具,因此您可以为这些类型提供其他方法,或将其传递给通用算法。但是,PAT不是一种类型。它不能放在数组中。无法将其传递给函数。它不能保存在变量中。这不是一种类型。没有“可比”之类的东西。有些类型符合可比较。

可以使用类型的橡皮擦将PAT强制转换为具体的类型,但这几乎总是一个错误并且非常不灵活,如果您必须发明一种新型的橡皮擦来做到这一点尤其糟糕。通常(假设有例外),假设您要使用类型橡皮擦,则可能是设计了错误的协议。

当您将“可比”(并通过“等于”)定为Shape的要求时,您说Shape是PAT。你不想那样但是又一次,您不想要Shape。

很难确切地知道如何设计它,因为您没有显示任何用例。协议来自用例。它们通常不会从模型中产生。因此,我将为您提供入门方法,然后我们将讨论如何根据您的需要实施更多的工作。

首先,您可以将这些形状建模为值类型。它们只是数据。没有理由使用引用语义(类)。

struct Triangle: Equatable {
    var base: Double
    var height: Double
}

struct Rectangle: Equatable {
    var firstSide: Double
    var secondSide: Double
}

我删除了Square,因为这是一个非常糟糕的例子。在继承模型中,正方形不是正确的矩形(请参见Circle-Ellipse Problem)。您碰巧使用不可变数据来摆脱它,但是不可变数据不是Swift中的常识。

似乎您想计算这些区域,所以我假设有一些算法对此很在意。它可以在“提供区域的区域”上工作。

protocol Region {
    var area: Double { get }
}

我们可以说,通过追溯建模,三角形和矩形与Region一致。这可以在任何地方完成;创建模型时不必决定。

extension Triangle: Region {
    var area: Double { get { return base * height / 2 } }
}

extension Rectangle: Region {
    var area: Double { get { return firstSide * secondSide } }
}

Now Region是一个简单的协议,因此将其放入数组没有问题:

struct Drawing {
    var areas: [Region]
}

留下了最初的平等问题。那有很多微妙之处。第一个也是最重要的一点是,在Swift中,“等于”(至少在与Equatable协议绑定时)意味着“可以用于任何目的”。因此,如果您说“三角形==矩形”,则必须表示“在可以使用该三角形的任何情况下,您都可以随意使用矩形”。它们恰好具有相同的面积这一事实似乎并不是定义该替代的非常有用的方法。

类似地,说“三角形小于矩形”也没有意义。有意义的是说三角形的面积小于矩形的面积,但这仅意味着Area的类型符合Comparable,而不是形状本身。 (在您的示例中,Area等效于Double。)

肯定有一些方法可以测试区域之间的平等性(或类似的平等性),但这在很大程度上取决于您打算如何使用它。它不是自然地从模型中产生的。这取决于您的用例。 Swift的强大之处在于,它允许相同的模型对象符合许多不同的协议,从而支持许多不同的用例。

如果您可以通过本示例提供更多要执行的操作的指针(调用代码的外观),那么我可以对此进行扩展。特别要先充实Drawing。如果您从不访问该数组,那么放进它就没关系。在该数组上,理想的for循环是什么样的?

您正在处理的示例几乎与最著名的面向协议的编程对话:Protocol-Oriented Programming in Swift(也称为“ Crusty对话”)中使用的示例相同。这是一个开始了解如何在Swift中思考的好地方。我相信它将引发更多问题。