如何将通用协议用作变量类型


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
以下是2014年Apple开发者论坛上的一个相关主题,Apple的Swift开发者在一定程度上解决了这个问题:devforums.apple.com/thread/230611(注意:需要Apple开发者帐户才能查看此问题页)。
titaniumdecoy

Answers:


88

正如Thomas所指出的,您可以通过根本不提供类型来声明变量(或者可以将其显式地指定为type 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?还是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样式运行时支持?这很不方便。
Tamerlane 2014年

我的.NET知识有点模糊...您是否有一个类似.NET的示例在该示例中可以使用?同样,很难看清示例中的协议正在购买什么。在运行时,如果将不同类型的打印机分配给p变量,然后将无效类型传递给变量,您期望的行为是print什么?运行时异常?
空速速度

@AirspeedVelocity我已经更新了问题,以包括C#示例。至于为什么我需要它的价值在于,这将使我能够开发到接口而不是实现。如果需要将printable传递给函数,则可以在声明中使用interface并传递许多不同的实现,而无需触碰我的函数。想想也是实现收集库,您将需要在类型T.这种代码加上额外的限制
帖木尔

4
从理论上讲,如果可以像C#中那样使用尖括号创建通用协议,是否可以创建协议类型的变量?(StoringType <Int>,StoringType <String>)
GeRyCh

1
在Java中,您可以执行var someStorer: StoringType<Int>或的等效操作var someStorer: StoringType<String>并解决您概述的问题。
JeremyP

42

在这个问题上还没有提到另一个解决方案,它使用的是一种称为类型擦除的技术。要实现通用协议的抽象接口,请创建一个类或结构来包装符合该协议的对象或结构。包装类,通常命名为“ Any {protocol name}”,本身符合协议并通过将所有调用转发给内部对象来实现其功能。在操场上尝试以下示例:

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>属性。相反,我们获得了指向_printbaseprint函数的函数指针。调用base.print而不调用它会返回一个函数,该函数会将base当作自变量,并因此保留以供将来调用。

要记住的另一件事是,该解决方案实质上是动态调度的另一层,这意味着对性能会有轻微的影响。同样,类型擦除实例在基础实例之上还需要额外的内存。由于这些原因,类型擦除不是免费的抽象。

显然,有一些工作可以设置类型擦除,但是如果需要通用协议抽象,它可能会非常有用。在swift标准库中可以找到这种模式,其类型为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

解决您的更新用例:

(btwPrintable已经是标准的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。就是这样。感谢您提供的代码示例,我认为需要一些时间才能消化它:)
Tamerlane 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.