Cómo utilizar el protocolo genérico como tipo de variable

89

Digamos que tengo un protocolo:

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

Y aquí está la implementación

class Printer<T> : Printable {

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

Mi expectativa era que debería poder usar la Printablevariable para imprimir valores como este:

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

El compilador se queja de este error:

"el protocolo 'Imprimible' solo se puede utilizar como una restricción genérica porque tiene requisitos de tipo propio o asociados"

Estoy haciendo algo mal ? Cualquier forma de arreglar esto ?

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

EDICIÓN 2: Ejemplo del mundo real de lo que quiero. Tenga en cuenta que esto no se compilará, pero presentará lo que quiero lograr.

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()
}
Tamerlán
fuente
1
Aquí hay un hilo relevante en los foros de desarrolladores de Apple de 2014 donde esta pregunta es abordada (hasta cierto punto) por un desarrollador de Swift en Apple: devforums.apple.com/thread/230611 (Nota: Se requiere una cuenta de desarrollador de Apple para ver esto página.)
titaniumdecoy

Respuestas:

88

Como señala Thomas, puede declarar su variable sin dar un tipo en absoluto (o podría darlo explícitamente como tipo Printer<Int>. Pero aquí hay una explicación de por qué no puede tener un tipo de Printableprotocolo.

No puede tratar los protocolos con tipos asociados como protocolos regulares y declararlos como tipos de variables independientes. Para pensar por qué, considere este escenario. Suponga que declaró un protocolo para almacenar algún tipo arbitrario y luego recuperarlo:

// 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, hasta ahora todo bien.

Ahora, la razón principal por la que haría que un tipo de variable fuera un protocolo que implementa un tipo, en lugar del tipo real, es para que pueda asignar diferentes tipos de objetos que se ajusten a ese protocolo a la misma variable y obtener polimórficos comportamiento en tiempo de ejecución dependiendo de cuál sea realmente el objeto.

Pero no puede hacer esto si el protocolo tiene un tipo asociado. ¿Cómo funcionaría el siguiente código en la práctica?

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

En el código anterior, ¿cuál sería el tipo de x? Un Int? ¿O un String? En Swift, todos los tipos deben corregirse en tiempo de compilación. Una función no puede cambiar dinámicamente de devolver un tipo a otro en función de factores determinados en tiempo de ejecución.

En su lugar, solo puede usarlo StoredTypecomo una restricción genérica. Suponga que desea imprimir cualquier tipo de tipo almacenado. Podrías escribir una función como esta:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

Esto está bien, porque en el momento de la compilación, es como si el compilador escribiera dos versiones de printStoredValue: una para Ints y otra para Strings. Dentro de esas dos versiones, xse sabe que es de un tipo específico.

Velocidad aerodinámica
fuente
20
En otras palabras, no hay forma de tener un protocolo genérico como parámetro y la razón es que Swift no admite el soporte de tiempo de ejecución de estilo .NET de genéricos. Esto es bastante inconveniente.
Tamerlán,
Mi conocimiento de .NET es un poco confuso ... ¿tiene un ejemplo de algo similar en .NET que funcione en este ejemplo? Además, es un poco difícil ver qué le está comprando el protocolo de su ejemplo. En tiempo de ejecución, ¿cuál esperaría que fuera el comportamiento si asignara impresoras de diferentes tipos a su pvariable y luego pasara tipos no válidos print? ¿Excepción en tiempo de ejecución?
Velocidad aerodinámica
@AirspeedVelocity He actualizado la pregunta para incluir el ejemplo de C #. Bueno, en cuanto al valor por el que lo necesito, es que esto me permitirá desarrollar una interfaz, no la implementación. Si necesito pasar imprimible a una función, puedo usar la interfaz en la declaración y pasar muchas implementaciones diferentes sin tocar mi función. También piensa en la implementación de la biblioteca de recogida necesitará este tipo de código más restricciones adicionales en el tipo T.
Tamerlán
4
Teóricamente, si fuera posible crear protocolos genéricos usando corchetes angulares como en C #, ¿se permitiría la creación de variables de tipo de protocolo? (StoringType <Int>, StoringType <String>)
GeRyCh
1
En Java, podría hacer el equivalente de var someStorer: StoringType<Int>o var someStorer: StoringType<String>y resolver el problema que describe.
JeremyP
42

Hay una solución más que no se ha mencionado en esta pregunta, que está utilizando una técnica llamada borrado de tipo . Para lograr una interfaz abstracta para un protocolo genérico, cree una clase o estructura que envuelva un objeto o estructura que se ajuste al protocolo. La clase contenedora, normalmente denominada 'Cualquier {nombre de protocolo}', se ajusta en sí misma al protocolo e implementa sus funciones reenviando todas las llamadas al objeto interno. Pruebe el siguiente ejemplo en un parque infantil:

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

Se printersabe que el tipo de es AnyPrinter<Int>y se puede utilizar para abstraer cualquier posible implementación del protocolo de impresora. Si bien AnyPrinter no es técnicamente abstracto, su implementación es solo una caída a un tipo de implementación real y se puede usar para desacoplar los tipos de implementación de los tipos que los usan.

Una cosa a tener en cuenta es que AnyPrinterno es necesario retener explícitamente la instancia base. De hecho, no podemos porque no podemos declarar AnyPrintertener una Printer<T>propiedad. En cambio, obtenemos un puntero de función _printa la printfunción de base . Llamar base.printsin invocarlo devuelve una función donde la base se cursa como la variable propia y, por lo tanto, se retiene para futuras invocaciones.

Otra cosa a tener en cuenta es que esta solución es esencialmente otra capa de distribución dinámica, lo que significa un ligero impacto en el rendimiento. Además, la instancia de borrado de tipos requiere memoria adicional además de la instancia subyacente. Por estas razones, el borrado de tipos no es una abstracción gratuita.

Obviamente, hay algo de trabajo para configurar el borrado de tipos, pero puede ser muy útil si se necesita una abstracción de protocolo genérico. Este patrón se encuentra en la biblioteca estándar rápida con tipos como AnySequence. Más información: http://robnapier.net/erasure

PRIMA:

Si decide que desea inyectar la misma implementación en Printertodas partes, puede proporcionar un inicializador conveniente para el AnyPrinterque inyecta ese tipo.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

Esta puede ser una forma fácil y SECA de expresar inyecciones de dependencia para los protocolos que usa en su aplicación.

Patrick Goley
fuente
Gracias por esto. Me gusta este patrón de borrado de tipo (usando punteros de función) más que usar una clase abstracta (que por supuesto, no existe y debe ser simulada fatalError()) que se describe en otros tutoriales de borrado de tipo.
Chase
4

Abordar su caso de uso actualizado:

(por cierto, Printableya es un protocolo Swift estándar, por lo que probablemente desee elegir un nombre diferente para evitar confusiones)

Para hacer cumplir restricciones específicas sobre los implementadores de protocolos, puede restringir los alias de tipo del protocolo. Entonces, para crear su colección de protocolos que requiera que los elementos sean imprimibles:

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

Ahora, si quisiera implementar una colección que solo pudiera contener elementos imprimibles:

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

Sin embargo, esto probablemente sea de poca utilidad, ya que no puede restringir las estructuras de colección Swift existentes de esa manera, solo las que implementa.

En su lugar, debe crear funciones genéricas que restrinjan su entrada a colecciones que contienen elementos imprimibles.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}
Velocidad aerodinámica
fuente
Oh, hombre, esto parece enfermo. Lo que necesitaba es tener un protocolo con soporte genérico. Esperaba tener algo como esto: Protocol Collection <T>: SequenceType. Y eso es. Gracias por las muestras de código, creo que tomará un tiempo digerirlo :)
Tamerlane