如何使用通用协议作为变量类型

假设我有一个协议:

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’只能用作通用约束,因为 本身或有关连的类型规定」

我是不是做错了什么? 不管怎样,要解决这个问题?

**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()
}
37911 次浏览

As Thomas points out, you can declare your variable by not giving a type at all (or you could explicitly give it as type Printer<Int>. But here's an explanation of why you can't have a type of the Printable protocol.

You can't treat protocols with associated types like regular protocols and declare them as standalone variable types. To think about why, consider this scenario. Suppose you declared a protocol for storing some arbitrary type and then fetching it back:

// 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, so far so good.

Now, the main reason you would have a type of a variable be a protocol a type implements, rather than the actual type, is so that you can assign different kinds of object that all conform to that protocol to the same variable, and get polymorphic behavior at runtime depending on what the object actually is.

But you can't do this if the protocol has an associated type. How would the following code work in practice?

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

In the above code, what would the type of x be? An Int? Or a String? In Swift, all types must be fixed at compile time. A function cannot dynamically shift from returning one type to another based on factors determined at runtime.

Instead, you can only use StoredType as a generic constraint. Suppose you wanted to print out any kind of stored type. You could write a function like this:

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


printStoredValue(intStorer)
printStoredValue(stringStorer)

This is OK, because at compile time, it's as if the compiler writes out two versions of printStoredValue: one for Ints, and one for Strings. Within those two versions, x is known to be of a specific type.

Addressing your updated use case:

(btw Printable is already a standard Swift protocol so you’d probably want to pick a different name to avoid confusion)

To enforce specific restrictions on protocol implementors, you can constrain the protocol's typealias. So to create your protocol collection that requires the elements to be printable:

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

Now if you wanted to implement a collection that could only contain printable elements:

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

However, this is probably of little actual utility, since you can’t constrain existing Swift collection structs like that, only ones you implement.

Instead, you should create generic functions that constrain their input to collections containing printable elements.

func printCollection
<C: CollectionType where C.Generator.Element: Printable>
(source: C) {
for x in source {
x.print()
}
}

There is one more solution that hasn't been mentioned on this question, which is using a technique called type erasure. To achieve an abstract interface for a generic protocol, create a class or struct that wraps an object or struct that conforms to the protocol. The wrapper class, usually named 'Any{protocol name}', itself conforms to the protocol and implements its functions by forwarding all calls to the internal object. Try the example below in a 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

The type of printer is known to be AnyPrinter<Int> and can be used to abstract any possible implementation of the Printer protocol. While AnyPrinter is not technically abstract, it's implementation is just a fall through to a real implementing type, and can be used to decouple implementing types from the types using them.

One thing to note is that AnyPrinter does not have to explicitly retain the base instance. In fact, we can't since we can't declare AnyPrinter to have a Printer<T> property. Instead, we get a function pointer _print to base's print function. Calling base.print without invoking it returns a function where base is curried as the self variable, and is thusly retained for future invocations.

Another thing to keep in mind is that this solution is essentially another layer of dynamic dispatch which means a slight hit on performance. Also, the type erasing instance requires extra memory on top of the underlying instance. For these reasons, type erasure is not a cost free abstraction.

Obviously there is some work to set up type erasure, but it can be very useful if generic protocol abstraction is needed. This pattern is found in the swift standard library with types like AnySequence. Further reading: http://robnapier.net/erasure

BONUS:

If you decide you want to inject the same implementation of Printer everywhere, you can provide a convenience initializer for AnyPrinter which injects that type.

extension AnyPrinter {


convenience init() {


let nsLogger = NSLogger<T>()


self.init(base: nsLogger)
}
}


let printer = AnyPrinter<Int>()


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

This can be an easy and DRY way to express dependency injections for protocols that you use across your app.