Я пытаюсь написать расширение для Array, которое позволит преобразовать массив необязательного T в массив не факультативных T.
например. это можно записать как свободную функцию, например:
Но я не могу заставить это работать как расширение. Я пытаюсь сказать компилятору, что расширение применяется только к массивам необязательных значений. Это то, что у меня есть до сих пор:
Ответ 3
TL; DR
Чтобы избежать возможных ошибок/путаницы, не используйте array.flatMap { $0 }
для удаления nils; используйте вместо этого метод расширения, например 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 - это тот, который люди используют для удаления nils, передавая { $0 }
как transform
. Это работает, поскольку метод выполняет отображение, а затем отфильтровывает все элементы nil
.
Возможно, вам интересно: "Почему Apple не переименовала №2 в removeNils()
"?. Следует иметь в виду, что использование flatMap
для удаления nils - это не единственное использование # 2. Фактически, поскольку обе версии принимают функцию transform
, они могут быть намного более мощными, чем те, что приведены выше.
Например, # 1 может легко разбить массив строк на отдельные символы (сгладить) и загладить каждую букву (карту):
["abc", "d"].flatMap { $0.uppercaseString.characters } == ["A", "B", "C", "D"]
В то время как число # 2 может легко удалить все четные числа (сгладить) и умножить каждое число на -1
(map):
[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
, как представляется, является скорее сообществом 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
map
function, чтобы получить необязательный тип, который может быть распакован. Если вы опустите эту часть, вы получите эту синтаксическую ошибку: "error: initializer для условной привязки должен иметь необязательный тип, а не" Self ".Generator.Element '." Для получения дополнительной информации о том, как map()
помогает нам, см. этот ответ, я написал о добавлении метода расширения в SequenceType для подсчета non-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 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
, сложнее скомпоновать еще один (т.е. Третий) метод с тем же именем, который имеет совершенно другую функцию.
Единственный недостаток использования removeNils
vs. flatMap
заключается в том, что для проверки типа может потребоваться немного больше намеков:
[1, nil, 3].flatMap { $0 } // works
[1, nil, 3].removeNils() // syntax error: type of expression is ambiguous without more context
// but it 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()
).
Назначение self отличается от назначения новой переменной?!
Взгляните на следующий код:
var a: [String?] = [nil, nil]
var b = a.flatMap{$0}
b // == []
a = a.flatMap{$0}
a // == [nil, nil]
Удивительно, но a = a.flatMap { $0 }
не удаляет nils, когда вы назначаете его a
, но он удаляет nils, когда вы назначаете его b
! Я предполагаю, что это имеет какое-то отношение к перегруженным flatMap
и Swift, которые мы не хотели использовать.
Вы можете временно решить проблему, переведя ее в ожидаемый тип:
a = a.flatMap { $0 } as [String]
a // == []
Но это легко забыть. Вместо этого я бы рекомендовал использовать метод removeNils()
выше.