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

时间:2014-12-31 19:57:30

标签: ios xcode generics swift

我们说我有一个协议:

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)

编译器抱怨此错误:

  

&#34; protocol&#39; Printable&#39;只能用作通用约束因为   它有自我或相关的类型要求&#34;

我做错了吗?无论如何要解决这个问题?

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

3 个答案:

答案 0 :(得分:79)

正如托马斯指出的那样,你可以通过不提供类型来声明你的变量(或者你可以明确地将它作为类型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的两个版本:一个用于Int s,一个用于String s。在这两个版本中,x已知是特定类型。

答案 1 :(得分:32)

在这个问题上还有一个未提及的解决方案,即使用一种名为 type erasure 的技术。要实现通用协议的抽象接口,请创建一个包装符合协议的对象或结构的类或结构。通常名为“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>属性。相反,我们得到一个函数指针_print到base print函数。在不调用它的情况下调用base.print会返回一个函数,其中base被作为自变量进行curry,因此保留用于将来的调用。

要记住的另一件事是,这个解决方案本质上是另一层动态调度,这意味着性能略有下降。此外,类型擦除实例需要在底层实例之上的额外内存。由于这些原因,类型擦除不是一种免费的抽象。

显然,设置类型擦除有一些工作,但如果需要通用协议抽象,它可能非常有用。此模式位于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

这可以是一种简单而干燥的方式来表达您在应用中使用的协议的依赖注入。

答案 2 :(得分:4)

解决更新后的用例:

(顺便说一句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()
        }
}