Как использовать общий протокол в качестве типа переменной

89

Допустим, у меня есть протокол:

public protocol Printable {
    typealias T
    func Print(val:T)
}

А вот и реализация

class Printer<T> : Printable {

    func Print(val: T) {
        println(val)
    }
}

Я ожидал, что я должен иметь возможность использовать Printableпеременную для вывода таких значений:

let p:Printable = Printer<Int>()
p.Print(67)

Компилятор жалуется на эту ошибку:

«протокол« Printable »может использоваться только как общее ограничение, потому что он имеет требования типа Self или связанные с ним»

Я делаю что-то неправильно ? Как бы то ни было, чтобы это исправить?

**EDIT :** Adding similar code that works in C#

public interface IPrintable<T> 
{
    void Print(T val);
}

public class Printer<T> : IPrintable<T>
{
   public void Print(T val)
   {
      Console.WriteLine(val);
   }
}


//.... inside Main
.....
IPrintable<int> p = new Printer<int>();
p.Print(67)

РЕДАКТИРОВАТЬ 2: Пример реального мира того, что я хочу. Обратите внимание, что это не будет компилироваться, но представляет то, чего я хочу достичь.

protocol Printable 
{
   func Print()
}

protocol CollectionType<T where T:Printable> : SequenceType 
{
   .....
   /// here goes implementation
   ..... 
}

public class Collection<T where T:Printable> : CollectionType<T>
{
    ......
}

let col:CollectionType<Int> = SomeFunctiionThatReturnsIntCollection()
for item in col {
   item.Print()
}
Тамерлан
источник
1
Вот соответствующая ветка на форумах разработчиков Apple с 2014 года, где этот вопрос решается (в определенной степени) разработчиком Swift в Apple: devforums.apple.com/thread/230611 (Примечание: для просмотра этого требуется учетная запись разработчика Apple. стр.)
titaniumdecoy

Ответы:

88

Как указывает Томас, вы можете объявить свою переменную, вообще не задавая типа (или явно указав ее как тип Printer<Int>. Но вот объяснение того, почему у вас не может быть типа Printableпротокола.

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

// a general protocol that allows for storing and retrieving
// a specific type (as defined by a Stored typealias
protocol StoringType {
    typealias Stored

    init(_ value: Stored)
    func getStored() -> Stored
}

// An implementation that stores Ints
struct IntStorer: StoringType {
    typealias Stored = Int
    private let _stored: Int
    init(_ value: Int) { _stored = value }
    func getStored() -> Int { return _stored }
}

// An implementation that stores Strings
struct StringStorer: StoringType {
    typealias Stored = String
    private let _stored: String
    init(_ value: String) { _stored = value }
    func getStored() -> String { return _stored }
}

let intStorer = IntStorer(5)
intStorer.getStored() // returns 5

let stringStorer = StringStorer("five")
stringStorer.getStored() // returns "five"

Хорошо, пока все хорошо.

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

Но вы не можете этого сделать, если у протокола есть связанный тип. Как следующий код будет работать на практике?

// as you've seen this won't compile because
// StoringType has an associated type.

// randomly assign either a string or int storer to someStorer:
var someStorer: StoringType = 
      arc4random()%2 == 0 ? intStorer : stringStorer

let x = someStorer.getStored()

Какой тип xбудет в приведенном выше коде ? An Int? Или String? В Swift все типы должны быть исправлены во время компиляции. Функция не может динамически переходить от возврата одного типа к другому на основе факторов, определенных во время выполнения.

Вместо этого вы можете использовать только StoredTypeкак общее ограничение. Предположим, вы хотите распечатать любой сохраненный тип. Вы можете написать такую ​​функцию:

func printStoredValue<S: StoringType>(storer: S) {
    let x = storer.getStored()
    println(x)
}

printStoredValue(intStorer)
printStoredValue(stringStorer)

Это нормально, потому что во время компиляции компилятор как бы записывает две версии printStoredValue: одну для Ints и одну для Strings. В этих двух версиях, xкак известно , относится к определенному типу.

Скорость полета
источник
20
Другими словами, нет способа использовать общий протокол в качестве параметра, и причина в том, что Swift не поддерживает поддержку универсальных шаблонов в стиле .NET? Это довольно неудобно.
Тамерлан
Мои знания .NET немного туманны ... у вас есть пример чего-то подобного в .NET, которое будет работать в этом примере? Кроме того, немного сложно понять, что вам нравится в протоколе в вашем примере. Каким будет поведение во время выполнения, если вы назначите своей pпеременной принтеры разных типов, а затем передадите в них недопустимые типы print? Исключение времени выполнения?
Airspeed Velocity
@AirspeedVelocity Я обновил вопрос, включив в него пример C #. Что касается ценности, зачем мне это нужно, так это то, что это позволит мне разработать интерфейс, а не реализацию. Если мне нужно передать функцию для печати, я могу использовать интерфейс в объявлении и передать множество различных реализаций, не касаясь моей функции. Также подумайте о реализации библиотеки коллекций, вам понадобится такой код плюс дополнительные ограничения для типа T.
Тамерлан
4
Теоретически, если бы можно было создавать общие протоколы с использованием угловых скобок, как в C #, было бы разрешено создание переменных типа протокола? (StoringType <Int>, StoringType <String>)
GeRyCh, 07
1
В Java вы можете сделать эквивалент var someStorer: StoringType<Int>или var someStorer: StoringType<String>и решить обозначенную вами проблему.
JeremyP
42

Есть еще одно решение, которое не упоминалось по этому вопросу, и оно использует технику, называемую стиранием типа . Чтобы получить абстрактный интерфейс для универсального протокола, создайте класс или структуру, которая обертывает объект или структуру, которая соответствует протоколу. Класс-оболочка, обычно называемый Any {имя протокола}, сам соответствует протоколу и реализует свои функции, перенаправляя все вызовы внутреннему объекту. Попробуйте пример ниже на детской площадке:

import Foundation

public protocol Printer {
    typealias T
    func print(val:T)
}

struct AnyPrinter<U>: Printer {

    typealias T = U

    private let _print: U -> ()

    init<Base: Printer where Base.T == U>(base : Base) {
        _print = base.print
    }

    func print(val: T) {
        _print(val)
    }
}

struct NSLogger<U>: Printer {

    typealias T = U

    func print(val: T) {
        NSLog("\(val)")
    }
}

let nsLogger = NSLogger<Int>()

let printer = AnyPrinter(base: nsLogger)

printer.print(5) // prints 5

Этот тип printerизвестен AnyPrinter<Int>и может использоваться для абстрагирования любой возможной реализации протокола принтера. Хотя AnyPrinter не является технически абстрактным, его реализация - это просто переход к реальному реализующему типу и может использоваться для отделения реализующих типов от типов, использующих их.

Следует отметить, что AnyPrinterне требуется явно сохранять базовый экземпляр. Фактически, мы не можем, поскольку не можем заявить, AnyPrinterчто у нас есть Printer<T>собственность. Вместо этого мы получаем указатель функции _printна базовую printфункцию. Вызов base.printбез его вызова возвращает функцию, в которой base каррирована как собственная переменная и, таким образом, сохраняется для будущих вызовов.

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

Очевидно, что есть некоторая работа по настройке стирания типов, но это может быть очень полезно, если требуется абстракция общего протокола. Этот шаблон можно найти в стандартной быстрой библиотеке с такими типами, как AnySequence. Дополнительная литература: http://robnapier.net/erasure

БОНУС:

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

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

printer.print(10) //prints 10 with NSLog

Это может быть простой и СУХОЙ способ выразить инъекции зависимостей для протоколов, которые вы используете в своем приложении.

Патрик Гоули
источник
Спасибо за это. Мне больше нравится этот шаблон стирания типов (с использованием указателей на функции), чем использование абстрактного класса (который, конечно, не существует и должен быть подделан с использованием fatalError()), описанный в других руководствах по стиранию типов.
Чейз
4

Решение вашего обновленного варианта использования:

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

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

// because of how how collections are structured in the Swift std lib,
// you’d first need to create a PrintableGeneratorType, which would be
// a constrained version of GeneratorType
protocol PrintableGeneratorType: GeneratorType {
    // require elements to be printable:
    typealias Element: Printable
}

// then have the collection require a printable generator
protocol PrintableCollectionType: CollectionType {
    typealias Generator: PrintableGenerator
}

Теперь, если вы хотите реализовать коллекцию, которая может содержать только печатаемые элементы:

struct MyPrintableCollection<T: Printable>: PrintableCollectionType {
    typealias Generator = IndexingGenerator<T>
    // etc...
}

Тем не менее, это, вероятно, не очень полезно, поскольку вы не можете ограничить существующие структуры коллекции Swift подобным образом, а только те, которые вы реализуете.

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

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}
Скорость полета
источник
Ой, это выглядит ужасно. Мне нужно было просто иметь протокол с общей поддержкой. Я надеялся получить что-то вроде этого: Protocol Collection <T>: SequenceType. Вот и все. Спасибо за образцы кода, думаю, это займет некоторое время, чтобы их переварить :)
Тамерлан