通过类型过滤 Swift 的 [AnyObject] 数组

29

假设我有一个 AnyObject 类型的数组。

let grabBag: [AnyObject] = [ "Tom", 4, "Dick", NSObject(), "Harry" ]

我希望将其转换为字符串数组,并仅提取实际上是字符串的元素。 我期望这段代码可以工作:

let strings = grabBag.filter{ $0 is String } as! [String]      // 1

但是它会报错,错误信息为'Bool' is not convertible to 'String'。然而下面的代码可以正常运行:

let definitelyStrings = grabBag.filter{ $0 is String }         // 2
let strings = definitelyStrings as! [String]                   //

为什么2可行而1不行?是否有一种比2更简单的方法从一个[AnyObject]中提取和转换元素为[T]类型?


在示例2中,您不需要第二行:let strings = grabBag.filter{ $0 is String }就足够了。无需强制类型转换。通过strings is [String]证明它是正确的,该语句将返回true。 - vadian
2
@vadian let strings = grabBag.filter{ $0 is String } 返回的是带有 OP 的 grabBag 数组的 [AnyObject],而不是 [String] - Eric Aya
@EricD:但是你可以使用grabBag.filter{ $0 is String } .map{$0.lowercaseString },这通常在[AnyObject]中是不可能的。 - vadian
@vadian,您可以使用[AnyObject]来实现这个功能。 let lc = grabBag.map{ $0.lowercaseString }。它返回一个[String!]。这主要是因为在此上下文中,[AnyObject]隐式映射到NSArray。如果没有导入Foundation,您将无法执行此操作。 - Rob Napier
6个回答

59

最好使用compactMap来编写一个漂亮的一行代码:

let strings = grabBag.compactMap { $0 as? String }

现在 strings 的类型是 [String]


8
这就是flatMap的用途:
let strings = grabBag.flatMap{ $0 as? String }

这里需要一个返回可选值的闭包;如果该可选值不为nil,则将其添加到结果中。
(请注意,这与其他语言中的flatMap的含义不同,甚至与Swift中的另一种含义也不同。更好的名称应该是mapOptional或mapSome。但即使不一致,它仍然有点直观。它“映射到可选项,然后展开所有的nil”。Rob Mayoff指出,如果Optionals是SequenceTypes,那么这将是一个合理的名称。)

1
Scala的flatMap与Swift的flatMap处理它们各自的Option的方式相同。但是,Scala之所以能够“免费”使用它,是因为Scala的Option是一个完整的可遍历集合类型(包含0个或1个元素)。不幸的是,Swift的Optional目前不符合SequenceType - rob mayoff
@robmayoff 我误读了Scala的签名。它是flatMap[B](f: (A) ⇒ GenTraversableOnce[B]): List[B]。我以为它是flatMap[B](f: (A) ⇒ List[List[B]]): List[B]。好吧,我接受了。 - Rob Napier

5

我认为测试1的失败显然是编译器的错误。实际上,它在 REPL 中崩溃:

Welcome to Apple Swift version 2.0 (700.1.100.2 700.1.74). Type :help for assistance.
  1> import Foundation
  2> let grabBag: [AnyObject] = [ "Tom", 4, "Dick", NSObject(), "Harry" ]
grabBag: [AnyObject] = 5 values {
  [0] = "Tom"
  [1] = Int64(4)
  [2] = "Dick"
  [3] = {
    isa = NSObject
  }
  [4] = "Harry"
}
  3> let strings = grabBag.filter { $0 is String } as! String
strings: String = {
  _core = {
    _baseAddress =
    _countAndFlags =
    _owner = <extracting data from value failed>

  }
}
Execution interrupted. Enter Swift code to recover and continue.
Enter LLDB commands to investigate (type :help for assistance.)
4> :bt
* thread #1: tid = 0x501bac, 0x00000001005c41f4 $__lldb_expr12`main + 420 at repl.swift:3, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
  * frame #0: 0x00000001005c41f4 $__lldb_expr12`main + 420 at repl.swift:3
    frame #1: 0x0000000100001420 repl_swift`_mh_execute_header + 5152
    frame #2: 0x00007fff8dd725c9 libdyld.dylib`start + 1
    frame #3: 0x00007fff8dd725c9 libdyld.dylib`start + 1

无论如何,正如Rob Napier所回答的那样,grabBag.flatMap { $0 as? String }更简短,也许更简单易懂。

4

这里是一个基于ayaio答案的微小Swift 5数组扩展。如果你经常想按类型过滤而又不想每次都编写闭包,可能会很有用。

extension Array {
   func filteredByType<T> (_: T.Type) -> [T] {
       return compactMap({ (element) in
           return element as? T
       })
   }
}

使用示例:

let array: [Any] = ["foo", 47, ["baz"], "bar"]
let stringArray: [String] = array.filteredByType(String.self)
print(stringArray) // ["foo", "bar"]

1
你的这个扩展想法很好,但是它会失败。假设我有一个 NSWindow 和一个基于 NSWindow 的类叫做 MyWindowMyWindow 类的所有元素都将被视为 NSWindow,因此过滤器将无法工作。 - Duck
2
要么我没有理解你的观点,要么我认为我不会认为那是“失败”。这就是继承的工作方式。考虑一下:如果你有class Base {}class Special: Base {}在一个数组中 let array: [Any] = [Base(), Special()],然后通过过滤器过滤该数组 let baseItems = array.filteredByType(Base.self) // count 2let specialItems = array.filteredByType(Special.self) // count 1,我认为这是基于类型的过滤器的预期行为。 - Gamma
@Duck 请查看我的回答 - Mendy

0

改进的扩展,适用于任何序列:

extension Sequence {
    func filter<T>(by: T.Type) -> [T] {
        return compactMap{$0 as? T}
    }
}

0

按类型过滤数组的扩展:

extension Array {
    func filtered<T>(by _: T.Type) -> [T] {
        return filter { type(of: $0) == T.self } as! [T]
   }
}

使用示例:

view.addSubview(UIView())
view.addSubview(UILabel())
view.addSubview(UIImageView())
view.addSubview(UIView())

let allUIViewsInSubviews      = view.subviews.filtered(by: UIView.self)
let allUILabelsInSubviews     = view.subviews.filtered(by: UILabel.self)
let allUIImageViewsInSubviews = view.subviews.filtered(by: UIImageView.self)
print(allUIViewsInSubviews.count) // 2
print(allUILabelsInSubviews.count) // 1
print(allUIImageViewsInSubviews.count) // 1

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