【问题标题】:How to use generic protocol as a variable type如何使用泛型协议作为变量类型
【发布时间】:2014-12-31 19:57:30
【问题描述】:

假设我有一个协议:

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)

编译器抱怨这个错误:

"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)

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

【问题讨论】:

  • 这是 2014 年 Apple 开发者论坛上的一个相关主题,Apple 的 Swift 开发者在一定程度上解决了这个问题:devforums.apple.com/thread/230611(注意:需要 Apple 开发者帐户才能查看此页面。)

标签: ios xcode generics swift


【解决方案1】:

正如 Thomas 所指出的,您可以通过根本不提供类型来声明变量(或者您可以明确地将其指定为类型 Printer&lt;Int&gt;。但这里解释了为什么您不能拥有 @987654322 的类型@协议。

您不能将具有关联类型的协议视为常规协议,并将它们声明为独立变量类型。要考虑原因,请考虑这种情况。假设您声明了一个用于存储任意类型然后将其取回的协议:

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

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

现在,您将变量的类型作为类型实现的协议而不是实际类型的主要原因是,您可以将所有符合该协议的不同类型的对象分配给同一个变量,并在运行时根据对象的实际情况获得多态行为。

但如果协议具有关联类型,则不能这样做。以下代码在实践中如何工作?

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

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

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

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

这没关系,因为在编译时,就好像编译器写出两个版本的printStoredValue:一个用于Ints,一个用于Strings。在这两个版本中,x 被称为特定类型。

【讨论】:

  • 换句话说,没有办法将泛型协议作为参数,原因是 Swift 不支持泛型的 .NET 样式运行时支持?这很不方便。
  • 我的 .NET 知识有点模糊......你有在 .NET 中可以使用的类似示例吗?此外,很难看出您示例中的协议正在为您购买什么。在运行时,如果您将不同类型的打印机分配给您的p 变量,然后将无效类型传递给print,您期望的行为是什么?运行时异常?
  • 好的,C# 示例有点不同。在那里,您在某种程度上指定了协议。在 Swift 中,相当于创建一个泛型函数,并通过 where 子句限制函数的类型(参见我的第二个答案)
  • 理论上,如果可以像在 C# 中那样使用尖括号创建泛型协议,是否允许创建协议类型的变量? (StoringType, StoringType)
  • 在 Java 中,您可以执行 var someStorer: StoringType&lt;Int&gt;var someStorer: StoringType&lt;String&gt; 的等效操作并解决您概述的问题。
【解决方案2】:

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

printer 的类型已知为AnyPrinter&lt;Int&gt;,可用于抽象打印机协议的任何可能实现。虽然 AnyPrinter 在技术上不是抽象的,但它的实现只是一个真正的实现类型,并且可以用来将实现类型与使用它们的类型解耦。

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

要记住的另一件事是,此解决方案本质上是另一层动态调度,这意味着对性能的轻微影响。此外,类型擦除实例在底层实例之上需要额外的内存。由于这些原因,类型擦除不是免费的抽象。

显然,设置类型擦除需要做一些工作,但如果需要通用协议抽象,它会非常有用。这种模式可以在 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

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

【讨论】:

  • 谢谢。我更喜欢这种类型擦除模式(使用函数指针),而不是使用其他类型擦除教程中描述的抽象类(当然,它不存在,必须使用 fatalError() 伪造)。
【解决方案3】:

解决您更新的用例:

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

要对协议实现者实施特定限制,您可以约束协议的类型别名。因此,要创建要求元素可打印的协议集合:

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

现在如果你想实现一个只能包含可打印元素的集合:

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

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

相反,您应该创建将输入限制为包含可打印元素的集合的通用函数。

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

【讨论】:

  • 天哪,这看起来很恶心。我需要的只是拥有具有通用支持的协议。我希望有这样的东西:protocol Collection:SequenceType。就是这样。感谢您提供的代码示例,我认为需要一段时间才能消化它:)
猜你喜欢
  • 2023-04-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-12
  • 1970-01-01
  • 1970-01-01
  • 2023-03-27
  • 1970-01-01
相关资源
最近更新 更多