Вы допустили почти все основные ошибки, с которыми вы можете столкнуться при разработке протоколов, но все они являются чрезвычайно распространенными ошибками, и это неудивительно. Все так делают, когда начинают, и требуется время, чтобы привести свою голову в нужное место. Я думаю об этой проблеме уже почти четыре года и даю докладов по этому вопросу, и я все еще путаюсь с ней, и мне приходится резервировать и пересматривать свои протоколы, потому что я впадаю в те же ошибки.
Протоколы не являются заменой абстрактных классов. Существует два совершенно разных вида протоколов: простые протоколы и 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. Я уверен, что это вызовет еще больше вопросов.