Мой недавний пост в блоге, 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).
Я создал новый репозиторий для этих маленьких технических примеров, и вы можете найти весь исходный код этого поста здесь .