Swift中的泛型数组

63

我一直在尝试使用不同类型的通用类数组。通过以下示例代码最易于解释我的问题:

// Obviously a very pointless protocol...
protocol MyProtocol {
    var value: Self { get }
}

extension Int   : MyProtocol {  var value: Int    { return self } }
extension Double: MyProtocol {  var value: Double { return self } }

class Container<T: MyProtocol> {
    var values: [T]

    init(_ values: T...) {
        self.values = values
    }

    func myMethod() -> [T] {
        return values
    }
}

现在,如果我试图创建一个类似下面的容器数组:
var containers: [Container<MyProtocol>] = []

我遇到了以下错误:

协议'MyProtocol'只能用作泛型约束,因为它具有Self或关联类型的要求。

要解决这个问题,我可以使用[AnyObject]

let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]
// Explicitly stating the types just for clarity.

但是当枚举 containers 时,现在又出现了另一个“问题”:
for container in containers {
    if let c = container as? Container<Int> {
        println(c.myMethod())

    } else if let c = container as? Container<Double> {
        println(c.myMethod())
    }
}

如上所示的代码,确定了container的类型后,在两种情况下都调用了相同的方法。我的问题是:
是否有更好的方法来获取具有正确类型的Container,而不是将其强制转换为每种可能的Container类型?或者我是否忽略了其他什么东西?

“链表”的使用在这里是否合适呢? class ContainterNode<T, W> { var head: ContainerNode<T> var next: ContainerNode<W>? } - Congruent Tech. UG
5个回答

58

有一种方法 - 有点儿 - 可以做到你想要的 - 有点儿。通过协议,可以消除类型限制并仍然获得你想要的结果,但这不总是很美观。以下是我在你的情况下提出的协议:

protocol MyProtocol {
    func getValue() -> Self 
}

extension Int: MyProtocol {
    func getValue() -> Int {
        return self
    }
}

extension Double: MyProtocol {
    func getValue() -> Double {
        return self
    }
}

请注意,你最初在协议声明中放置的 value 属性已更改为返回该对象的方法。
这并不是很有趣。
但现在,由于你已经摆脱了协议中的 value 属性,MyProtocol 可以被用作类型,而不仅仅是类型约束。你的 Container 类现在甚至不需要再是泛型了。你可以像这样声明它:
class Container {
    var values: [MyProtocol]

    init(_ values: MyProtocol...) {
        self.values = values
    }

    func myMethod() -> [MyProtocol] {
        return values
    }
}

由于 Container 不再是泛型,您可以创建一个 Container 数组并遍历它们,打印出 myMethod() 方法的结果:

var containers = [Container]()

containers.append(Container(1, 4, 6, 2, 6))
containers.append(Container(1.2, 3.5))

for container in containers {
    println(container.myMethod())
}

//  Output: [1, 4, 6, 2, 6]
//          [1.2, 3.5]

关键在于构建一个协议,只包含通用函数,并对符合条件的类型没有其他要求。如果您能做到这一点,那么您就可以将该协议用作类型,而不仅仅是类型约束。

而且,作为奖励(如果您愿意这样称呼它),您的MyProtocol值数组甚至可以混合符合MyProtocol的不同类型。因此,如果您像这样为String添加了MyProtocol扩展:

extension String: MyProtocol {
    func getValue() -> String {
        return self
    }
}

您实际上可以使用混合类型初始化Container

let container = Container(1, 4.2, "no kidding, this works")

[警告-我正在一个在线游乐场中测试这个。我还没有能够在Xcode中测试它...]

编辑:

如果您仍希望Container是通用的,并且只包含一种类型的对象,您可以通过使遵循自己的协议来实现:

protocol ContainerProtocol {
    func myMethod() -> [MyProtocol]
}

class Container<T: MyProtocol>: ContainerProtocol {
    var values: [T] = []

    init(_ values: T...) {
        self.values = values
    } 

    func myMethod() -> [MyProtocol] {
        return values.map { $0 as MyProtocol }
    }
}

现在您仍然可以拥有一个包含 [ContainerProtocol] 对象的数组并通过迭代调用 myMethod():
let containers: [ContainerProtocol] = [Container(5, 3, 7), Container(1.2, 4,5)]

for container in containers {
    println(container.myMethod())
}

也许你仍然无法理解,但现在Container被限制为单一类型,而你仍然可以遍历ContainterProtocol对象的数组。

如果 OP 想让 Container 拥有一个 MyProtocol 对象数组,只要 MyProtocol 具有必需的值属性,Container 就必须是泛型。 - Aaron Rasmussen
谢谢你的回答。不幸的是,我需要 Container 只存储单一类型,但这是一个有趣的解决方案,我相信以后我会找到它的用处。 - ABakerSmith
看看我编辑过的答案。我认为你仍然可以使用协议来实现你想要做的事情。 - Aaron Rasmussen
1
我最终采纳了@Rob Napier的建议,并想出了另一种解决方案。不过,我认为这提供了一个很好的答案来回应原始问题。再次感谢! - ABakerSmith
1
这是一个非常有趣的解决方案。当你深入挖掘values的内容时,它变得更加有趣。它只是[MyProtocol],因此您唯一能够调用的是getValue(),它会为您提供像多态一样的动态调度,但不会让您窥视内部情况。getValue()的结果只是另一个MyProtocol,而不是IntDouble。因此,您不能应用任何其他不在协议中的“类似数字”的事物,只能再次使用getValue()。基本上,Self最终蒸发了,而getValue()并没有做任何有用的事情。但仍然是非常有趣的探索。 - Rob Napier
显示剩余4条评论

8
这是一个关于“您想发生什么?”的好例子,并实际展示了如果Swift有真正的一流类型会爆炸的复杂性。
protocol MyProtocol {
    var value: Self { get }
}

非常好。 MyProtocol.value 返回实现它的任何类型,记住这必须在编译时确定,而不是运行时。

var containers: [Container<MyProtocol>] = []

那么,在编译时确定,这是什么类型?不要考虑编译器,只需在纸上进行。是的,不确定那会是什么类型。我的意思是具体类型,没有元类型。

let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]

您走上错误的道路,当AnyObject已经混入了您的签名。这种方式不会起作用。因为只有苦衣。您还需要一个类型,并且您没有提供它。您提供了一个限制类型的规则,但没有实际的类型。回到您真正的问题上,并深入思考它。 (元类型分析几乎从来不是您的“真正”问题,除非您正在攻读计算机科学博士学位,否则您将在Idris而不是Swift中进行此操作。)您要解决什么实际问题?

1
感谢您的见解。"你实际解决的问题是什么?"让我重新思考我真正想要实现的目标。通过采用不同的方法来尝试存储所有内容,并进行一些重构,一切都可以在不使用AnyObject的情况下正常工作。 - ABakerSmith
1
Swift的泛型仍然很原始。而在其他语言中简单的事情,在Swift中仍然不可能或者太笨重了。没有必要让OP回到真正的问题上。 - Aleks N.
1
嗨,罗布,你的观察也帮了我很多。我已经到了一个“过于通用”的位置,而不是使用类型,我只是使用了“Any”作为解决方法。我仍在思考是否走错了路,或者Swift还不够“成熟”来做到这一点。无论如何,谢谢! - Roi Mulia

4

使用像Equatable这样的协议可以更好地解释。您不能声明一个数组[Equatable],因为尽管两个Int实例可以相互比较,两个Double实例也可以相互比较,但您不能将IntDouble进行比较,尽管它们都实现了Equatable

MyProtocol是一个协议,这意味着它提供了一个通用的接口。不幸的是,在定义中还使用了Self。这意味着符合MyProtocol的每种类型将以不同的方式实现它。

您自己编写了它 - Int将具有var value:Int作为value,而MyObject将具有var value:MyObject作为value

这意味着符合MyProtocol的结构/类不能用于替换另一个符合MyProtocol的结构/类。这也意味着您无法在没有指定具体类型的情况下以这种方式使用MyProtocol

如果将Self替换为具体类型,例如AnyObject,它将起作用。但是,目前(Xcode 6.3.1)在编译时会触发分段错误。


1
你不能将Int和Double进行比较 -- 虽然可以,但Swift缺乏自动转换。 - Aleks N.

1

如果你在游乐场里尝试这个修改过的示例,它会系统性崩溃:

// Obviously a very pointless protocol...
protocol MyProtocol {
    var value: Int { get }
}

extension Int   : MyProtocol {  var value: Int    { return self } }
//extension Double: MyProtocol {  var value: Double { return self } }

class Container<T: MyProtocol> {
    var values: [T]

    init(_ values: T...) {
        self.values = values
    }
}


var containers: [Container<MyProtocol>] = []

可能他们仍在处理这个问题,未来情况可能会有所改变。无论如何,就目前而言,我对此的解释是协议不是具体类型。因此,您不知道符合协议的东西将占用多少RAM空间(例如,一个Int可能不占用与Double相同的RAM量)。因此,在RAM中分配数组可能是一个非常棘手的问题。使用NSArray,您正在分配指针数组(指向NSObjects的指针),它们都占用相同的RAM量。您可以将NSArray视为“指向NSObject的指针”的具体类型数组。因此,没有计算RAM分配的问题。
请注意,Swift中的Array和Dictionary都是Generic Struct,而不是像Obj-C中包含指向对象的指针的对象。
希望这可以帮助你。

-3
我将数组声明更改为AnyObject类型的数组,以便可以使用filter、map和reduce函数(还添加了一些其他对象进行检查)。
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0), "Hello", "World", 42]

这将允许您在循环数组之前检查数组中的类型并进行过滤

let strings = containers.filter({ return ($0 is String) })

println(strings) // [Hello, World]

for ints in containers.filter({ return ($0 is Int) }) {
    println("Int is \(foo)") // Int is 42
}

let ints = containers.filter({ return ($0 is Container<Int>) })
// as this array is known to only contain Container<Int> instances, downcast and unwrap directly
for i in ints as [Container<Int>] {
    // do stuff
    println(i.values) // [1, 2, 3]
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接