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
How to use generic protocol as a variable type
提问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 Printable
variable 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 Printable
protocol.
正如 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 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
?还是一个String
?在 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 Int
s, and one for String
s. Within those two versions, x
is known to be of a specific type.
这是可以的,因为在编译时,就好像编译器写出两个版本printStoredValue
:一个用于Int
s,一个用于String
s。在这两个版本中,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 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.
的类型printer
是已知的AnyPrinter<Int>
并且可以用于抽象打印机协议的任何可能的实现。虽然 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.
需要注意的一件事是AnyPrinter
不必显式保留基本实例。事实上,我们不能,因为我们不能声明AnyPrinter
拥有一个Printer<T>
属性。相反,我们得到一个指向_print
baseprint
函数的函数指针。调用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 Printer
everywhere, you can provide a convenience initializer for AnyPrinter
which 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 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()
}
}