Почему не связанные типы протоколов используют синтаксис типового типа в Swift?
Меня смущает различие между синтаксисом, используемым для связанных типов протоколов, с одной стороны, и универсальными типами с другой.
Например, в Swift можно определить универсальный тип, используя что-то вроде
struct Stack<T> {
var items = [T]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
в то время как один определяет протокол со связанными типами, используя что-то вроде
protocol Container {
typealias T
mutating func append(item: T)
var count: Int { get }
subscript(i: Int) -> T { get }
}
Почему последний не просто:
protocol Container<T> {
mutating func append(item: T)
var count: Int { get }
subscript(i: Int) -> T { get }
}
Есть ли какая-то глубокая (или, возможно, просто очевидная и утерянная для меня) причина, по которой язык не принял последний синтаксис?
Ответы
Ответ 1
Это было рассмотрено несколько раз в devlist. Основной ответ заключается в том, что связанные типы более гибкие, чем параметры типа. Хотя у вас есть конкретный случай здесь одного параметра типа, вполне возможно иметь несколько. Например, коллекции имеют тип элемента, но также тип индекса и тип генератора. Если вы специализируетесь на них полностью с параметризацией типа, вам придется говорить о таких вещах, как Array<String, Int, Generator<String>>
или тому подобное. (Это позволило бы мне создавать массивы, которые были бы индексированы чем-то другим, кроме Int, которое можно было бы считать особенностью, но также добавляет много сложности.)
Можно пропустить все это (Java делает), но тогда у вас есть меньше способов, которыми вы можете ограничить свои типы. Java на самом деле довольно ограничен в том, как он может ограничивать типы. Вы не можете иметь произвольный тип индексирования в своих коллекциях на Java. Scala расширяет систему типов Java со связанными типами, как Swift. Связанные типы были невероятно мощными в Scala. Они также являются постоянным источником путаницы и разрывания волос.
Независимо от того, стоит ли эта дополнительная мощность, это совершенно другой вопрос, и только время покажет. Но связанные типы определенно более мощные, чем простая параметризация типа.
Ответ 2
Ответ RobNapier (как обычно) довольно хорош, но только для альтернативной перспективы, которая может оказаться еще более полезной...
О связанных типах
Протокол - это абстрактный набор требований - контрольный список, который должен выполнять конкретный тип, чтобы сказать, что он соответствует протоколу. Традиционно каждый думает об этом контрольном списке поведения: методы или свойства, реализованные конкретным типом. Связанные типы - это способ присвоения имен тем, которые участвуют в таком контрольном списке, и, таким образом, расширение определения, в то же время оставляя его открытым в отношении того, как соответствующий тип реализует соответствие.
Когда ты видишь:
protocol SimpleSetType {
associatedtype Element
func insert(_ element: Element)
func contains(_ element: Element) -> Bool
// ...
}
Это означает, что для того, чтобы тип заявлял о соответствии SimpleSetType
, этот тип не только должен содержать функции insert(_:)
и contains(_:)
, эти две функции должны принимать параметры одного типа друг с другом. Но не имеет значения, какой тип этого параметра.
Вы можете реализовать этот протокол с универсальным или не универсальным типом:
class BagOfBytes: SimpleSetType {
func insert(_ byte: UInt8) { /*...*/ }
func contains(_ byte: UInt8) -> Bool { /*...*/ }
}
struct SetOfEquatables<T: Equatable>: SimpleSetType {
func insert(_ item: T) { /*...*/ }
func contains(_ item: T) -> Bool { /*...*/ }
}
Обратите внимание, что нигде BagOfBytes
или SetOfEquatables
определяют связь между SimpleSetType.Element
и типом, используемым в качестве параметра для их двух методов - компилятор SetOfEquatables
определяет, что эти типы связаны с правильными методами, поэтому они удовлетворяют требованию протокола для связанного с ним тип.
Об общих параметрах типа
Там, где связанные типы расширяют ваш словарный запас для создания абстрактных контрольных списков, параметры универсального типа ограничивают реализацию конкретного типа. Когда у вас есть общий класс, как это:
class ViewController<V: View> {
var view: V
}
Он не говорит о том, что существует много разных способов сделать ViewController
(если у вас есть view
), он говорит, что ViewController
- это реальная, конкретная вещь, и у него есть view
. Более того, мы не знаем точно, какой тип представления у любого данного экземпляра ViewController
, но мы знаем, что это должен быть View
(либо подкласс класса View
, либо тип, реализующий протокол View
... не говори)
Или, другими словами, написание универсального типа или функции является своего рода ярлыком для написания реального кода. Возьмите этот пример:
func allEqual<T: Equatable>(a: T, b: T, c: T) {
return a == b && b == c
}
Это имеет тот же эффект, как если бы вы прошли все типы Equatable
и написали:
func allEqual(a: Int, b: Int, c: Int) { return a == b && b == c }
func allEqual(a: String, b: String, c: String) { return a == b && b == c }
func allEqual(a: Samophlange, b: Samophlange, c: Samophlange) { return a == b && b == c }
Как вы можете видеть, мы создаем здесь код, реализуя новое поведение - во многом в отличие от типов, связанных с протоколами, где мы только описываем требования для выполнения чего-то еще.
TL;DR
Связанные типы и параметры универсальных типов являются очень разными инструментами: ассоциированные типы являются языком описания, а универсальные - языком реализации. Они имеют совершенно разные цели, хотя их использование иногда выглядит схожим (особенно, когда речь идет о тонких на первый взгляд различиях, таких как абстрактная схема для коллекций любого типа элемента и фактический тип коллекции, который все еще может иметь любой родительский элемент). Поскольку они очень разные звери, у них разный синтаксис.
дальнейшее чтение
Swift команда имеет хорошую рецензию на дженерики, протоколов и связанных с ними функций здесь.