简体   繁体   中英

Type erasure on protocol with Self type requirement

I have a protocol named Shape that conforms to Comparable . Ultimately I would like to create an array of whatever conformed to this protocol, even if they are not the same sub type.

I created some classes conforming to Shape , namely Triangle , Square and Rectangle . What I want to do is to define another class called Drawing that can accept an array of any 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
    }
}

However, when I try to use Shape as the type of the array, I get the following error Protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements . How can I declare an array containing any type of shapes?

You have hit just about every basic mistake you can hit in designing protocols, but they are all extremely common mistakes and not surprising. Everyone does it this way when they start, and it takes awhile to get your head in the right space. I've been thinking about this problem for almost four years now, and give talks on the subject, and I still mess it up and have to back up and redesign my protocols because I fall into the same mistakes.

Protocols are not a replacement for abstract classes. There are two completely different kinds of protocols: simple protocols, and PATs (protocols with associated type).

A simple protocol represents a concrete interface, and can be used in some of the ways you might use an abstract class in other languages, but is best thought of as a list of requirements. That said, you can think of a simple protocol as if it were a type (it actually becomes an existential, but it's pretty close).

A PAT is a tool for constraining other types so that you can give those types additional methods, or pass them to generic algorithms. But a PAT is not a type. It can't be put in an Array. It can't be passed to a function. It cannot be held in a variable. It is not a type. There is no such thing as "a Comparable." There are types that conform to Comparable.

It is possible to use type erasers to force a PAT into a concrete type, but it is almost always a mistake and very inflexible, and it's particularly bad if you have to invent a new type eraser to do it. As a rule (and there are exceptions), assume that if you're reaching for a type eraser you've probably mis-designed your protocols.

When you made Comparable (and through it Equatable) a requirement of Shape, you said that Shape is a PAT. You didn't want that. But then again, you didn't want Shape.

It's difficult to know precisely how to design this, because you don't show any use cases. Protocols emerge from use cases. They do not spring up from the model typically. So I'll provide how you get started, and then we can talk about how to implement further pieces based on what you would do with it.

First, you would model these kinds of shapes a value types. They're just data. There's no reason for reference semantics (classes).

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

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

I've deleted Square because it's a very bad example. Squares are not properly rectangles in inheritance models (see the Circle-Ellipse Problem ). You happen to get away with it using immutable data, but immutable data is not the norm in Swift.

It seems you'd like to compute area on these, so I assume there's some algorithm that cares about that. It could work on "regions that provide an area."

protocol Region {
    var area: Double { get }
}

And we can say that Triangles and Rectangles conform to Region through retroactive modeling. This can be done anywhere; it doesn't have to be decided at the time that the models are created.

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

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

Now Region is a simple protocol, so there's no problem putting it in an array:

struct Drawing {
    var areas: [Region]
}

That leaves the original question of equality. That has lots of subtleties. The first, and most important, is that in Swift "equals" (at least when tied to the Equatable protocol) means "can be substituted for any purpose." So if you say "triangle == rectangle" you have to mean "in any context that this triangle could be used, you're free to use the rectangle instead." The fact that they happen to have the same area doesn't seem a very useful way to define that substitution.

Similarly it is not meaningful to say "a triangle is less than a rectangle." What is meaningful is to say that a triangle's area is less than a rectangle's, but that's just means the type of Area conforms to Comparable , not the shapes themselves. (In your example, Area is equivalent to Double .)

There are definitely ways to go forward and test for equality (or something similar to equality) among Regions, but it highly depends on how you plan to use it. It doesn't spring naturally from the model; it depends on your use case. The power of Swift is that it allows the same model objects to be conformed to many different protocols, supporting many different use cases.

If you can give some more pointers of where you're going with this example (what the calling code would look like), then I can expand on that. In particular, start by fleshing out Drawing a little bit. If you never access the array, then it doesn't matter what you put in it. What would a desirable for loop look like over that array?

The example you're working on is almost exactly the example used in the most famous protocol-oriented programming talk: Protocol-Oriented Programming in Swift , also called "the Crusty talk." That's a good place to start understanding how to think in Swift. I'm sure it will raise even more questions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM