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

Возврат ограниченных генериков из функций и методов

Я хотел бы создать функцию, которая возвращает объект, соответствующий протоколу, но протокол использует typealias. Учитывая следующий пример игрушки:

protocol HasAwesomeness {
    typealias ReturnType
    func hasAwesomeness() -> ReturnType
}

extension String: HasAwesomeness {
    func hasAwesomeness() -> String {
        return "Sure Does!"
    }
}

extension Int: HasAwesomeness {
    func hasAwesomeness() -> Bool {
        return false
    }
}

String и Int были расширены, чтобы соответствовать HasAwesomeness, и каждый из них реализует метод hasAwesomeness() для возврата другого типа.

Теперь я хотел бы создать класс, который возвращает объект, который соответствует протоколу HasAwesomeness. Мне все равно, что такое класс, просто я могу отправить сообщение hasAwesomenss(). Когда я пытаюсь выполнить следующее, я генерирую ошибку компиляции:

class AmazingClass: NSObject {
    func returnsSomethingWithAwesomeness(key: String) -> HasAwesomeness {
        ...
    }
}

ОШИБКА: Протокол "HasAwesomeness" может использоваться только в качестве общего ограничения, поскольку он имеет собственные или связанные требования типа

Как вы можете себе представить, целью returnsSomethingWithAwesomeness является возвращение a String или Int на основе параметра key. Ошибка, которую компилятор выбрасывает sorta-sorta, имеет смысл, почему она запрещена, но она дает представление об исправлении синтаксиса.

func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T
{
    ...
}

Хорошо, мое чтение - это метод returnsSomethingWithAwesomeness - это общий метод, который возвращает любой тип T, который имеет подтип HasAwesomness. Но в следующей реализации возникают больше ошибок типа компиляции:

func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T
{
    if key == "foo" {
        return "Amazing Foo"
    }
    else {
        return 42
    }
}

ОШИБКА: Тип 'T' не соответствует протоколу 'StringLiteralConvertible'

ОШИБКА: Тип 'T' не соответствует протоколу IntegerLiteralConvertible

Хорошо, теперь я застрял. Кто-нибудь, пожалуйста, помогите заполнить пробелы в моем понимании типов и дженериков, возможно, указывая на полезные ресурсы?

4b9b3361

Ответ 1

Я думаю, что ключом к пониманию того, что здесь происходит, является различение вещей, которые динамически определяются во время выполнения, и вещи, которые статически определяются во время компиляции. Это не помогает, что на большинстве языков, таких как Java, протоколы (или интерфейсы) связаны с получением полиморфного поведения во время выполнения, тогда как в Swift протоколы со связанными типами также используются для получения полиморфного поведения во время компиляции.

Всякий раз, когда вы видите общий заполнитель, например T в вашем примере, какой тип заполняется для этого T, определяется во время компиляции. Итак, в вашем примере:

func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T

говорит: returnsSomethingWithAwesomeness - это функция, которая может работать на любом типе T, только если T соответствует HasAwesomeness.

Но то, что заполняется для T, определяется в точке returnsSomethingWithAwesomeness, называется - Swift будет просматривать всю информацию на сайте вызова и решать, какой тип T, и заменить все T заполнители с этим типом. *

Итак, предположим, что на сайте вызова выбор T - это String, вы можете думать о returnsSomethingWithAwesomeness как переписываемом со всеми вхождениями placeholder T, замененными на String:

// giving the type of s here fixes T as a String
let s: String = returnsSomethingWithAwesomeness("bar")

func returnsSomethingWithAwesomeness(key: String) -> String {
    if key == "foo" {
        return "Amazing Foo"
    }
    else {
        return 42
    }
}

Примечание. T заменяется на String, а не с типом HasAwesomeness. HasAwesomeness используется только как ограничение, т.е. ограничение возможных типов T.

Когда вы смотрите на это так, вы можете видеть, что return 42 в else не имеет смысла - как вы могли бы вернуть 42 из функции, которая возвращает строку?

Чтобы returnsSomethingWithAwesomeness мог работать с тем, что заканчивается T, Swift ограничивает использование только тех функций, которые гарантированно будут доступны из заданных ограничений. В этом случае все, что мы знаем о T, это то, что оно соответствует HasAwesomeness. Это означает, что вы можете вызвать метод returnsSomethingWithAwesomeness на любом T или использовать его с другой функцией, которая ограничивает тип HasAwesomeness или назначает одну переменную типа T другой (все назначения поддержки типов) и все.

Вы не можете сравнить его с другими Ts (нет гарантии, что он поддерживает ==). Вы не можете создавать новые (кто знает, будет ли T иметь соответствующий метод инициализации?). И вы не можете создать его из строкового или целочисленного литерала (для этого потребуется T соответствовать либо StringLiteralConvertible, либо IntegerLiteralConvertible, что не обязательно - следовательно, эти две ошибки при попытке создать тип, используя один из эти виды литералов).

Можно написать общие функции, которые возвращают общий тип, который все соответствует протоколу. Но то, что будет возвращено, будет конкретным типом, а не протоколом, поэтому тип не будет определяться динамически. Например:

func returnCollectionContainingOne<C: ExtensibleCollectionType where C.Generator.Element == Int>() -> C {

    // this is allowed because the ExtensibleCollectionType procol 
    // requires the type implement an init() that takes no parameters
    var result = C()

    // and it also defines an `append` function that allows you to do this:
    result.append(1)

    // note, the reason it was possible to give a "1" as the argument to
    // append was because of the "where C.Generator.Element == Int" part
    // of the generic placeholder constraint 

    return result
}

// now you can use returnCollectionContainingOne with arrays:
let a: [Int] = returnCollectionContainingOne()

// or with ContiguousArrays:
let b: ContiguousArray = returnCollectionContainingOne()

Подумайте о returnCollectionContainingOne в этом коде, поскольку в действительности это две функции, одна из которых реализована для ContiguousArray и одна для Array, написанная автоматически компилятором в точке, которую вы вызываете (и, следовательно, где она может исправить C для определенного типа). Не одна функция, которая возвращает протокол, а две функции, возвращающие два разных типа. Таким образом, таким же образом returnsSomethingWithAwesomeness не может возвращать либо String, либо Int во время выполнения на основе некоторого динамического аргумента, вы не могли бы написать версию returnCollectionContainingOne, которая вернула бы массив или смежный массив. Все, что он может вернуть, это T, и во время компиляции любой T действительно может быть заполнен компилятором.

* это небольшое упрощение того, что делает компилятор, но он делает это для объяснения.