Подтвердить что ты не робот

Почему не связанные типы протоколов используют синтаксис типового типа в 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 }
}

Есть ли какая-то глубокая (или, возможно, просто очевидная и утерянная для меня) причина, по которой язык не принял последний синтаксис?

4b9b3361

Ответ 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 команда имеет хорошую рецензию на дженерики, протоколов и связанных с ними функций здесь.