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

时间:2023-01-06 00:41:52

Let's say I have a protocol :

假设我有一个协议:

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

And here is the implementation

这是实现

class Printer<T> : Printable {

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

My expectation was that I must be able to use Printable variable to print values like this :

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

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

Compiler is complaining with this error :

编译器在抱怨这个错误:

"protocol 'Printable' can only be used as a generic constraint because it has Self or associated type requirements"

"协议'可打印'只能作为通用约束使用,因为它具有自我或相关的类型需求"

Am I doing something wrong ? Anyway to fix this ?

我做错什么了吗?怎么解决这个问题?

**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: Real world example of what I want. Note that this will not compile, but presents what I want to achieve.

编辑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 个解决方案

#1


77  

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.

正如Thomas所指出的,可以通过完全不提供类型来声明变量(或者可以显式地将其指定为type Printer )。但是这里有一个解释为什么你不能有一种类型的可打印协议。

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.

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

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:

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

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.

这是可以的,因为在编译时,编译器会写出两个版本的printStoredValue:一个用于int,一个用于字符串。在这两个版本中,x是已知的特定类型。

#2


27  

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:

在这个问题上还有一个没有提到的解决方案,那就是使用一种叫做类型擦除的技术。要为通用协议实现抽象接口,请创建一个类或结构来包装符合该协议的对象或结构。包装器类通常命名为“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

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.

打印机的类型是AnyPrinter ,可以用来抽象打印机协议的任何可能实现。虽然AnyPrinter在技术上并不是抽象的,但是它的实现只是一个真正实现类型,可以用来将实现类型与使用它们的类型分离开来。

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.

需要注意的一点是,任何打印机都不必显式地保留基实例。事实上,我们不能声明任何打印机具有 属性。相反,我们得到一个函数指针_print到base的打印函数。调用基础。不调用它的print返回一个函数,在这个函数中,base被作为自变量进行处理,并被保留为将来的调用。

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

显然,需要进行一些设置类型擦除的工作,但是如果需要通用协议抽象,那么它将非常有用。这种模式可以在swift标准库中找到,它的类型类似于AnySequence。进一步阅读: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.

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

#3


4  

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)

(顺便说一句,Printable已经是一个标准的Swift协议,所以您可能需要选择一个不同的名称来避免混淆)

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.

但是,这可能没有什么实际的用途,因为您不能像那样约束现有的Swift集合结构,只能约束您实现的结构。

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

#1


77  

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.

正如Thomas所指出的,可以通过完全不提供类型来声明变量(或者可以显式地将其指定为type Printer )。但是这里有一个解释为什么你不能有一种类型的可打印协议。

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.

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

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:

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

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.

这是可以的,因为在编译时,编译器会写出两个版本的printStoredValue:一个用于int,一个用于字符串。在这两个版本中,x是已知的特定类型。

#2


27  

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:

在这个问题上还有一个没有提到的解决方案,那就是使用一种叫做类型擦除的技术。要为通用协议实现抽象接口,请创建一个类或结构来包装符合该协议的对象或结构。包装器类通常命名为“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

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.

打印机的类型是AnyPrinter ,可以用来抽象打印机协议的任何可能实现。虽然AnyPrinter在技术上并不是抽象的,但是它的实现只是一个真正实现类型,可以用来将实现类型与使用它们的类型分离开来。

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.

需要注意的一点是,任何打印机都不必显式地保留基实例。事实上,我们不能声明任何打印机具有 属性。相反,我们得到一个函数指针_print到base的打印函数。调用基础。不调用它的print返回一个函数,在这个函数中,base被作为自变量进行处理,并被保留为将来的调用。

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

显然,需要进行一些设置类型擦除的工作,但是如果需要通用协议抽象,那么它将非常有用。这种模式可以在swift标准库中找到,它的类型类似于AnySequence。进一步阅读: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.

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

#3


4  

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)

(顺便说一句,Printable已经是一个标准的Swift协议,所以您可能需要选择一个不同的名称来避免混淆)

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.

但是,这可能没有什么实际的用途,因为您不能像那样约束现有的Swift集合结构,只能约束您实现的结构。

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