创build一个扩展来从Swift中的数组中过滤nils
我试图写一个扩展到数组,这将允许一个可选的T数组被转换成一个非可选的T数组。
例如,这可以写成这样一个免费的function:
func removeAllNils(array: [T?]) -> [T] { return array .filter({ $0 != nil }) // remove nils, still a [T?] .map({ $0! }) // convert each element from a T? to a T }
但是,我不能把它作为一个扩展。 我试图告诉编译器,扩展只适用于可选值的数组。 这是我迄今为止:
extension Array { func filterNils<U, T: Optional<U>>() -> [U] { return filter({ $0 != nil }).map({ $0! }) } }
(它不会编译!)
限制为generics结构或类定义的types是不可能的 – 该数组被devise为与任何types一起工作,因此您不能添加适用于子types的方法。 types约束只能在声明generics时指定
实现你所需要的唯一方法是创build一个全局函数或一个静态方法 – 在后一种情况下:
extension Array { static func filterNils(array: [T?]) -> [T] { return array.filter { $0 != nil }.map { $0! } } } var array:[Int?] = [1, nil, 2, 3, nil] Array.filterNils(array)
或者简单地使用flatMap
,它可以用来删除所有的nil值:
[1, 2, nil, 4].flatMap { $0 } // Returns [1, 2, 4]
从Swift 2.0开始,你不需要编写你自己的扩展来过滤一个数组中的nil值,你可以使用flatMap
来平滑数组并过滤nils:
let optionals : [String?] = ["a", "b", nil, "d"] let nonOptionals = optionals.flatMap{$0} print(nonOptionals)
打印:
[a, b, d]
注意:
有2个flatMap
函数:
-
一个
flatMap
用于删除上面显示的非零值。 请参阅 – https://developer.apple.com/documentation/swift/sequence/2907182-flatmap -
另一个
flatMap
用于连接结果。 请参阅 – https://developer.apple.com/documentation/swift/sequence/2905332-flatmap
TL; DR
为了避免潜在的错误/混淆,不要使用array.flatMap { $0 }
来删除nils; 使用一个扩展方法,如array.removeNils()
来代替(下面的实现, 更新为Swift 3.0 )。
虽然array.flatMap { $0 }
大部分时间都在工作,但赞成array.removeNils()
扩展有几个原因:
-
removeNils
描述了你想要做的事情 :删除nil
值。 有人不熟悉flatMap
需要查看它,当他们查看它时,如果他们密切关注,他们会得出和我的下一个点相同的结论; -
flatMap
有两个不同的实现,它们完成两个完全不同的事情 。 基于types检查, 编译器将决定哪一个被调用。 这在Swift中可能是非常有问题的,因为type-inference被大量使用。 (例如,要确定variables的实际types,您可能需要检查多个文件。)重构可能会导致您的应用程序调用错误版本的flatMap
,从而导致难以发现的错误 。 - 由于有两个完全不同的function,因此可以使得理解
flatMap
变得更加困难,因为您可以很容易地将这两者混为一谈 。 - 可以在非可选数组(例如
[Int]
)上调用flatMap
,所以如果将数组从[Int?]
重构为[Int]
,可能会不小心留下flatMap { $0 }
调用 ,编译器不会提醒您。 最好它只会返回自己,在最坏的情况下,会导致其他实现被执行,可能导致错误。 - 在Swift 3中,如果你没有显式的返回types, 编译器会select错误的版本 ,这会导致意想不到的后果。 (见下面的Swift 3部分)
- 最后,它会减慢编译器的速度,因为types检查系统需要确定要调用哪个重载函数。
回顾一下,这个函数有两个版本,不幸的是,命名为flatMap
。
-
通过去除嵌套层次来平整序列(例如
[[1, 2], [3]] -> [1, 2, 3]
)public struct Array<Element> : RandomAccessCollection, MutableCollection { /// Returns an array containing the concatenated results of calling the /// given transformation with each element of this sequence. /// /// Use this method to receive a single-level collection when your /// transformation produces a sequence or collection for each element. /// /// In this example, note the difference in the result of using `map` and /// `flatMap` with a transformation that returns an array. /// /// let numbers = [1, 2, 3, 4] /// /// let mapped = numbers.map { Array(count: $0, repeatedValue: $0) } /// // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]] /// /// let flatMapped = numbers.flatMap { Array(count: $0, repeatedValue: $0) } /// // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] /// /// In fact, `s.flatMap(transform)` is equivalent to /// `Array(s.map(transform).joined())`. /// /// - Parameter transform: A closure that accepts an element of this /// sequence as its argument and returns a sequence or collection. /// - Returns: The resulting flattened array. /// /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence /// and *n* is the length of the result. /// - SeeAlso: `joined()`, `map(_:)` public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element] }
-
删除序列中的元素(例如
[1, nil, 3] -> [1, 3]
)public struct Array<Element> : RandomAccessCollection, MutableCollection { /// Returns an array containing the non-`nil` results of calling the given /// transformation with each element of this sequence. /// /// Use this method to receive an array of nonoptional values when your /// transformation produces an optional value. /// /// In this example, note the difference in the result of using `map` and /// `flatMap` with a transformation that returns an optional `Int` value. /// /// let possibleNumbers = ["1", "2", "three", "///4///", "5"] /// /// let mapped: [Int?] = numbers.map { str in Int(str) } /// // [1, 2, nil, nil, 5] /// /// let flatMapped: [Int] = numbers.flatMap { str in Int(str) } /// // [1, 2, 5] /// /// - Parameter transform: A closure that accepts an element of this /// sequence as its argument and returns an optional value. /// - Returns: An array of the non-`nil` results of calling `transform` /// with each element of the sequence. /// /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence /// and *n* is the length of the result. public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] }
#2是人们用{ $0 }
作为transform
来移除nil的方法。 这是因为该方法执行映射,然后过滤掉所有nil
元素。
您可能想知道“为什么苹果不重命名#2 removeNils()
”? 有一件事要记住,使用flatMap
删除nils不是#2的唯一用法。 实际上,由于两个版本都具有transform
function,所以它们可以比上面的例子更加强大。
例如,#1可以很容易地将一个string数组分割成单个字符(拼合),并大写每个字母(地图):
["abc", "d"].flatMap { $0.uppercaseString.characters } == ["A", "B", "C", "D"]
而#2号码可以很容易地删除所有的偶数(平铺),并乘以-1
(地图):
[1, 2, 3, 4, 5, 6].flatMap { ($0 % 2 == 0) ? nil : -$0 } == [-1, -3, -5]
(请注意,最后一个例子可能会导致Xcode 7.3旋转很长一段时间,因为没有明确的types,进一步certificate为什么这些方法应该有不同的名称)。
盲目使用flatMap { $0 }
删除nil
的真正危险不是当你在[1, 2]
调用时,而是当你像[[1], [2]]
上调用它的时候[[1], [2]]
。 在前一种情况下,它会无害地调用调用#2并返回[1, 2]
。 在后一种情况下,你可能会认为它会做同样的事情(无害地返回[[1], [2]]
因为没有nil
值),但是它实际上会返回[1, 2]
因为它使用了调用#1。
flatMap { $0 }
被用来删除nil
的事实似乎更多的是Swift 社区的 推荐,而不是来自Apple的推荐。 也许如果苹果注意到这个趋势,他们最终会提供一个removeNils()
函数或类似的东西。
在此之前,我们只剩下我们自己的解决scheme了。
解
// Updated for Swift 3.0 protocol OptionalType { associatedtype Wrapped func map<U>(_ f: (Wrapped) throws -> U) rethrows -> U? } extension Optional: OptionalType {} extension Sequence where Iterator.Element: OptionalType { func removeNils() -> [Iterator.Element.Wrapped] { var result: [Iterator.Element.Wrapped] = [] for element in self { if let element = element.map({ $0 }) { result.append(element) } } return result } }
(注意:不要混淆element.map
…这与本文讨论的flatMap
没有任何关系,它使用Optional
的map
函数来获取可展开的可选types,如果省略这部分,你会得到这样的语法错误:“错误:条件绑定的初始值设定项必须具有可选types,而不是'Self.Generator.Element'”有关map()
如何帮助我们的更多信息,请参阅我写的有关添加SequenceType上的扩展方法来计算非nils 。)
用法
let a: [Int?] = [1, nil, 3] a.removeNils() == [1, 3]
例
var myArray: [Int?] = [1, nil, 2] assert(myArray.flatMap { $0 } == [1, 2], "Flat map works great when it's acting on an array of optionals.") assert(myArray.removeNils() == [1, 2]) var myOtherArray: [Int] = [1, 2] assert(myOtherArray.flatMap { $0 } == [1, 2], "However, it can still be invoked on non-optional arrays.") assert(myOtherArray.removeNils() == [1, 2]) // syntax error: type 'Int' does not conform to protocol 'OptionalType' var myBenignArray: [[Int]?] = [[1], [2, 3], [4]] assert(myBenignArray.flatMap { $0 } == [[1], [2, 3], [4]], "Which can be dangerous when used on nested SequenceTypes such as arrays.") assert(myBenignArray.removeNils() == [[1], [2, 3], [4]]) var myDangerousArray: [[Int]] = [[1], [2, 3], [4]] assert(myDangerousArray.flatMap { $0 } == [1, 2, 3, 4], "If you forget a single '?' from the type, you'll get a completely different function invocation.") assert(myDangerousArray.removeNils() == [[1], [2, 3], [4]]) // syntax error: type '[Int]' does not conform to protocol 'OptionalType'
(请注意,最后一个flatMap返回[1, 2, 3, 4]
而removeNils()将返回[[1], [2, 3], [4]]
。)
解决scheme类似于@fabb链接到的答案 。
但是,我做了一些修改:
- 我没有把这个方法命名为
flatten
,因为已经有了一个flatten
的序列types方法,并且把完全不同的方法命名为同一个名字,这首先使我们陷入混乱之中。 更不用说,它更容易误解什么是flatten
不是removeNils
。 - 而不是在
OptionalType
上创build一个新的typesT
,它使用与Optional
使用(Wrapped
)相同的名称。 - 而不是执行
map{}.filter{}.map{}
,导致O(M + N)
时间,我循环一次数组。 - 而不是使用
flatMap
从Generator.Element
到Generator.Element.Wrapped?
,我用map
。map
函数内部不需要返回nil
值,所以map
就足够了。 通过避免使用flatMap
函数,很难将具有完全不同function的另一个(即第三个)方法混合在一起。
使用removeNils
vs. flatMap
的一个缺点是types检查器可能需要更多的提示:
[1, nil, 3].flatMap { $0 } // works [1, nil, 3].removeNils() // syntax error: type of expression is ambiguous without more context // but it's not all bad, since flatMap can have similar problems when a variable is used: let a = [1, nil, 3] // syntax error: type of expression is ambiguous without more context a.flatMap { $0 } a.removeNils()
我没有看太多,但似乎你可以添加:
extension SequenceType { func removeNils() -> Self { return self } }
如果你想能够在包含非可选元素的数组上调用该方法。 这可以使一个巨大的重命名(例如flatMap { $0 }
– > removeNils()
)更容易。
分配给自己不同于分配给一个新的variables?!
看看下面的代码:
var a: [String?] = [nil, nil] var b = a.flatMap{$0} b // == [] a = a.flatMap{$0} a // == [nil, nil]
令人惊讶的是, a = a.flatMap { $0 }
不会在您将其分配给a
时删除nils,但是当您将其分配给b
时,它将删除nils! 我的猜测是,这与重载的flatMap
和Swiftselect我们不想使用的那个有关。
您可以暂时通过将其转换为预期types来解决问题:
a = a.flatMap { $0 } as [String] a // == []
但这很容易忘记。 相反,我会build议使用上面的removeNils()
方法。
从Swift 2.0开始,可以通过使用where
子句添加一个方法来处理子types。 正如在这个Apple Forum Thread中所讨论的,这可以用来过滤一个数组的nil
值。 积分转到@nnnnnnnn和@SteveMcQwark。
由于where
子句尚不支持generics(如Optional<T>
),因此需要通过Protocol协议解决方法。
protocol OptionalType { typealias T func intoOptional() -> T? } extension Optional : OptionalType { func intoOptional() -> T? { return self.flatMap {$0} } } extension SequenceType where Generator.Element: OptionalType { func flatten() -> [Generator.Element.T] { return self.map { $0.intoOptional() } .filter { $0 != nil } .map { $0! } } } let mixed: [AnyObject?] = [1, "", nil, 3, nil, 4] let nonnils = mixed.flatten() // 1, "", 3, 4
斯威夫特4
这适用于Swift 4:
protocol OptionalType { associatedtype Wrapped var optional: Wrapped? { get } } extension Optional: OptionalType { var optional: Wrapped? { return self } } extension Sequence where Iterator.Element: OptionalType { func removeNils() -> [Iterator.Element.Wrapped] { return self.flatMap { $0.optional } } }
testing:
class UtilitiesTests: XCTestCase { func testRemoveNils() { let optionalString: String? = nil let strings: [String?] = ["Foo", optionalString, "Bar", optionalString, "Baz"] XCTAssert(strings.count == 5) XCTAssert(strings.removeNils().count == 3) let integers: [Int?] = [2, nil, 4, nil, nil, 5] XCTAssert(integers.count == 6) XCTAssert(integers.removeNils().count == 3) } }