Як використовувати загальний протокол як тип змінної


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' може використовуватися лише як загальне обмеження, оскільки він має вимоги до власного або пов'язаного типу"

Я щось роблю не так? У будь-якому випадку це виправити?

**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)

EDIT 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? А Int? Або a 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? Виняток виконання?
Швидкість повітряної швидкості

@AirspeedVelocity Я оновив запитання, включивши приклад C #. Ну а щодо значення, навіщо мені це потрібно, це те, що це дозволить мені розвиватися до інтерфейсу, а не до реалізації. Якщо мені потрібно передати функцію для друку функції, я можу використовувати інтерфейс у декларації та передати багато реалізацій різниці, не торкаючись своєї функції. Також подумайте про впровадження бібліотеки колекцій, яка вам знадобиться цей тип коду, а також додаткові обмеження для типу T.
Tamerlane

4
Теоретично, якби було можливо створити загальні протоколи за допомогою кутових дужок, як у C #, чи буде дозволено створення змінних типу протоколу? (StoringType <Int>, StoringType <String>)
GeRyCh

1
У Java ви можете зробити еквівалент var someStorer: StoringType<Int>або var someStorer: StoringType<String>і вирішити проблему, яку ви окреслили.
JeremyP

42

Є ще одне рішення, про яке не згадувалось у цьому питанні, - використання методу, який називається стиранням типу . Щоб отримати абстрактний інтерфейс для загального протоколу, створіть клас або структуру, яка обгортає об’єкт або структуру, що відповідає протоколу. Клас обгортки, який зазвичай називається "Будь-яке {ім'я протоколу}", сам відповідає протоколу та реалізує його функції, переадресовуючи всі виклики до внутрішнього об'єкта. Спробуйте приклад нижче на дитячому майданчику:

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>і може бути використаний для абстрагування будь-якої можливої ​​реалізації протоколу Printer. Хоча AnyPrinter технічно не абстрактний, його реалізація - це лише падіння до реального типу реалізації і може бути використана для відокремлення типів реалізації від типів, що їх використовують.

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

Ще одне, про що слід пам’ятати, це те, що це рішення - це, по суті, ще один рівень динамічної диспетчеризації, що означає незначний вплив на продуктивність. Також екземпляр стирання типу вимагає додаткової пам'яті поверх базового екземпляра. З цих причин стирання типу не є безкоштовною абстракцією.

Очевидно, є певна робота з налаштування стирання типів, але це може бути дуже корисно, якщо потрібна загальна абстракція протоколу. Цей шаблон знаходиться у швидкій стандартній бібліотеці з типами типу 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

Вирішення вашого оновленого випадку використання:

(btw 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()
        }
}

О, людина, це виглядає хворим. Мені потрібно було лише мати протокол із загальною підтримкою. Я сподівався отримати щось подібне: протокол Collection <T>: SequenceType. І це все. Дякую за зразки коду, я думаю, це займе деякий час, щоб переварити його :)
Тамерлан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.