【问题标题】:Generic Types Collection泛型类型集合
【发布时间】:2014-12-19 18:00:34
【问题描述】:

基于上一个已解决的问题,但它导致了另一个问题。如果协议/类类型存储在集合中,则检索并实例化它们会引发错误。下面是一个假设的例子。该范例基于“Program to Interface not an implementation”What does it mean to "program to an interface"?

instantiate from protocol.Type reference dynamically at runtime

public protocol ISpeakable {
    init()
    func speak()
}

class Cat : ISpeakable {
    required init() {}
    func speak() {
        println("Meow");
    }
}

class Dog : ISpeakable {
    required init() {}
    func speak() {
        println("Woof");
    }
}

//Test class is not aware of the specific implementations of ISpeakable at compile time
class Test {
    func instantiateAndCallSpeak<T: ISpeakable>(Animal:T.Type) {
        let animal = Animal()
        animal.speak()
    }
}

// Users of the Test class are aware of the specific implementations at compile/runtime

//works
let t = Test()
t.instantiateAndCallSpeak(Cat.self)
t.instantiateAndCallSpeak(Dog.self)

//doesn't work if types are retrieved from a collection
//Uncomment to show Error - IAnimal.Type is not convertible to T.Type
var animals: [ISpeakable.Type] = [Cat.self, Dog.self, Cat.self]

for animal in animals {
    //t.instantiateAndCallSpeak(animal) //throws error
}

for (index:Int, value:ISpeakable.Type) in enumerate(animals) {
    //t.instantiateAndCallSpeak(value) //throws error
}

编辑 - 我当前的解决方法是遍历集合,但当然它是有限的,因为 api 必须知道各种实现。另一个限制是这些类型的子类(例如 PersianCat、GermanShepherd)不会调用它们的覆盖函数,或者我去 Objective-C 进行救援(NSClassFromString 等)或等待 SWIFT 支持此功能。

注意(背景):这些类型由实用程序的用户推送到数组中,并在通知时执行 for 循环

var animals: [ISpeakable.Type] = [Cat.self, Dog.self, Cat.self]

for Animal in animals {
    if Animal is Cat.Type {
        if let AnimalClass = Animal as? Cat.Type {
            var instance = AnimalClass()
            instance.speak()
        }
    } else if Animal is Dog.Type {
        if let AnimalClass = Animal as? Dog.Type {
            var instance = AnimalClass()
            instance.speak()
        }
    }
}

【问题讨论】:

  • 这是一种设计模式/哲学,与 Java、.NET - C# 等、ActionScript 等无关
  • 我知道。但它在许多 Java 书籍中经常被提及,因此有评论。
  • 我明白了,我被阻止了,棺材上的最后一颗钉子,我完成了,构建了一些设计模式实用程序,我也就此联系了 Chris Lattner,你认为这是编译器的错误?
  • 问题是什么?
  • 顺便说一句,该系列是一个红鲱鱼。问题在于“超类型”。只需说let Speaker : ISpeakable.Type = Cat.self; t.instantiateAndCallSpeak(Speaker),您就可以更简单地得到相同的错误

标签: swift generic-programming generic-collections


【解决方案1】:

基本上答案是:正确,你不能那样做。 Swift 需要在编译时确定类型参数的具体类型,而不是在运行时。这出现在很多小角落案例中。例如,你不能构造一个泛型闭包并将其存储在一个变量中而不指定类型。

如果我们把它归结为一个最小的测试用例,这可能会更清楚一点

protocol Creatable { init() }

struct Object : Creatable { init() {} }

func instantiate<T: Creatable>(Thing: T.Type) -> T {
    return Thing()
}

// works. object is of type "Object"
let object = instantiate(Object.self)   // (1)

// 'Creatable.Type' is not convertible to 'T.Type'
let type: Creatable.Type = Object.self
let thing = instantiate(type)  // (2)

在第 1 行,编译器有一个问题:T 在这个 instantiate 的实例中应该是什么类型?这很简单,应该是Object。这是一个具体的类型,所以一切都很好。

在第 2 行,没有 Swift 可以生成的具体类型T。它只有Creatable,这是一个抽象类型(我们通过代码检查知道type 的实际值,但Swift 不考虑值,只考虑类型)。取回协议是可以的,但是把它们做成类型参数就不行了。今天只是不合法的 Swift。

Swift Programming Language: Generic Parameters and Arguments 中暗示了这一点:

在声明泛型类型、函数或初始化程序时,请指定泛型类型、函数或初始化程序可以使用的类型参数。当创建泛型类型的实例或调用泛型函数或初始化程序时,这些类型参数充当占位符,由 实际具体 类型参数替换。 (强调我的)

你需要在 Swift 中做任何你想做的事情。

作为一个有趣的奖励,尝试明确要求不可能:

let thing = instantiate(Creatable.self)

而且...迅速崩溃。


从您进一步的 cmets 来看,我认为闭包完全符合您的要求。您已经使您的协议需要简单的构造 (init()),但这是不必要的限制。您只需要调用者告诉函数如何构造对象。使用闭包很容易,并且完全不需要类型参数化。这不是解决方法。我相信这是实现您所描述的模式的更好方法。考虑以下内容(一些小改动使示例更像 Swift):

// Removed init(). There's no need for it to be trivially creatable.
// Cocoa protocols that indicate a method generally end in "ing" 
// (NSCopying, NSCoding, NSLocking). They do not include "I"
public protocol Speaking {
    func speak()
}

// Converted these to structs since that's all that's required for
// this example, but it works as well for classes.
struct Cat : Speaking {
    func speak() {
        println("Meow");
    }
}

struct Dog : Speaking {
    func speak() {
        println("Woof");
    }
}

// Demonstrating a more complex object that is easy with closures,
// but hard with your original protocol
struct Person: Speaking {
    let name: String
    func speak() {
        println("My name is \(name)")
    }
}

// Removed Test class. There was no need for it in the example,
// but it works fine if you add it.
// You pass a closure that returns a Speaking. We don't care *how* it does
// that. It doesn't have to be by construction. It could return an existing one.
func instantiateAndCallSpeak(builder: () -> Speaking) {
    let animal = builder()
    animal.speak()
}

// Can call with an immediate form.
// Note that Cat and Dog are not created here. They are not created until builder()
// is called above. @autoclosure would avoid the braces, but I typically avoid it.
instantiateAndCallSpeak { Cat() }
instantiateAndCallSpeak { Dog() }

// Can put them in an array, though we do have to specify the type here. You could
// create a "typealias SpeakingBuilder = () -> Speaking" if that came up a lot.
// Again note that no Speaking objects are created here. These are closures that
// will generate objects when applied.
// Notice how easy it is to pass parameters here? These don't all have to have the
// same initializers.
let animalBuilders: [() -> Speaking] = [{ Cat() } , { Dog() }, { Person(name: "Rob") }]

for animal in animalBuilders {
    instantiateAndCallSpeak(animal)
}

【讨论】:

  • 感谢 Rob 对知识的深入分享,所以说到 SWIFT 的另一种方式,我还在思考这个问题。如果我可以在一行中说出我需要什么,那就是“让这个实用程序存储对数组中一组类的引用并在它们上调用特定方法”,只有 API 的调用者知道这些类,因为它们是用户创建,这是我试图在 SWIFT 中实现的宏命令设计模式,任何横向思维 Rob?
  • 首先:你绝对需要上课吗?你能用函数(闭包)实现你想要的一切吗?而不是“调用特定方法”,只需传递将是该方法的函数。你可能太沉迷于面向对象的思维。闭包通常是命令模式的理想选择(这就是为什么 Cocoa 甚至在 ObjC 中也朝着它们发展的原因)。特别是,您应该阅读 Ole 关于此模式的文章,其中包含 curried 函数:oleb.net/blog/2014/07/swift-instance-methods-curried-functions
  • 是的,我意识到没有出路......我已经为我当前的方法添加了一个编辑部分,但这涉及类型检查和强制转换,显然它是有限的,因为模式 api 必须知道各种实现,限制使用此实用程序的人仅限于这些类型。暂时它会完成这项工作。我看过 curried 函数,它类似于我们在 javascript functionName.call(this, params)functionRef.apply(this, [params]) 中的函数。目标行动模式...(继续)
  • 目标动作模式似乎非常特定于领域,不仅如此,该模式还必须知道各种实现,或者参与者本身正在实例化相关部分,这种模式是理想的并且可以在 (ViewComponent - > GoF 的中介者模式)。
  • 实际上它是作为字典实现的,结果是编译器错误,var commands: [String: () -&gt; Command] = [:] 代替了
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-24
  • 2016-06-11
  • 2011-04-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多