Стирание типа по протоколу с требованием типа Self - PullRequest
2 голосов
/ 04 апреля 2019

У меня есть протокол с именем Shape, который соответствует Comparable. В конечном итоге я хотел бы создать массив того, что соответствует этому протоколу, даже если они не одного и того же подтипа.

Я создал несколько классов, соответствующих Shape, а именно Triangle, Square и Rectangle. Я хочу определить другой класс с именем Drawing, который может принимать массив любой формы.

//: 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 Ответ

7 голосов
/ 05 апреля 2019

Вы допустили почти все основные ошибки, с которыми вы можете столкнуться при разработке протоколов, но все они являются чрезвычайно распространенными ошибками, и это неудивительно. Все так делают, когда начинают, и требуется время, чтобы привести свою голову в нужное место. Я думаю об этой проблеме уже почти четыре года и даю докладов по этому вопросу, и я все еще путаюсь с ней, и мне приходится резервировать и пересматривать свои протоколы, потому что я впадаю в те же ошибки.

Протоколы не являются заменой абстрактных классов. Существует два совершенно разных вида протоколов: простые протоколы и PAT (протоколы с ассоциированным типом).

Простой протокол представляет конкретный интерфейс и может использоваться некоторыми способами, которыми вы можете использовать абстрактный класс в других языках, но его лучше всего рассматривать как список требований. Тем не менее, вы можете думать о простом протоколе как о типе (он на самом деле становится экзистенциальным, но он довольно близок).

PAT - это инструмент для ограничения других типов, так что вы можете дать этим типам дополнительные методы или передать их в универсальные алгоритмы. Но PAT это не тип. Его нельзя поместить в массив. Это не может быть передано в функцию. Его нельзя хранить в переменной. Это не тип. Нет такой вещи, как «Сопоставимый». Существуют типы, которые соответствуют сопоставимым.

Можно использовать ластики типов, чтобы принудительно преобразовать PAT в конкретный тип, но это почти всегда ошибка и очень негибкая, и это особенно плохо, если для этого нужно изобрести ластик нового типа. Как правило (и есть исключения), предположите, что если вы пытаетесь найти ластик типов, вы, вероятно, неправильно разработали свои протоколы.

Когда вы сделали Comparable (и через него Equatable) требованием Shape, вы сказали, что Shape - это PAT. Вы этого не хотели. Но опять же, вы не хотели Shape.

Трудно точно знать, как спроектировать это, потому что вы не показываете какие-либо варианты использования. Протоколы появляются из вариантов использования. Они обычно не возникают из модели. Поэтому я расскажу, как вы начали, а затем мы поговорим о том, как реализовать дальнейшие части, основываясь на том, что вы будете делать с ним.

Во-первых, вы должны смоделировать эти виды фигур и типы значений. Они просто данные. Нет оснований для ссылочной семантики (классов).

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

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

Я удалил Квадрат, потому что это очень плохой пример. Квадраты не являются правильными прямоугольниками в моделях наследования (см. Circle-Ellipse Problem ). Вам это удается, используя неизменные данные, но неизменные данные не являются нормой в Swift.

Кажется, вы хотели бы вычислить площадь по ним, поэтому я предполагаю, что есть какой-то алгоритм, который заботится об этом. Он может работать в «регионах, которые предоставляют площадь».

protocol Region {
    var area: Double { get }
}

И мы можем сказать, что Треугольники и Прямоугольники соответствуют Региону посредством ретроактивного моделирования. Это можно сделать где угодно; это не должно быть решено в то время, когда модели созданы.

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

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

Теперь Region - это простой протокол, поэтому нет проблем с его помещением в массив:

struct Drawing {
    var areas: [Region]
}

Это оставляет изначальный вопрос равенства. Это имеет много тонкостей. Первое и самое важное, что в Swift «равно» (по крайней мере, когда он связан с протоколом Equatable) означает «может быть заменено для любой цели». Так что если вы говорите «треугольник == прямоугольник», вы должны иметь в виду «в любом контексте, что этот треугольник можно использовать, вы можете использовать вместо этого прямоугольник». Тот факт, что они имеют одинаковую область, кажется не очень полезным способом определения этой замены.

Точно так же не имеет смысла говорить "треугольник меньше, чем прямоугольник". Имеет смысл сказать, что площадь треугольника меньше площади прямоугольника, но это означает, что тип Area соответствует Comparable, а не сами фигуры. (В вашем примере Area эквивалентно Double.)

Определенно, есть способы пойти вперед и проверить на равенство (или что-то похожее на равенство) среди регионов, но это сильно зависит от того, как вы планируете его использовать. Это не вытекает естественно из модели; это зависит от вашего варианта использования. Сила Swift заключается в том, что он позволяет согласовывать одни и те же объекты модели со многими различными протоколами, поддерживая множество различных вариантов использования.

Если вы можете дать еще несколько указаний о том, куда вы идете с этим примером (как будет выглядеть вызывающий код), то я могу расширить это. В частности, начните с того, что немного уточните Drawing. Если вы никогда не обращаетесь к массиву, то не имеет значения, что вы в него вставили. Как будет выглядеть желательный цикл for над этим массивом?

Пример, над которым вы работаете, является почти точно примером, используемым в самом известном протоколе, ориентированном на протоколирование: Протоколно-ориентированное программирование в Swift , также называемое "Crusty talk". Это хорошее место, чтобы начать понимать, как думать в Swift. Я уверен, что это вызовет еще больше вопросов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...