Понимание списка в Swift

Языковой справочник не выявил никаких следов понимания списка. Какой самый лучший способ сделать это в Swift? Я ищу что-то похожее на:

evens = [ x for x in range(10) if x % 2 == 0]

7 ответов

Решение

Начиная с Swift 2.x, есть несколько коротких эквивалентов для понимания списка в стиле Python.

Самая простая адаптация формулы Python (которая выглядит как "применить преобразование к последовательности, к которой применяется фильтр") включает в себя цепочку map а также filter методы доступны для всех SequenceType с, и начиная с Range:

// Python: [ x for x in range(10) if x % 2 == 0 ]
let evens = (0..<10).filter { $0 % 2 == 0 }

// Another example, since the first with 'x for x' doesn't
// use the full ability of a list comprehension:
// Python: [ x*x for x in range(10) if x % 2 == 0 ]
let evenSquared = (0..<10).filter({ $0 % 2 == 0 }).map({ $0 * $0 })

Обратите внимание, что Range является абстрактным - он фактически не создает весь список значений, которые вы запрашиваете, а просто конструкцию, которая лениво предоставляет их по требованию. (В этом смысле это больше похоже на Python xrange.) Тем не менее filter вызов возвращает Array Таким образом, вы теряете "ленивый" аспект там. Если вы хотите сохранить коллекцию ленивой, просто скажите так:

// Python: [ x for x in range(10) if x % 2 == 0 ]
let evens = (0..<10).lazy.filter { $0 % 2 == 0 }
// Python: [ x*x for x in range(10) if x % 2 == 0 ]
let evenSquared = (0..<10).lazy.filter({ $0 % 2 == 0 }).map({ $0 * $0 })

В отличие от синтаксиса понимания списка в Python (и аналогичных конструкций в некоторых других языках), эти операции в Swift следуют тому же синтаксису, что и другие операции. То есть это тот же стиль синтаксиса для построения, фильтрации и работы с диапазоном чисел, что и для фильтрации и работы с массивом объектов - вам не нужно использовать синтаксис функции / метода для одного вида работы и список синтаксиса понимания для другого.

И вы можете передать другие функции в filter а также map звонки и цепочка в других удобных преобразованиях, как sort а также reduce:

// func isAwesome(person: Person) -> Bool
// let people: [Person]
let names = people.filter(isAwesome).sort(<).map({ $0.name })

let sum = (0..<10).reduce(0, combine: +)

В зависимости от того, к чему вы стремитесь, могут быть более краткие способы сказать, что вы имеете в виду. Например, если вы хотите получить список четных целых чисел, вы можете использовать stride:

let evenStride = 0.stride(to: 10, by: 2) // or stride(through:by:), to include 10

Как и в случае с диапазонами, это дает вам генератор, поэтому вы захотите сделать Array из него или итерации по нему, чтобы увидеть все значения:

let evensArray = Array(evenStride) // [0, 2, 4, 6, 8]

Изменить: сильно переработан для Swift 2.x. Смотрите историю редактирования, если вы хотите Swift 1.x.

С Swift 3, в соответствии с вашими потребностями или вкусами, вы можете выбрать один из семи следующих кодов Playground, которые являются своего рода эквивалентом понимания списка Python.


# 1. С помощью stride(from:to:by:) функция

let sequence = stride(from: 0, to: 10, by: 2)
let evens = Array(sequence)
// let evens = sequence.map({ $0 }) // also works
print(evens) // prints [0, 2, 4, 6, 8]

#2. С помощью CountableRangefilter(_:) метод

let range = 0 ..< 10
let evens = range.filter({ $0 % 2 == 0 })
print(evens) // prints [0, 2, 4, 6, 8]

#3. С помощью CountableRangeflatMap(_:) метод

let range = 0 ..< 10
let evens = range.flatMap({ $0 % 2 == 0 ? $0 : nil })
print(evens) // prints [0, 2, 4, 6, 8]

#4. С помощью sequence(first:next:) функция

let unfoldSequence = sequence(first: 0, next: {
    $0 + 2 < 10 ? $0 + 2 : nil
})
let evens = Array(unfoldSequence)
// let evens = unfoldSequence.map({ $0 }) // also works
print(evens) // prints [0, 2, 4, 6, 8]

#5. С помощью AnySequenceinit(_:) инициализатор

let anySequence = AnySequence<Int>({ () -> AnyIterator<Int> in
    var value = 0
    return AnyIterator<Int> {
        defer { value += 2 }
        return value < 10 ? value : nil
    }
})
let evens = Array(anySequence)
// let evens = anySequence.map({ $0 }) // also works
print(evens) // prints [0, 2, 4, 6, 8]

#6. Использование цикла for с предложением where

var evens = [Int]()
for value in 0 ..< 10 where value % 2 == 0 {
    evens.append(value)
}
print(evens) // prints [0, 2, 4, 6, 8]

# 7. Использование цикла for с условием if

var evens = [Int]()
for value in 0 ..< 10 {
    if value % 2 == 0 {
        evens.append(value)
    }
}
print(evens) // prints [0, 2, 4, 6, 8]

Как правило, понимание списка в Python можно записать в виде:

[f(x) for x in xs if g(x)]

Который так же, как

map(f, filter(g, xs))

Поэтому в Swift вы можете написать это как

listComprehension<Y>(xs: [X], f: X -> Y, g: X -> Bool) = map(filter(xs, g), f)

Например:

map(filter(0..<10, { $0 % 2 == 0 }), { $0 })

С Swift 2 вы можете сделать что-то вроде этого:

var evens = [Int]()
for x in 1..<10 where x % 2 == 0 {
    evens.append(x)
}

// or directly filtering Range due to default implementations in protocols (now a method)
let evens = (0..<10).filter{ $0 % 2 == 0 }

Должен признать, я удивлен, что никто не упомянул flatmap, так как я думаю, что Swift ближе всего перечисляет (или устанавливает, или диктует) понимание.

var evens = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].flatMap({num -> Int? in 
    if num % 2 == 0 {return num} else {return nil}
})

Flatmap берет замыкание, и вы можете либо вернуть отдельные значения (в этом случае он вернет массив со всеми ненулевыми значениями и отбросить нули), либо вернуть сегменты массива (в этом случае он объединит все ваши сегменты вместе и верни это.)

Похоже, что Flatmap в большинстве случаев (всегда?) Не может вывести возвращаемые значения. Конечно, в этом случае это невозможно, поэтому я указываю это как -> Int? так что я могу вернуть ноль, и, таким образом, отбросить нечетные элементы.

Вы можете вкладывать плоские карты, если хотите. И я нахожу их намного более интуитивными (хотя, очевидно, также немного более ограниченными), чем комбинация карты и фильтра. Например, "четные квадраты" в верхнем ответе, используя flatmap, становятся

var esquares = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].flatMap({num -> Int? in 
    if num % 2 == 0 {return num * num} else {return nil}
})

Синтаксис не такой однострочный, как у Python, не совсем такой же, как и все остальное. Я не уверен, что мне нравится это меньше (потому что для простых случаев в python это очень короткий и все еще очень читаемый) или больше (потому что сложные случаи могут выйти из-под контроля, и опытные программисты на python часто думают, что они идеально удобочитаемым и поддерживаемым, когда новичку в той же компании может потребоваться полчаса, чтобы разобраться, что он должен был сделать, не говоря уже о том, что он на самом деле делает.

Вот версия flatMap, из которой вы возвращаете отдельные элементы или ноль, а вот версия, из которой вы возвращаете сегменты.

Вероятно, стоит также рассмотреть и array.map, и array.forEach, потому что оба они также очень удобны.

Одним из аспектов понимания списка, который не был упомянут в этой теме, является тот факт, что вы можете применить его к декартовому продукту нескольких списков. Пример в Python:

[x + y for x in range(1,6) for y in range(3, 6) if x % 2 == 0]

... или Хаскель:

[x+y | x <- [1..5], y <- [3..5], x `mod` 2 == 0]

В Swift эквивалентная логика с 2 списками

list0
    .map { e0 in
        list1.map { e1 in
            (e0, e1)
        }
    }
.joined()
.filter(f)
.map(g)

И нам пришлось бы увеличивать уровень вложенности по мере увеличения количества входных списков.

Я недавно сделал небольшую библиотеку для решения этой проблемы (если вы считаете это проблемой). Следуя моему первому примеру, с библиотекой мы получаем

Array(1...5, 3...5, where: { n, _ in n % 2 == 0}) { $0 + $1 }

Обоснование (и больше о понимании списка в целом) объясняется в блоге.

Вот расширение типов массивов, которые объединяют filter а также map в один метод:

extension Array {

    func filterMap(_ closure: (Element) -> Element?) -> [Element] {

        var newArray: [Element] = []
        for item in self {
            if let result = closure(item) {
                newArray.append(result)
            }
        }
        return newArray
    }

}

Это похоже на mapза исключением того, что вы можете вернуть nil, чтобы указать, что вы не хотите, чтобы элемент добавлялся в новый массив. Например:

let items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let newItems = items.filterMap { item in
    if item < 5 {
        return item * 2
    }
    return nil
}

Это также можно было бы записать более кратко:

let newItems = items.filterMap { $0 < 5 ? $0 * 2 : nil }

В обоих этих примерах, если элемент меньше 5, то он умножается на два и добавляется в новый массив. Если замыкание возвращает nil, то элемент не добавляется. Следовательно,newIems является [2, 4, 6, 8].

Вот эквивалент Python:

items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
newItems = [n * 2 for n in items if n < 5]

Одним из способов будет:

var evens: Int[]()
for x in 0..<10 {
    if x%2 == 0 {evens += x} // or evens.append(x)
}
Другие вопросы по тегам