TL;DR
Свифт 4
Используйте 1_. Apple обновила фреймворк, чтобы он больше не вызывал ошибок/путаницы.
Свифт 3
Чтобы избежать потенциальных ошибок/путаницы, не используйте array.flatMap { $0 }
для удаления нулей; вместо этого используйте метод расширения, такой как array.removeNils()
(реализация ниже, обновлена для Swift 3.0).
Хотя array.flatMap { $0 }
работает большую часть времени, есть несколько причин отдать предпочтение расширению array.removeNils()
:
removeNils
описывает именно то, что вы хотите сделать: удалить nil
значений. Кому-то, кто не знаком с flatMap
, нужно будет поискать его, и, когда он это сделает, если внимательно присмотрится, он придет к тому же выводу, что и мой следующий пункт;
flatMap
имеет две разные реализации, которые делают две совершенно разные вещи. Основываясь на проверке типов, компилятор решает, какой из них вызывать. Это может быть очень проблематично в Swift, так как активно используется вывод типов. (Например, чтобы определить фактический тип переменной, вам может потребоваться проверить несколько файлов.) Рефакторинг может привести к тому, что ваше приложение вызовет неправильную версию flatMap
, что может привести к трудно обнаруживаемым ошибкам. .
- Поскольку это две совершенно разные функции, это значительно усложняет понимание
flatMap
, поскольку вы можете легко объединить два.
flatMap
можно вызывать для необязательных массивов (например, [Int]
), поэтому при рефакторинге массива с [Int?]
на [Int]
вы можете случайно оставить flatMap { $0 }
вызовов, о которых компилятор вас не предупредит. В лучшем случае он просто вернет сам себя, в худшем — вызовет выполнение другой реализации, что может привести к ошибкам.
- В Swift 3, если вы явно не укажете возвращаемый тип, компилятор выберет неправильную версию, что приведет к непредвиденным последствиям. (См. раздел Swift 3 ниже)
- Наконец, это замедляет работу компилятора, поскольку системе проверки типов необходимо определить, какую из перегруженных функций вызывать.
Напомним, что есть две версии рассматриваемой функции, обе, к сожалению, названы 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
.
Вы можете задаться вопросом «Почему Apple не переименовала #2 в removeNils()
»? Следует иметь в виду, что использование flatMap
для удаления нулей — не единственное использование #2. На самом деле, поскольку обе версии используют функцию transform
, они могут быть гораздо более мощными, чем приведенные выше примеры.
Например, #1 может легко разделить массив строк на отдельные символы (выравнивание) и сделать каждую букву заглавной (карта):
["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 вращаться в течение очень долгого времени, потому что не указаны явные типы. Еще одно доказательство того, почему методы должны иметь разные имена.)
Реальная опасность слепого использования flatMap { $0 }
для удаления nil
возникает не тогда, когда вы вызываете его для [1, 2]
, а когда вы вызываете его для чего-то вроде [[1], [2]]
. В первом случае он безвредно вызовет вызов № 2 и вернет [1, 2]
. В последнем случае вы можете подумать, что он сделает то же самое (безвредно вернет [[1], [2]]
, так как нет значений nil
), но на самом деле он вернет [1, 2]
, поскольку он использует вызов № 1.
Тот факт, что flatMap { $0 }
используется для удаления nil
s, кажется, больше относится к Swift сообществу рекомендация, а не от Apple. Возможно, если Apple заметит эту тенденцию, они в конечном итоге предоставят функцию removeNils()
или что-то подобное.
А пока нам остается придумать собственное решение.
Решение
// 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
's map
function, чтобы получить необязательный тип, который можно развернуть. Если вы пропустите эту часть, вы получите следующую синтаксическую ошибку: " ошибка: инициализатор для условной привязки должен иметь дополнительный тип, а не 'Self.Generator.Element'." Дополнительную информацию о том, как map()
помогает нам, см. >этот ответ я написал о добавлении метода расширения в SequenceType для подсчета ненулевых значений.)
использование
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]]
.)
Решение похоже на ответ, на который ссылается @fabb.
Тем не менее, я сделал несколько модификаций:
- Я не назвал метод
flatten
, так как уже существует метод flatten
для типов последовательностей, а присвоение одного и того же имени совершенно разным методам — вот что в первую очередь привело нас к этой неразберихе. Не говоря уже о том, что гораздо проще неправильно понять, что делает flatten
, чем removeNils
.
- Вместо того, чтобы создавать новый тип
T
для OptionalType
, он использует то же имя, что и Optional
(Wrapped
).
- Вместо выполнения
map{}.filter{}.map{}
, что приводит к O(M + N)
время, я перебираю массив один раз.
- Вместо использования
flatMap
для перехода от Generator.Element
к Generator.Element.Wrapped?
я использую map
. Нет необходимости возвращать значения nil
внутри функции map
, поэтому map
будет достаточно. Избегая функции flatMap
, сложнее объединить еще один (т.е. 3-й) метод с тем же именем, который имеет совершенно другую функцию.
Единственный недостаток использования removeNils
по сравнению с flatMap
заключается в том, что для проверки типов может потребоваться немного больше подсказок:
[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()
).
Присвоение себе отличается от присвоения новой переменной?!
Взгляните на следующий код:
var a: [String?] = [nil, nil]
var b = a.flatMap{$0}
b // == []
a = a.flatMap{$0}
a // == [nil, nil]
Удивительно, но a = a.flatMap { $0 }
не удаляет нули, когда вы назначаете его a
, но удаляет нули, когда вы назначаете его b
! Я предполагаю, что это как-то связано с перегруженным flatMap
и Swift, выбравшим тот, который мы не собирались использовать.
Вы можете временно решить проблему, приведя ее к ожидаемому типу:
a = a.flatMap { $0 } as [String]
a // == []
Но об этом можно легко забыть. Вместо этого я бы рекомендовал использовать описанный выше метод removeNils()
.
Обновлять
Похоже, есть предложение отказаться от поддержки хотя бы одной из (3) перегрузок flatMap
: https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md
person
Senseful
schedule
24.07.2016