Jak używać protokołu ogólnego jako typu zmiennej

89

Powiedzmy, że mam protokół:

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

A oto realizacja

class Printer<T> : Printable {

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

Spodziewałem się, że będę mógł używać Printablezmiennej do drukowania takich wartości:

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

Kompilator skarży się z tym błędem:

„Protokół„ do druku ”może być używany tylko jako ogólne ograniczenie, ponieważ ma własne lub powiązane wymagania dotyczące typu"

Czy robię coś źle ? W każdym razie, aby to naprawić?

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

EDYCJA 2: Prawdziwy przykład tego, czego chcę. Zauważ, że to się nie skompiluje, ale przedstawia to, co chcę osiągnąć.

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()
}
Tamerlane
źródło
1
Oto odpowiedni wątek na forach programistów Apple z 2014 r., W którym to pytanie jest (do pewnego stopnia) adresowane przez programistę Swift w Apple: devforums.apple.com/thread/230611 (Uwaga: do wyświetlenia tego wymagane jest konto programisty Apple strona.)
titaniumdecoy

Odpowiedzi:

88

Jak zauważa Thomas, możesz zadeklarować swoją zmienną, nie podając w ogóle typu (lub możesz jawnie podać typ jako typ Printer<Int>. Ale oto wyjaśnienie, dlaczego nie możesz mieć typu Printableprotokołu.

Nie można traktować protokołów z powiązanymi typami jak zwykłych protokołów i deklarować ich jako samodzielne typy zmiennych. Aby zastanowić się, dlaczego, rozważ ten scenariusz. Załóżmy, że zadeklarowałeś protokół do przechowywania dowolnego typu, a następnie przywracał go:

// 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"

OK, na razie dobrze.

Teraz głównym powodem, dla którego typ zmiennej byłby protokół implementowany przez typ, a nie rzeczywisty typ, jest to, że możesz przypisać różne rodzaje obiektów, które wszystkie są zgodne z tym protokołem, do tej samej zmiennej i uzyskać polimorficzne zachowanie w czasie wykonywania w zależności od tego, czym właściwie jest obiekt.

Ale nie możesz tego zrobić, jeśli protokół ma powiązany typ. Jak w praktyce działałby poniższy kod?

// 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()

Jaki byłby typ tego kodu w powyższym kodzie x? An Int? Albo String? W Swift wszystkie typy muszą zostać naprawione w czasie kompilacji. Funkcja nie może dynamicznie przechodzić z zwracania jednego typu do innego na podstawie czynników określonych w czasie wykonywania.

Zamiast tego można go używać tylko StoredTypejako ogólnego ograniczenia. Załóżmy, że chcesz wydrukować dowolny rodzaj przechowywanego typu. Możesz napisać taką funkcję:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

To jest w porządku, ponieważ w czasie kompilacji kompilator wypisze dwie wersje printStoredValue: jedną dla Ints, a drugą dla Strings. W tych dwóch wersjach xjest znany jako szczególny typ.

Prędkość powietrza
źródło
20
Innymi słowy, nie ma sposobu, aby mieć protokół ogólny jako parametr, a powodem jest to, że Swift nie obsługuje obsługi typów generycznych w czasie wykonywania w stylu .NET? Jest to dość niewygodne.
Tamerlane
Moja wiedza na temat .NET jest trochę mglista ... czy masz przykład czegoś podobnego w .NET, który działałby w tym przykładzie? Ponadto trochę trudno jest zobaczyć, jaki protokół z twojego przykładu kupuje. Jakiego zachowania można oczekiwać w czasie wykonywania, gdyby przypisać do pzmiennej drukarki różnych typów, a następnie przekazać do niej nieprawidłowe typy print? Wyjątek w czasie wykonywania?
Prędkość
@AirspeedVelocity Zaktualizowałem pytanie, aby zawierało przykład C #. Jeśli chodzi o wartość, dla której jest to potrzebne, jest to, że pozwoli mi to rozwinąć interfejs, a nie implementację. Jeśli potrzebuję przekazać printable do funkcji, mogę użyć interfejsu w deklaracji i przekazać wiele implementacji różnic bez dotykania mojej funkcji. Pomyśl także o wdrożeniu biblioteki kolekcji, będziesz potrzebować tego rodzaju kodu oraz dodatkowych ograniczeń dla typu T.
Tamerlane
4
Teoretycznie, gdyby można było tworzyć protokoły generyczne przy użyciu nawiasów ostrych, jak w C #, czy byłoby dozwolone tworzenie zmiennych typu protokołu? (StoringType <Int>, StoringType <String>)
GeRyCh,
1
W Javie możesz zrobić odpowiednik var someStorer: StoringType<Int>lub var someStorer: StoringType<String>i rozwiązać zarysowany problem.
JeremyP
42

Jest jeszcze jedno rozwiązanie, które nie zostało wspomniane w tym pytaniu, a mianowicie użycie techniki zwanej wymazywaniem typów . Aby uzyskać abstrakcyjny interfejs dla protokołu ogólnego, utwórz klasę lub strukturę, która otacza obiekt lub strukturę zgodną z protokołem. Klasa opakowania, zwykle nazywana „Any {nazwa protokołu}”, sama jest zgodna z protokołem i implementuje swoje funkcje, przekazując wszystkie wywołania do obiektu wewnętrznego. Wypróbuj poniższy przykład na placu zabaw:

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

Typ printerjest znany AnyPrinter<Int>i można go użyć do abstrakcji dowolnej możliwej implementacji protokołu drukarki. Chociaż AnyPrinter nie jest technicznie abstrakcyjna, jego implementacja jest tylko przejściem do prawdziwego typu implementującego i może być używana do oddzielania typów implementujących od typów, które ich używają.

Należy zwrócić uwagę na to, że AnyPrinternie musi jawnie zachowywać instancji podstawowej. W rzeczywistości nie możemy, ponieważ nie możemy zadeklarować AnyPrinterposiadania Printer<T>właściwości. Zamiast tego otrzymujemy wskaźnik funkcji do funkcji _printbazy print. Wywołanie base.printbez wywołania zwraca funkcję, w której baza jest pobierana jako zmienna self, a zatem jest zachowywana dla przyszłych wywołań.

Inną rzeczą, o której należy pamiętać, jest to, że to rozwiązanie jest zasadniczo kolejną warstwą dynamicznej wysyłki, co oznacza niewielki spadek wydajności. Ponadto instancja wymazywania typu wymaga dodatkowej pamięci w górnej części podstawowej instancji. Z tych powodów wymazywanie typów nie jest bezpłatną abstrakcją.

Oczywiście jest trochę pracy, aby ustawić wymazywanie typów, ale może być bardzo przydatne, jeśli potrzebna jest abstrakcyjna abstrakcja protokołu. Ten wzorzec znajduje się w szybkiej bibliotece standardowej z typami takimi jak AnySequence. Dalsza lektura: http://robnapier.net/erasure

PREMIA:

Jeśli zdecydujesz, że chcesz wstrzyknąć tę samą implementację Printerwszędzie, możesz zapewnić wygodny inicjator, AnyPrinterktóry wstrzykuje ten typ.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

Może to być łatwy i SUCHY sposób wyrażania iniekcji zależności dla protokołów używanych w całej aplikacji.

Patrick Goley
źródło
Dzięki za to. Ten wzorzec wymazywania typu (przy użyciu wskaźników funkcji) podoba mi się bardziej niż używanie klasy abstrakcyjnej (która oczywiście nie istnieje i musi zostać sfałszowana fatalError()), która jest opisana w innych samouczkach dotyczących usuwania typów.
Chase
4

Adresowanie zaktualizowanego przypadku użycia:

(przy okazji Printableto już standardowy protokół Swift, więc prawdopodobnie chciałbyś wybrać inną nazwę, aby uniknąć nieporozumień)

Aby wymusić określone ograniczenia dotyczące implementatorów protokołu, można ograniczyć alias typu protokołu. Aby więc utworzyć kolekcję protokołów, która wymaga, aby elementy można było wydrukować:

// 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
}

Jeśli chcesz zaimplementować kolekcję, która może zawierać tylko elementy do druku:

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

Jednak jest to prawdopodobnie mało użyteczne, ponieważ nie możesz ograniczyć istniejących struktur kolekcji Swift, takich jak ta, tylko tych, które implementujesz.

Zamiast tego należy utworzyć funkcje ogólne, które ograniczają dane wejściowe do kolekcji zawierających elementy drukowalne.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}
Prędkość powietrza
źródło
Człowieku, to wygląda na chore. Potrzebowałem tylko protokołu z ogólną obsługą. Miałem nadzieję, że otrzymam coś takiego: protokół Collection <T>: SequenceType. I to wszystko. Dzięki za próbki kodu myślę, że jego przetrawienie zajmie trochę czasu :)
Tamerlane