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

Tam*_*ane 81 generics xcode ios swift

假设我有一个协议:

public protocol Printable {
    typealias T
    func Print(val:T)
}
Run Code Online (Sandbox Code Playgroud)

这是实施

class Printer<T> : Printable {

    func Print(val: T) {
        println(val)
    }
}
Run Code Online (Sandbox Code Playgroud)

我的期望是我必须能够使用Printable变量来打印这样的值:

let p:Printable = Printer<Int>()
p.Print(67)
Run Code Online (Sandbox Code Playgroud)

编译器抱怨此错误:

"protocol'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)
Run Code Online (Sandbox Code Playgroud)

编辑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()
}
Run Code Online (Sandbox Code Playgroud)

Air*_*ity 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"
Run Code Online (Sandbox Code Playgroud)

好的,到目前为止一切顺利.

现在,你有一个变量类型的主要原因是一个类型实现的协议,而不是实际的类型,这样你就可以将所有符合该协议的不同种类的对象分配给同一个变量,并获得多态运行时的行为取决于对象的实际内容.

但如果协议具有关联类型,则无法执行此操作.下面的代码如何在实践中起作用?

// 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()
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,类型x是什么?一个Int?还是一个String?在Swift中,所有类型都必须在编译时修复.函数不能根据在运行时确定的因素动态地从一种类型转换到另一种类型.

相反,您只能StoredType用作通用约束.假设您想要打印出任何类型的存储类型.你可以写一个这样的函数:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)
Run Code Online (Sandbox Code Playgroud)

这没关系,因为在编译时,就好像编译器写出了两个版本printStoredValue:一个用于Ints,一个用于Strings.在这两个版本中,x已知是特定类型.

  • 换句话说,没有办法将通用协议作为参数,原因是Swift不支持.NET样式运行时支持泛型?这非常不方便. (17认同)
  • 从理论上讲,如果可以使用C#中的尖括号创建通用协议,是否允许创建协议类型的变量?(StoringType <Int>,StoringType <String>) (3认同)

Pat*_*ley 34

还有一个解决方案在这个问题上没有提到,它使用的是一种称为类型擦除的技术.要实现通用协议的抽象接口,请创建一个包装符合协议的对象或结构的类或结构.通常名为"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
Run Code Online (Sandbox Code Playgroud)

printer已知的类型是AnyPrinter<Int>可用于抽象打印机协议的任何可能实现的类型.虽然AnyPrinter在技术上并不是抽象的,但它的实现只是一种真正的实现类型,可用于将实现类型与使用它们的类型分离.

需要注意的一点是,AnyPrinter不必显式保留基本实例.事实上,我们不能因为我们不能申报AnyPrinter拥有Printer<T>财产.相反,我们得到一个指向_printbase 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
Run Code Online (Sandbox Code Playgroud)

对于您在整个应用中使用的协议,这可以是一种简单且干燥的方式来表达依赖注入.