Сначала очистка кода
Форматирование
Для начала, чтение / понимание этого кода было бы намного проще, если бы он был правильно отформатирован. Итак, давайте начнем с этого:
[1, 2, 3]
.publisher
.map({ (val) in
return val * 3
})
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Something went wrong: \(error)")
case .finished:
print("Received Completion")
}
},
receiveValue: { value in
print("Received value \(value)")
}
)
Очистка выражения map
Мы можем дополнительно очистить карту:
Использование неявного возврата
map({ (val) in
return val * 3
})
Использование неявного возврата
map({ (val) in
val * 3
})
Снять ненужные скобки вокруг объявления параметра
map({ val in
val * 3
})
Удалите ненужные новые строки. Иногда они полезны для визуального разделения вещей, но это достаточно простое закрытие, которое просто добавляет ненужный шум
map({ val in val * 3 })
Используйте неявный параметр вместо val
, который в любом случае не описательный
map({ $0 * 3 })
Использовать конечный синтаксис закрытия
map { $0 * 3 }
Окончательный результат
с нумерацией строк, так что я могу легко вернуться назад.
/* 1 */[1, 2, 3]
/* 2 */ .publisher
/* 3 */ .map { $0 * 3 }
/* 4 */ .sink(
/* 5 */ receiveCompletion: { completion in
/* 6 */ switch completion {
/* 7 */ case .failure(let error):
/* 8 */ print("Something went wrong: \(error)")
/* 9 */ case .finished:
/* 10 */ print("Received Completion")
/* 11 */ }
/* 12 */ },
/* 13 */ receiveValue: { value in
/* 14 */ print("Received value \(value)")
/* 15 */ }
/* 16 */ )
Проходя через него.
Строка 1, [1, 2, 3]
Строка 1 - литерал массива. Это выражение, как 1
, "hi"
, true
, someVariable
или 1 + 1
. Массив, подобный этому, не должен быть назначен чему-либо для его использования.
Интересно, что это не обязательно означает, что это массив. Вместо этого у Свифта есть ExpressibleByArrayLiteralProtocol
. Любой соответствующий тип может быть инициализирован из литерала массива. Например, Set
соответствует, поэтому вы можете написать: let s: Set = [1, 2, 3]
, и вы получите Set
, содержащий 1
, 2
и 3
. При отсутствии другой информации о типе (например, приведенной выше аннотации типа Set
) Swift использует Array
в качестве предпочтительного литерального типа массива.
Строка 2, .publisher
Строка 2 вызывает свойство publisher
литерала массива. Это возвращает Sequence<Array<Int>, Never>
. Это не обычный Swift.Sequence
, который не является универсальным протоколом c, а скорее находится в пространстве имен Publishers
(без учета регистра) в Combine
модуль. Таким образом, его полностью определенный тип - Combine.Publishers.Sequence<Array<Int>, Never>
.
Это Publisher
, чей Output
равен Int
, а тип Failure
равен Never
(то есть ошибка невозможно, так как невозможно создать экземпляр типа Never
.
Строка 3, map
Строка 3 вызывает функцию экземпляра map
(известный также как метод) значения Combine.Publishers.Sequence<Array<Int>, Never>
выше. Каждый раз, когда элемент проходит через эту цепочку, он будет преобразован закрытием, заданным для map
.
1
будет go in, 3
выйдет. - Тогда
2
войдет go в, и 6
выйдет. - Наконец
3
будет go в, и 6
выйдет. * Результатом этого выражения является еще один Combine.Publishers.Sequence<Array<Int>, Never>
Строка 4, sink(receiveCompletion:receiveValue:)
Строка 4 - это вызов Combine.Publishers.Sequence<Array<Int>, Never>.sink(receiveCompletion:receiveValue:)
. С двумя аргументами закрытия.
- Закрытие
{ completion in ... }
предоставляется в качестве аргумента параметра, помеченного receiveCompletion:
- Закрытие
{ value in ... }
предоставляется в качестве аргумента параметра с пометкой receiveValue:
Sink создает нового подписчика со значением Subscription<Array<Int>, Never>
, которое у нас было выше. Когда элементы поступают, вызывается закрытие receiveValue
и передается в качестве аргумента его параметру value
.
В конце концов издатель завершает работу, вызывая закрытие receiveCompletion:
. Аргументом для параметра completion
будет значение типа Subscribers.Completion
, которое является перечислением с регистром .failure(Failure)
или .finished
. Поскольку тип Failure
равен Never
, на самом деле здесь невозможно создать значение .failure(Never)
. Таким образом, завершение всегда будет .finished
, что приведет к вызову print("Received Completion")
. Оператор print("Something went wrong: \(error)")
является мертвым кодом, который никогда не может быть достигнут.
Обсуждение "декларативного"
Нет единого элемента syntacti c, который делает этот код квалифицированным как "декларативный". Декларативный стиль - это отличие от «императивного» стиля. В императивном стиле ваша программа состоит из ряда императивов или шагов, которые необходимо выполнить, обычно с очень жестким порядком.
В декларативном стиле ваша программа состоит из серии объявлений. Детали того, что необходимо для выполнения этих объявлений, абстрагированы, например, от библиотек, таких как Combine
и SwiftUI
. Например, в этом случае вы заявляете, что print("Received value \(value)")
из тройного числа должно быть напечатано всякий раз, когда число приходит из [1, 2, 3].publisher
. Издатель является базовым примером c, но вы можете представить издателя, который генерирует значения из текстового поля, где события происходят в неизвестное время.
Мой любимый пример маскировки императивных и декларативных стилей - использование такая функция, как Array.map(_:)
.
Вы можете написать:
var input: [InputType] = ...
var result = [ResultType]()
for element in input {
let transformedElement = transform(element)
result.append(result)
}
, но есть много проблем:
- Это много кода кода, который вы повторяете по всей базе кода, с небольшими различиями.
- Сложнее читать. Поскольку
for
является такой общей конструкцией, здесь возможно много вещей. Чтобы точно выяснить, что происходит, вам нужно изучить более подробно. Вы упустили возможность оптимизации, не вызвав Array.reserveCapacity(_:)
. Эти повторные вызовы append
могут достигнуть максимальной емкости буфера result
массивов. В этот момент:
- должен быть выделен новый больший буфер
- существующие элементы
result
должны быть скопированы поверх - старый буфер должен быть выпущен
- и, наконец, новый
transformedElement
должен быть добавлен в
Эти операции могут быть дорогими. И по мере того, как вы добавляете все больше и больше элементов, вы можете исчерпать емкость несколько раз, вызывая многократные операции перезарядки. Вызвав result.reserveCapacity(input.count)
, вы можете указать массиву заранее выделить буфер идеального размера, чтобы не потребовалось никаких операций повторного копирования.
Массив result
должен быть изменяемым, даже если вам не понадобится изменять его после его создания.
Этот код может быть записан как вызов map
:
let result = input.map(transform)
Это имеет много преимуществ:
- Это короче (хотя это не всегда хорошо, в этом случае ничего не теряется, если оно будет короче)
- Это более понятно.
map
- это очень специфический c инструмент, который может выполнять только одно действие. Как только вы видите map
, вы знаете, что input.count == result.count
, и что результатом является массив выходных данных transform
функции / замыкания. - Это оптимизировано, внутренне
map
вызывает reserveCapacity
, и он никогда не забудет этого. result
может быть неизменным.
Вызов map
соответствует более декларативному стилю программирования. Вы не возитесь с деталями размеров массивов, итераций, добавления или чего-либо еще. Если у вас input.map { $0 * $0 }
, вы говорите: «Я хочу, чтобы элементы ввода были в квадрате», конец. Реализация карты будет иметь for
1 oop, append
с и др. c. необходимо сделать это. Несмотря на то, что она реализована в императивном стиле, функция абстрагирует ее и позволяет писать код на более высоких уровнях абстракции, где вы не копаетесь с не относящимися к делу вещами, такими как for
loop.