Защо и кога да използваме lazy с Array в Swift?

[1, 2, 3, -1, -2].filter({ $0 > 0 }).count // => 3

[1, 2, 3, -1, -2].lazy.filter({ $0 > 0 }).count // => 3

Какво е предимството на добавянето на lazy към второто изявление. Според моето разбиране, когато се използва променлива lazy, паметта се инициализира към тази променлива по времето, когато е използвана. Как има смисъл в този контекст?

въведете описание на изображението тук

Опитвайки се да разберем използването на LazySequence малко по-подробно. Бях използвал функциите map, reduce и filter за последователности, но никога за lazy последователност. Трябва да разберете защо да използвате това?


person Deep Arora    schedule 19.08.2018    source източник
comment
lazy инициализацията обикновено се използва за тежки елементи като UIKit елементи. Не съм виждал да се използва с масиви.   -  person Rakesha Shastri    schedule 19.08.2018
comment
Никога преди не съм виждал тази употреба на мързелив. Откъде научи за това?   -  person Duncan C    schedule 19.08.2018
comment
Подготвях предложение за SE - 0220, където попаднах на това. github.com/apple/swift-evolution/ blob/master/proposals/   -  person Deep Arora    schedule 19.08.2018
comment
Може би, ако масивите са големи и филтрирането отнема много време, не искате да филтрирате масива веднага?   -  person Rakesha Shastri    schedule 19.08.2018
comment
За разбираемо описание, моля, гледайте WWDC 2018: Ефективно използване на колекции от 15: 00   -  person vadian    schedule 25.06.2019


Отговори (2)


lazy променя начина, по който се обработва масивът. Когато lazy не се използва, filter обработва целия масив и съхранява резултатите в нов масив. Когато се използва lazy, стойностите в последователността или колекцията се произвеждат при поискване от функциите надолу по веригата. Стойностите не се съхраняват в масив; те просто се произвеждат, когато е необходимо.

Помислете за този модифициран пример, в който използвах reduce вместо count, за да можем да отпечатаме какво се случва:

Не използвам lazy:

В този случай всички елементи първо ще бъдат филтрирани, преди да се преброи нещо.

[1, 2, 3, -1, -2].filter({ print("filtered one"); return $0 > 0 })
    .reduce(0) { (total, elem) -> Int in print("counted one"); return total + 1 }
filtered one
filtered one
filtered one
filtered one
filtered one
counted one
counted one
counted one

Използване на lazy:

В този случай reduce иска елемент за преброяване и filter ще работи, докато намери такъв, след това reduce ще поиска друг и filter ще работи, докато намери друг.

[1, 2, 3, -1, -2].lazy.filter({ print("filtered one"); return $0 > 0 })
    .reduce(0) { (total, elem) -> Int in print("counted one"); return total + 1 }
filtered one
counted one
filtered one
counted one
filtered one
counted one
filtered one
filtered one

Кога да използвате lazy:

опция-щракването върху lazy дава това обяснение:

изскачащ прозорец за мързеливи в Xcode

От Дискусията за lazy:

Използвайте мързеливото свойство при верижни операции:

  1. за предотвратяване на междинните операции от разпределяне на хранилище

    or

  2. когато имате нужда само от част от крайната колекция, за да избегнете ненужни изчисления

    Бих добавил и трети:

  3. когато искате процесите надолу по веригата да започнат по-рано и да не се налага да чакате процесите нагоре по веригата да свършат цялата си работа първи

Така например бихте искали да използвате lazy преди filter, ако търсите първия положителен Int, защото търсенето ще спре веднага щом намерите такъв и това ще спести filter от необходимостта да филтрира целия масив и ще спестете необходимостта да отделяте място за филтрирания масив.

За 3-та точка си представете, че имате програма, която показва прости числа в диапазона 1...10_000_000, използвайки filter в този диапазон. Бихте предпочели да покажете простите числа, както сте ги намерили, отколкото да чакате да ги изчислите всички, преди да покажете нещо.

person vacawama    schedule 19.08.2018
comment
Това е хубав изчерпателен отговор. Един основен пример за време, което понякога мисля да използвам, е когато искам елемента „.first“ от масива. Потенциално може да спести много работа. - person bitwit; 19.08.2018
comment
Наистина подробна дискусия и стъпки за доказване за използването на lazy и механизма зад него. Гласуване за! - person Yongxiang Ruan; 27.07.2019
comment
тъй като Swift 5.2 работи само на iOS 13.4+ устройства, това поведение е леко променено, редът на Sequence функциите вече е обърнат, което означава, че функцията нагоре ще завърши първа, преди да изпълни функцията надолу по веригата, може да се намери прост пример тук. така че смятам, че третата точка в отговора ви вече не е валидна. - person JAHelia; 01.04.2020
comment
За да бъдем по-конкретни, промените 5.2 са тук: github.com/apple/swift/blob/master/ - person Michael Ozeryansky; 10.04.2020
comment
@JAHelia, това е интересна промяна и правилна корекция на грешки. Не знаех за тази грешка с множество филтри, така че всъщност не променя третата ми точка. Просто се опитвах да отбележа, че крайният потребител на филтрираните стойности може да започне със страничните ефекти по-рано, защото стойностите ще бъдат предадени, преди всички филтри нагоре да са приключили. Например, ако изпълнявате тези филтри на много голям масив във фонова нишка и след това отпечатвате резултатите на преден план, можете да започнете да актуализирате потребителския интерфейс веднага, вместо да се налага да чакате филтрите да завършат. - person vacawama; 10.04.2020
comment
Благодаря @MichaelOzeryansky. Това е полезна информация. - person vacawama; 10.04.2020
comment
Промяната не важи ли само за множество верижни филтри? Намаляването след филтриране трябва да остане непроменено imo. - person Frederik Winkelsdorf; 22.12.2020
comment
@FrederikWinkelsdorf, дори само един филтър, последван от намаляване, демонстрира разликата, както показва моят пример по-горе. Ключът е дали филтърът създава масив от филтрираните стойности преди редуцирането да се изпълнява, или с lazy предава всяка филтрирана стойност, за да редуцира незабавно, когато се генерират. - person vacawama; 22.12.2020
comment
@vacawama Благодаря за отговора! Абсолютно правилно, междинният масив е пропуснат, вероятно недоразумение: имах предвид въведената промяна в реда на последователността със Swift 5.2 (или на устройства ‹ iOS 13.4). Мисля, че няма за какво да се притеснявате, ако няколко филтъра не са свързани. Така че .lazy.filter.reduce трябва да бъде безопасен за използване във всяка известна среда. - person Frederik Winkelsdorf; 22.12.2020

Не бях виждал това преди, затова потърсих малко и го намерих.

Синтаксисът, който публикувате, създава мързелива колекция. Мързеливото събиране избягва създаването на цяла поредица от междинни масиви за всяка стъпка от вашия код. Не е толкова уместно, когато имате само оператор за филтър, ще има много по-голям ефект, ако направите нещо като filter.map.map.filter.map, тъй като без мързеливата колекция се създава нов масив на всяка стъпка.

Вижте тази статия за повече информация:

https://medium.com/developermind/lightning-read-1-lazy-collections-in-swift-fa997564c1a3

РЕДАКТИРАНЕ:

Направих някои сравнителни анализи и серия от функции от по-висок порядък като карти и филтри всъщност е малко по-бавна при мързелива колекция, отколкото при обикновена колекция.

Изглежда, че мързеливите колекции ви дават по-малък отпечатък от паметта на цената на малко по-бавна производителност.

Редактиране #2:

@discardableResult func timeTest() -> Double {
    let start = Date()
    let array = 1...1000000
    let random = array
        .map { (value) -> UInt32 in
            let random = arc4random_uniform(100)
            //print("Mapping", value, "to random val \(random)")
            return random
    }
    let result = random.lazy  //Remove the .lazy here to compare
        .filter {
            let result = $0 % 100 == 0
            //print("  Testing \($0) < 50", result)
            return result
        }
        .map { (val: UInt32) -> NSNumber in
            //print("    Mapping", val, "to NSNumber")
            return NSNumber(value: val)
        }
        .compactMap { (number) -> String? in
            //print("      Mapping", number, "to String")
            return formatter.string(from: number)
        }
        .sorted { (lhv, rhv) -> Bool in
            //print("        Sorting strings")
            return (lhv.compare(rhv, options: .numeric) == .orderedAscending)
    }
    
    let elapsed = Date().timeIntervalSince(start)
    
    print("Completed in", String(format: "%0.3f", elapsed), "seconds. count = \(result.count)")
    return elapsed
}

В кода по-горе, ако промените реда

let result = random.lazy  //Remove the .lazy here to compare

to

let result = random  //Removes the .lazy here

След това работи по-бързо. С lazy, моят бенчмарк показва, че отнема около 1,5 пъти повече време с .lazy колекцията в сравнение с прав масив.

person Duncan C    schedule 19.08.2018
comment
Не винаги по-бавно, ако не е необходимо да обработвате цялата колекция. (1...1000000).lazy.filter{ $0 % 2 == 0}.first е много по-бърз с lazy отколкото без. - person vacawama; 20.08.2018
comment
Всъщност, ако имате няколко стъпки във филтрирания масив, lazy е по-бавен. Вижте редакцията на моя отговор. Все пак разбирам какво казвате за работа само с малка част от филтрирания резултат. - person Duncan C; 20.08.2018