Статьи

Пользовательская функция Swift Zip, управляемая тестом, с использованием универсальных

Мой недавний пост в блоге,  Zip, Map и Generics , рассматривал новую функцию Swift  zip ()  для объединения двух массивов. zip  возвращает  SequenceType  с таким количеством элементов, как его кратчайшая входная последовательность. Что если мы захотим создать пользовательскую функцию zip, которая будет возвращать массив той же длины, что и самый длинный ввод, и дополнит самое короткое значением  nil ?

Функция будет работать примерно так:

    let arrayOne = [1, 2, 3, 4]
    let arrayTwo: [String?] = ["AAA", "BBB"]
    let result = longZip(arrayOne, arrayTwo) // expect [(1, "AAA"), (2, "BBB"), (3, nil), (4, nil)]

Например, если входные массивы являются необязательными строками,  [String] , наш сжатый результат должен будет возвращать необязательные значения,  [String?] , Чтобы учесть это заполнение. Поэтому при повторном использовании дженериков подпись  longZip будет выглядеть так:

    func longZip(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]

На этот раз давайте возьмем  тестовый  подход к разработке . Прежде чем писать какой-либо код, мы напишем тест. Я создал LongZipTests.swift,  который содержит два тестовых массива:

    let arrayOne = [1, 2, 55, 90]
    let arrayTwo = ["AAA", "BBB"]

Мои тесты гарантируют, что количество выходных данных равно 4, затем зациклите выходной поток, убедившись, что выходные элементы соответствуют элементам ввода или равны  нулю,  если нет ввода:

    func testLongZip_v1()
    {
        let resultOne = longZip_v1(arrayOne, arrayTwo)

        commonAssertions(resultOne)
    }

    func commonAssertions(results: [(Int?, String?)])
    {
        XCTAssert(results.count == 4, "Count")
        
        for (idx: Int, result:(Int?, String?))  in enumerate(results)
        {
            if idx < arrayOne.count
            {
                XCTAssertEqual(result.0!, arrayOne[idx], "Array One Value")
            }
            else
            {
                XCTAssertNil(result.0, "Array One nil")
            }
            
            if idx < arrayTwo.count
            {
                XCTAssertEqual(result.1!, arrayTwo[idx], "Array Two Value")
            }
            else
            {
                XCTAssertNil(result.1, "Array Two nil")
            }
        }
    }

Первая реализация  longZip ()  довольно проста: используйте  max (),  чтобы найти наибольшее количество, переберите оба массива с помощью  цикла for и заполните возвращаемый объект элементами из этих массивов или с помощью  nil,  если мы превысили счет :

    func longZip_v1(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]
    {
        let n = max(arrayOne.count, arrayTwo.count)
        
        var returnObjects = [(T?, U?)]()
        
        for var i = 0; i < n; i++
        {
            let returnObject: (T?, U?) = (i < arrayOne.count ? arrayOne[i] : nil, i < arrayTwo.count ? arrayTwo[i] : nil)
            
            returnObjects.append(returnObject)
        }
        
        return returnObjects
    }

Нажатие command-u выполняет тесты, которые, как правило, работают лучше, если ваши пальцы скрещены:

    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v1]' started.
    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v1]' passed (0.000 seconds).

Вы можете использовать  debugDescription (),  чтобы быть вдвойне уверенным:

    [(Optional(1), Optional("AAA")), (Optional(2), Optional("BBB")), (Optional(55), nil), (Optional(90), nil)]

Теперь у нас есть рабочая версия, пора разбирать код. Вторая версия использует  map ()  для преобразования из необязательного в необязательное и  extension ()  для добавления нулей, где это необходимо:

    func longZip_v2(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]
    {
        var arrayOneExtended = arrayOne.map({$0 as T?})
        var arrayTwoExtended = arrayTwo.map({$0 as U?})
        
        arrayOneExtended.extend([T?](count: max(0, arrayTwo.count - arrayOne.count), repeatedValue: nil))
        arrayTwoExtended.extend([U?](count: max(0, arrayOne.count - arrayTwo.count), repeatedValue: nil))
        
        return Array(zip(arrayOneExtended, arrayTwoExtended))
    }

… добавлен новый тестовый пример:

    func testLongZip_v2()
    {
        let resultOne = longZip_v2(arrayOne, arrayTwo)
        
        commonAssertions(resultOne)
    }

Пальцы скрещены на обеих руках …

    func testLongZip_v2()
    {
        let resultOne = longZip_v2(arrayOne, arrayTwo)
        
        commonAssertions(resultOne)
    }

Уф!

    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v2]' started.
    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v2]' passed (0.000 seconds).

Вторая версия была хорошей, но есть дублированный код и переменные, где я всегда предпочитаю константы. К сожалению, возвращение map ()  является неизменным, поэтому, несмотря на все мои усилия, я не смог связать  карту  и  продолжить  вместе. Однако, переместив этот общий код в отдельную функцию  extendWithNil () , я каким-то образом пошел на его устранение.

extendWithNil ()  принимает массив и целое число для желаемой новой длины и возвращает массив опций типа входного массива:

    func extendWithNil(array: [T], newCount: Int) -> [T?]

Опять же, прежде чем писать код, давайте напишем тест, который проверяет количество и то, что вновь добавленный элемент равен nil:

    func testExtendWithNil()
    {
        let array = ["AAA", "BBB"]
        let result = extendWithNil(array, 3)
        
        XCTAssert(resultOne.count == 3, "Count")
        XCTAssertNil(result[2], "Nil Added")
    }

Внутренности  extendWithNil ()  взяты из версии два из  longZip () :

    func extendWithNil(array: [T], newCount: Int) -> [T?]
    {
        var returnArray = array.map({$0 as T?})
        
        returnArray.extend([T?](count: max(0, newCount - array.count), repeatedValue: nil))
        
        return returnArray
    }

и, наконец, третья версия  longZip ()  использует  exteWithNil ()  для довольно аккуратной реализации:

    func longZip_v3(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]
    {
        let newCount = max(arrayOne.count, arrayTwo.count)
        
        let arrayOneExtended = extendWithNil(arrayOne, newCount)
        let arrayTwoExtended = extendWithNil(arrayTwo, newCount)
        
        return Array(zip(arrayOneExtended, arrayTwoExtended))
    }

Перед празднованием с коробкой  Сотовых Пальцев , заключительный тест:

    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testExtendWithNil]' started.
[Optional("AAA"), Optional("BBB"), nil]
    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testExtendWithNil]' passed (0.001 seconds).

    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v3]' started.
    Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v3]' passed (0.001 seconds).

Я создал новый репозиторий для этих маленьких технических примеров, и вы можете найти весь  исходный код этого поста здесь .