swift объединяет декларативный синтаксис - PullRequest
2 голосов
/ 17 января 2020

Декларативный синтаксис Swift Combine выглядит для меня странно, и кажется, что происходит много чего, что не видно.

Например, следующий пример кода собирается и работает на площадке Xcode:

[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)")
})

Я вижу, что я предполагаю, что это экземпляр массива, создаваемый с помощью [1, 2, 3]. Я предполагаю, что это литерал массива, но я не привык видеть, что он "объявлен", не присваивая его имени переменной или константе или используя _ =.

. Я вставил преднамеренно новую строку после а затем .publisher. Xcode игнорирует пробелы и символы новой строки?

Из-за этого стиля или из-за моего нововведения в визуальном разборе этого стиля я ошибочно подумал, что «receiveValue:» был параметром variadi c или каким-то новым синтаксисом, но позже понял, что на самом деле это аргумент. раковина (...).

Ответы [ 2 ]

3 голосов
/ 19 января 2020

Сначала очистка кода

Форматирование

Для начала, чтение / понимание этого кода было бы намного проще, если бы он был правильно отформатирован. Итак, давайте начнем с этого:

[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

Мы можем дополнительно очистить карту:

  1. Использование неявного возврата

    map({ (val) in
        return val * 3
    })
    
  2. Использование неявного возврата

    map({ (val) in
        val * 3
    })
    
  3. Снять ненужные скобки вокруг объявления параметра

    map({ val in
        val * 3
    })
    
  4. Удалите ненужные новые строки. Иногда они полезны для визуального разделения вещей, но это достаточно простое закрытие, которое просто добавляет ненужный шум

    map({ val in val * 3 })
    
  5. Используйте неявный параметр вместо val, который в любом случае не описательный

    map({ $0 * 3 })
    
  6. Использовать конечный синтаксис закрытия

    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:). С двумя аргументами закрытия.

    1. Закрытие { completion in ... } предоставляется в качестве аргумента параметра, помеченного receiveCompletion:
    2. Закрытие { 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)
    }
    

    , но есть много проблем:

    1. Это много кода кода, который вы повторяете по всей базе кода, с небольшими различиями.
    2. Сложнее читать. Поскольку for является такой общей конструкцией, здесь возможно много вещей. Чтобы точно выяснить, что происходит, вам нужно изучить более подробно.
    3. Вы упустили возможность оптимизации, не вызвав Array.reserveCapacity(_:). Эти повторные вызовы append могут достигнуть максимальной емкости буфера result массивов. В этот момент:

      • должен быть выделен новый больший буфер
      • существующие элементы result должны быть скопированы поверх
      • старый буфер должен быть выпущен
      • и, наконец, новый transformedElement должен быть добавлен в

      Эти операции могут быть дорогими. И по мере того, как вы добавляете все больше и больше элементов, вы можете исчерпать емкость несколько раз, вызывая многократные операции перезарядки. Вызвав result.reserveCapacity(input.count), вы можете указать массиву заранее выделить буфер идеального размера, чтобы не потребовалось никаких операций повторного копирования.

    4. Массив result должен быть изменяемым, даже если вам не понадобится изменять его после его создания.

    Этот код может быть записан как вызов map:

    let result = input.map(transform)
    

    Это имеет много преимуществ:

    1. Это короче (хотя это не всегда хорошо, в этом случае ничего не теряется, если оно будет короче)
    2. Это более понятно. map - это очень специфический c инструмент, который может выполнять только одно действие. Как только вы видите map, вы знаете, что input.count == result.count, и что результатом является массив выходных данных transform функции / замыкания.
    3. Это оптимизировано, внутренне map вызывает reserveCapacity, и он никогда не забудет этого.
    4. result может быть неизменным.

    Вызов map соответствует более декларативному стилю программирования. Вы не возитесь с деталями размеров массивов, итераций, добавления или чего-либо еще. Если у вас input.map { $0 * $0 }, вы говорите: «Я хочу, чтобы элементы ввода были в квадрате», конец. Реализация карты будет иметь for 1 oop, append с и др. c. необходимо сделать это. Несмотря на то, что она реализована в императивном стиле, функция абстрагирует ее и позволяет писать код на более высоких уровнях абстракции, где вы не копаетесь с не относящимися к делу вещами, такими как for loop.

2 голосов
/ 17 января 2020

Литералы

Сначала о литералах. Вы можете использовать литерал везде, где можете использовать переменную, содержащую это же значение. Между

let arr = [1,2,3]
let c = arr.count

и

let c = [1,2,3].count

Пробелами

Второе, относительно пробелов, нет. Проще говоря, Swift не волнует, разбиваете ли вы оператор перед точкой. Таким образом, нет никакой разницы между

let c = [1,2,3].count

и

let c = [1,2,3]
    .count

Цепочкой

И когда вы соединяете лот функций одна за другой Расщепление на самом деле отличный способ улучшить разборчивость. Вместо

let c = [1,2,3].filter {$0>2}.count

лучше написать

let c = [1,2,3]
    .filter {$0>2}
    .count

или для еще большей ясности

let c = [1,2,3]
    .filter {
        $0>2
    }
    .count

Выводы

Это все, что происходит в код, который вы показали: литерал, сопровождаемый длинной цепочкой вызовов методов. Для ясности они разбиты на отдельные строки. Вот и все.

Так что ничего из того, что вы упомянули в своем вопросе, не имеет ничего общего с Combine. Это просто базовый материал о языке Swift. Все, о чем вы говорите, может (и происходит) происходить в коде, который вообще не использует Combine.

Таким образом, с синтаксической точки зрения, ничего не происходит, «то, что не видно», кроме Знайте, что каждый вызов метода возвращает значение, к которому может быть применен следующий вызов метода (так же, как в моем примере кода выше, где я применяю .count к результату .filter). Конечно, поскольку ваш пример - Объединение, что-то «происходит, что не видно», а именно, что каждое из этих значений является издателем, оператором или подписчиком (и подписчики действительно подписываются) , Но это в основном просто вопрос о том, что такое Combine. Итак:

  • [1,2,3] - это массив, представляющий собой последовательность, поэтому он имеет метод publisher.

  • publisher Метод, который можно применить к последовательности, производит издатель.

  • Метод map (Combine's map, а не Array * map) может быть применен к издателю и создает другой объект, который является издателем.

  • К этому можно применить метод sink, который создает подписчика, и это конец цепочки.

...