Como usar protocolo genérico como um tipo de variável

89

Digamos que eu tenha um protocolo:

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

E aqui está a implementação

class Printer<T> : Printable {

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

Minha expectativa era que eu deveria ser capaz de usar Printablevariáveis ​​para imprimir valores como este:

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

O compilador está reclamando deste erro:

"protocolo 'Imprimível' só pode ser usado como uma restrição genérica porque tem requisitos próprios ou de tipo associado"

Estou fazendo algo errado ? Algum jeito de arrumar isso ?

**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: exemplo do mundo real do que eu quero. Observe que isso não irá compilar, mas apresenta o que desejo alcançar.

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
fonte
1
Aqui está um tópico relevante nos Fóruns de Desenvolvedores da Apple de 2014, onde esta questão é abordada (até certo ponto) por um desenvolvedor Swift da Apple: devforums.apple.com/thread/230611 (Observação: é necessária uma conta de desenvolvedor da Apple para visualizar isso página.)
titaniumdecoy

Respostas:

88

Como Thomas aponta, você pode declarar sua variável não fornecendo um tipo (ou você poderia explicitamente fornecê-la como tipo Printer<Int>. Mas aqui está uma explicação de por que você não pode ter um tipo de Printableprotocolo.

Você não pode tratar protocolos com tipos associados como protocolos regulares e declará-los como tipos de variáveis ​​independentes. Para pensar sobre o motivo, considere este cenário. Suponha que você tenha declarado um protocolo para armazenar algum tipo arbitrário e, em seguida, buscá-lo de volta:

// 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, até agora tudo bem.

Agora, a principal razão de você ter um tipo de variável sendo um protocolo implementado por um tipo, em vez do tipo real, é para que você possa atribuir diferentes tipos de objetos que estejam em conformidade com aquele protocolo para a mesma variável e obter polimórfico comportamento em tempo de execução dependendo do que o objeto realmente é.

Mas você não pode fazer isso se o protocolo tiver um tipo associado. Como o código a seguir funcionaria na prática?

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

No código acima, qual seria o tipo de x? Um Int? Ou um String? Em Swift, todos os tipos devem ser corrigidos em tempo de compilação. Uma função não pode mudar dinamicamente de retornar um tipo para outro com base em fatores determinados em tempo de execução.

Em vez disso, você só pode usar StoredTypecomo uma restrição genérica. Suponha que você queira imprimir qualquer tipo de tipo armazenado. Você poderia escrever uma função como esta:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

Isso está OK, porque em tempo de compilação, é como se o compilador escrevesse duas versões de printStoredValue: uma para Ints e outra para Strings. Dentro dessas duas versões, xé conhecido por ser de um tipo específico.

Velocidade da velocidade do ar
fonte
20
Em outras palavras, não há como ter o protocolo genérico como parâmetro e o motivo é que o Swift não oferece suporte ao estilo de execução .NET para genéricos? Isso é bastante inconveniente.
Tamerlane
Meu conhecimento .NET é um pouco nebuloso ... você tem um exemplo de algo semelhante em .NET que funcionaria neste exemplo? Além disso, é um pouco difícil ver o que o protocolo em seu exemplo está comprando para você. No tempo de execução, qual seria o comportamento se você atribuísse impressoras de tipos diferentes à sua pvariável e passasse tipos inválidos para ela print? Exceção de tempo de execução?
Velocidade da velocidade do ar
@AirspeedVelocity Eu atualizei a pergunta para incluir o exemplo C #. Bem quanto ao valor porque eu preciso é que isso me permitirá desenvolver uma interface não para a implementação. Se eu precisar passar imprimível para uma função, posso usar a interface na declaração e passar muitas implementações de diferença sem tocar na minha função. Pense também na implementação da biblioteca de coleção - você precisará deste tipo de código mais restrições adicionais no tipo T.
Tamerlane
4
Teoricamente, se fosse possível criar protocolos genéricos usando colchetes angulares como em C #, seria permitida a criação de variáveis ​​do tipo de protocolo? (StoringType <Int>, StoringType <String>)
GeRyCh
1
Em Java, você poderia fazer o equivalente a var someStorer: StoringType<Int>ou var someStorer: StoringType<String>e resolver o problema delineado.
JeremyP
42

Há mais uma solução que não foi mencionada nesta questão, que é usar uma técnica chamada apagamento de tipo . Para obter uma interface abstrata para um protocolo genérico, crie uma classe ou estrutura que envolva um objeto ou estrutura em conformidade com o protocolo. A classe wrapper, geralmente chamada de 'Any {protocol name}', está em conformidade com o protocolo e implementa suas funções encaminhando todas as chamadas para o objeto interno. Experimente o exemplo abaixo em um playground:

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

O tipo de printeré conhecido AnyPrinter<Int>e pode ser usado para abstrair qualquer implementação possível do protocolo da impressora. Embora AnyPrinter não seja tecnicamente abstrato, sua implementação é apenas uma queda para um tipo de implementação real e pode ser usada para separar os tipos de implementação dos tipos que os utilizam.

Uma coisa a observar é que AnyPrinternão é necessário reter explicitamente a instância base. Na verdade, não podemos, pois não podemos declarar AnyPrinterque temos uma Printer<T>propriedade. Em vez disso, obtemos um ponteiro de função _printpara a printfunção de base . Chamar base.printsem invocar retorna uma função em que base é curried como a variável própria e, portanto, é retida para invocações futuras.

Outra coisa a ter em mente é que essa solução é essencialmente outra camada de despacho dinâmico, o que significa um pequeno impacto no desempenho. Além disso, a instância de apagamento de tipo requer memória extra sobre a instância subjacente. Por essas razões, o apagamento de tipo não é uma abstração gratuita.

Obviamente, há algum trabalho para configurar a eliminação do tipo, mas pode ser muito útil se a abstração de protocolo genérico for necessária. Este padrão é encontrado na biblioteca padrão do Swift com tipos semelhantes AnySequence. Leitura adicional: http://robnapier.net/erasure

BÔNUS:

Se você decidir que deseja injetar a mesma implementação em Printertodos os lugares, poderá fornecer um inicializador de conveniência para o AnyPrinterqual injeta esse tipo.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

Essa pode ser uma maneira fácil e SECA de expressar injeções de dependência para protocolos que você usa em seu aplicativo.

Patrick Goley
fonte
Obrigado por isso. Eu gosto mais desse padrão de apagamento de tipo (usando ponteiros de função) do que de uma classe abstrata (que é claro, não existe e deve ser falsificada usando fatalError()) que é descrito em outros tutoriais de apagamento de tipo.
Chase
4

Abordando seu caso de uso atualizado:

(btw Printablejá é um protocolo Swift padrão, então você provavelmente vai querer escolher um nome diferente para evitar confusão)

Para impor restrições específicas aos implementadores de protocolo, você pode restringir as alias de tipo do protocolo. Portanto, para criar sua coleção de protocolo que requer que os elementos possam ser impressos:

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

Agora, se você quiser implementar uma coleção que possa conter apenas elementos imprimíveis:

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

No entanto, isso provavelmente é de pouca utilidade real, uma vez que você não pode restringir estruturas de coleção Swift existentes dessa forma, apenas aquelas que você implementa.

Em vez disso, você deve criar funções genéricas que restringem sua entrada a coleções que contêm elementos imprimíveis.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}
Velocidade da velocidade do ar
fonte
Oh cara, isso parece doentio. O que eu precisava é apenas ter protocolo com suporte genérico. Eu esperava ter algo assim: protocol Collection <T>: SequenceType. E é isso. Obrigado pelos exemplos de código, acho que vai demorar um pouco para digeri-los :)
Tamerlane