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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/27725803/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 04:11:48  来源:igfitidea点击:

How to use generic protocol as a variable type

iosxcodegenericsswift

提问by Tamerlane

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 Printablevariable to print values like this :

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

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"

“协议 'Printable' 只能用作通用约束,因为它具有 Self 或相关的类型要求”

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

采纳答案by Airspeed Velocity

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 Printableprotocol.

正如 Thomas 指出的那样,您可以通过根本不提供类型来声明变量(或者您可以明确地将其指定为 type Printer<Int>。但这里解释了为什么您不能拥有Printable协议类型。

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 xbe? 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?还是一个String?在 Swift 中,所有类型都必须在编译时固定。函数不能根据运行时确定的因素动态地从返回一种类型转换为另一种类型。

Instead, you can only use StoredTypeas 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, xis known to be of a specific type.

这是可以的,因为在编译时,就好像编译器写出两个版本printStoredValue:一个用于Ints,一个用于Strings。在这两个版本中,x已知属于特定类型。

回答by Patrick Goley

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 printeris 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.

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

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

需要注意的一件事是AnyPrinter不必显式保留基本实例。事实上,我们不能,因为我们不能声明AnyPrinter拥有一个Printer<T>属性。相反,我们得到一个指向_printbaseprint函数的函数指针。调用base.print而不调用它会返回一个函数,其中 base 被柯里化为 self 变量,因此保留以供将来调用。

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 Printereverywhere, you can provide a convenience initializer for AnyPrinterwhich injects that type.

如果您决定要在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

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

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

回答by Airspeed Velocity

Addressing your updated use case:

解决您更新的用例:

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