Маленькие хитрости

  • Автор темы Yakov
  • Дата начала
Y

Yakov

Гость
#1
Перебор коллекции документов - типичная задача.
У меня обычно это выглядит примерно так:
Код:
Dim collection As NotesDocumentCollection
Dim document As NotesDocument

Set collection = database.Search(...)

If collection Is Nothing Then Exit Sub 'на всякий случай
If collection.Count = 0 Then Exit Sub

Set document = collection.GetFirstDocument()
While Not document Is Nothing
Call processDocument(document)
Set document = collection.GetNextDocument(document)
Wend
Очень много кода, не правда ли? Вот бы сократить его до такого:
Код:
Dim iterator As CollectionIterator

Set iterator = New CollectionIterator(database.Search(...))

While iterator.hasNext()
Call processDocument(iterator.next())
Wend
Сказано - сделано!
Код:
Public Class CollectionIterator

Private collection As Variant
Private cursor As Variant

Public Sub new(collection As Variant)
Set Me.collection = Nothing
Set cursor = Nothing
If collection Is Nothing Then Exit Sub
If collection.count = 0 Then Exit Sub
Set Me.collection = collection
Set cursor = collection.getFirstDocument()
End Sub

Public Function hasNext() As Boolean
hasNext = Not cursor Is Nothing
End Function

Public Function next() As Variant
Dim tmp As Variant
If Not hasNext() Then
Error 188, "Collection has no more elements."	'ErrIllegalUseOfFunction
End If
Set tmp = cursor
Set cursor = collection.getNextDocument(cursor)
Set Me.next = tmp
End Function

End Class
 

ToxaRat

Чёрный маг
Lotus team
06.11.2007
3 226
25
#3
:)
от жеж сократил....
со стороны кодинда да, неоспоримо сокращения на лицо особенно если заюзать раз 20 то может код даже в пару раз меньше будет
однако со стороны откомпиленного кода мы получаем увеличение испольняемого кода в разы да еще и его замедление из-за постоянного обращения к стеку и прыганься по функциям...

кстате вместо
Код:
While Not document Is Nothing
предпочитаю
Код:
do until document Is Nothing
 

Omh

Well-Known Member
Lotus team
04.07.2007
2 210
1
#6
А ещё можно выйти с помощью
Код:
Exit Do
если приспичит...
 
13.03.2009
625
2
#7
;)
от жеж сократил....
со стороны кодинда да, неоспоримо сокращения на лицо особенно если заюзать раз 20 то может код даже в пару раз меньше будет
однако со стороны откомпиленного кода мы получаем увеличение испольняемого кода в разы да еще и его замедление из-за постоянного обращения к стеку и прыганься по функциям...
Ирония напрасна. Короче код - меньше ошибок. И читается он легче.
"Написать код, понятный компьютеру, может каждый, но только хорошие программисты пишут код, понятный людям." - М. Фаулер.
А замечания о производительности так просто чушь. Обход коллекции - операция затратная и лишнее обращение к стеку вызовов - чих: Вводная: сервер в локалке, коллекция 218 документов.
Вариант №1 - традиционный обход( while ... wend ). Вариант №2 - итератор от Yakov
запуск №1
Вариант 1: 0.69с
Вариант 2: 0.72с
запуск №2
Вариант 1: 0.78с
Вариант 2: 0,66с
to Yakov: вот только тип метода next() таки стоит поменять на NotesDocument и предоставить компилятору проверять типы.
Декларация Function processDoc( i_doc As NotesDocument ) ругаеца при включенном option declare - и прально делает.
 
Y

Yakov

Гость
#8
turumbay, спасибо за тест.

На самом деле, CollectionIterator имеет (абстрактный) суперкласс Iterator:
Код:
Public Class Iterator
Public Function hasNext() As Boolean
Public Function next() As Variant
End Class
Кроме CollectionIterator есть еще ObjectListIterator, который перебирает элементы самописного класса ObjectList, являющегося контейнером любых объектов.
 
13.03.2009
625
2
#9
На самом деле, CollectionIterator имеет (абстрактный) суперкласс Iterator:
Кроме CollectionIterator есть еще ObjectListIterator, который перебирает элементы самописного класса ObjectList, являющегося контейнером любых объектов.
Я так и подумал...
Обидно, что нет возможности изменить сигнатуру метода в подклассе. В таких случаях пишем враппер для возвращаемого объекта - слегка накладно, но получаем все преимущества строгой типизации.
Код:
Public Class TNext
End Class
Public Class TDocObject As TNext
Public getDocument As NotesDocument
End Class
Public Class TIterator
Public Function next() As TNext
End Function
End Class
Public Class TCollectionIterator
Public Function next() As TDocObject
End Function
End Class
Variant - источник трудноуловимых ошибок, особенно в сочетании с иерархией классов.
 
Y

Yakov

Гость
#11
turumbay
Тут уж лучше хитрый каст использовать:
Код:
Public Class Cast
Public Function toNotesDocument(value As Variant) As NotesDocument
Set toNotesDocument = value
End Function
End Class
Dim cast As New Cast()

...
Call processDocument(cast.toNotesDocument(iterator.next()))
fedotxxl
Завалится, наверное. Как только встретится такая задача, подумаю. ;)
 
13.03.2009
625
2
#12
turumbay
Тут уж лучше хитрый каст использовать
Не лучше. Никто не запретит передать в cast.toNotesDocument() все что угодно, не являющееся LN документом. И узнаем мы об этом только в момент выполнения. Все что можно поймать на этапе компиляции - должно там и ловиться.

P.S. В этой логике и каст не нужен. Можно напрямую присвоить
Код:
Dim doc As NotesDocument 
Set doc = iterator.next()
Call processDocument(doc)
Суть не меняется - просто переместили потенциальную ошибку в другое место. Метод возвращает вариант и при вызове метода невозможно убедиться что переменная слева от присваивания имеет требуемый тип.
 
13.03.2009
625
2
#14
А возвращаясь к сабжу про хитрости - у меня для обхода коллекции есть smartbutton в дизайнере:
Код:
LB:= @Char(33);
@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t.vbs & @echo W.AppActivate \"Designer\" >> t.vbs & @echo W.SendKeys \""+ "Dim session As New NotesSession{ENTER}"+ "\" >> t.vbs & @echo W.SendKeys \""+ "Dim db As NotesDatabase{ENTER}"+ "\" >> t.vbs ");

@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t1.vbs & @echo W.AppActivate \"Designer\" >> t1.vbs & @echo W.SendKeys \""+
"Dim collection As NotesDocumentCollection{ENTER}"+ "\" >> t1.vbs ");

@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t2.vbs & @echo W.AppActivate \"Designer\" >> t2.vbs & @echo W.SendKeys \""+
"Dim doc As NotesDocument{ENTER}"+ "\" >> t2.vbs ");

@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t3.vbs & @echo W.AppActivate \"Designer\" >> t3.vbs & @echo W.SendKeys \""+
"Set db = session.CurrentDatabase{ENTER}"+ "\" >> t3.vbs ");

@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t4.vbs & @echo W.AppActivate \"Designer\" >> t4.vbs & @echo W.SendKeys \""+
"Set collection = db.UnProcessedDocuments{ENTER}"+ "\" >> t4.vbs ");

@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t5.vbs & @echo W.AppActivate \"Designer\" >> t5.vbs & @echo W.SendKeys \""+
"Set doc = collection.getFirstDocument{ENTER}"+ "\" >> t5.vbs ");

@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t6.vbs & @echo W.AppActivate \"Designer\" >> t6.vbs & @echo W.SendKeys \""+
"while not doc is nothing{ENTER}"+ "\" >> t6.vbs ");

@Command([Execute]; "cmd"; " /c @echo set W = WScript.CreateObject(\"WScript.Shell\") > t7.vbs & @echo W.AppActivate \"Designer\" >> t7.vbs & @echo W.SendKeys \""+
"Set doc = collection.getNextDocument{(}doc{)}{UP}{ENTER}"+ "\" >> t7.vbs ");


@Command([Execute]; "cmd"; " /c cscript t.vbs & cscript t1.vbs & cscript t2.vbs & cscript t3.vbs & cscript t4.vbs & cscript t5.vbs & cscript t6.vbs & cscript t7.vbs" )
В результате нажатия имею код:
Код:
	Dim session As New NotesSession
Dim db As NotesDatabase
Dim collection As NotesDocumentCollection
Dim doc As NotesDocument
Set db = session.CurrentDatabase
Set collection = db.UnProcessedDocuments
Set doc = collection.getFirstDocument
While Not doc Is Nothing

Set doc = collection.getNextDocument(doc)
Wend
с курсором внутри цикла.
Лень, как говорица, двигатель прогресса. Таких кнопок имею несколько штук, для наиболее часто используемых конструкций( в т.ч. пресловутый errHandler )
 

ToxaRat

Чёрный маг
Lotus team
06.11.2007
3 226
25
#15
Вводная: сервер в локалке, коллекция 218 документов
;)
ужас, это даже не тест а насмешка, мне такая ситуация наверно в жизни выпала лишь пару раз, в основном всё на сервере и на милионах доках...
 
Y

Yakov

Гость
#16
ToxaRat
Ну тогда мой тест.
Машина Athlon64 3200+, 2Gb RAM, WinXP. На машине сервер 8.0.2, клиент 8.0.2. Других задач нет.
В базе 12500 документов.
Тест 1. Простой перебор коллекции.
"обычный" способ - 0,141 сек.
итератор - 0,219 сек.
Разница - 0,078 сек.
Тест 2. Перебор с доступом к полю "Form" документа.
"обычный" способ - 24,891 сек.
итератор - 25,281 сек.
Разница - 0,390 сек. (~1,5%) или 3,12 * 10^-5 сек./документ.
Принимая, что "тормоза" будут расти линейно с ростом коллекции, получим, что на миллионе документов будет разница в 31,5 секунду. А весь цикл займет чуть более 2000 секунд. (На самом деле, много дольше. Вся база в моем случае была в кэше сервера, к диску обращений не было.)
 
13.03.2009
625
2
#17
ужас, это даже не тест а насмешка, мне такая ситуация наверно в жизни выпала лишь пару раз, в основном всё на сервере и на милионах доках...
Со всей ответственностью заявляю: выборка репрезентативна, результаты теста подтверждают, что ухудшения производительности не наблюдается.
Чем вам сервер в локалке не угодил? Считаете, что результаты теста будут отличаться, если до сервера будет дайлапный канал?
Проверить на мульене документов предлагаю самостоятельно.
Вообще, наблюдаю склонность к необоснованным заявлениям относительно производительности. Сначала - про десятикратное ускорение в случае getItemValue vs расширенный синтаксис. Теперь про торомоза при лишнем обращении к стеку. Все высосано из пальца и не подкреплено цифрами.
З.Ы. Ничего личного.

Разница - 0,390 сек. (~1,5%) или 3,12 * 10^-5 сек./документ.
Недостаточно данных для вывода. Один и тот же код на коллекции 12500 документов будет выполняться с заметным разбросом. Второй тест предлагаю прогнать хотя бы пару раз. Будет видно, что разница обусловлена не использованием итератора, а временем доступа по сети.
 

TIA

:-)
Lotus team
15.05.2009
790
2
#18
Есть у такого итератора неприятные отличия в поведении, которые, на мой вкус, перевешивают прелесть небольшого уменьшения строк. Плохо то, что передав его в глубь кода, нельзя быть уверенным в неизменности его состояния, в отличие от NotesCollection. В С++ это бы решалось передачей копии объекта итератора. Поясню на примере

Код:
sub doActionA(iterator As CollectionIterator)
Do while iterator.hasNext() 
If IsSpecialDoc(iterator.getNext) then MakeSignal : Exit Do
Loop
end sub 

sub doActionB( iterator As CollectionIterator )
Do while iterator.hasNext()
'!!! обработает не всю коллекцию
...
Loop
end sub


sub Initialize
Dim iterator As CollectionIterator

Set iterator = New CollectionIterator(database.Search(...))

call doActionA( iterator ) 'меняет состояние итератора
call doActionB( iterator ) 'здесь уже новое состояние
End Sub
В данном конкретном случае помог бы метод позиционирования в начало коллекции, применяемый перед каждым использованием. Но дело не в этом. А в том, что doActionA имеет побочный эффект в виде изменения позиции в итераторе. Это также неприятно с точки зрения внесения изменений. Предположим, doActionA не менял состояния итератора и тогда doActionB проходил всю коллекцию. Но стоит изменить doActionA так, как в примере, doActionB перестаёт работать. И чтобы понять, можем ли мы выполнить проход по коллекции на уровне doActionA, нужно проанализировать тонну кода, который потенциально мог пользовать итератор после всех ста тысяч точек вызова doActionA.

NotesCollection требует для получения очередного документа, параметром текущий документ. Так вот этот текущий документ у каждой точки обращения к коллекции свой. И потому код более устойчив к модификациям.
 
Y

Yakov

Гость
#19
TIA
Это итератор в стиле java.util.Iterator.
Зная особенности реализации итератора, ваш код можно починить так:
Код:
Dim collection As NotesDocumentCollection
Dim iterator As CollectionIterator
Set collection = database.Search(...)

Set iterator = New CollectionIterator(collection)
call doActionA( iterator )

Set iterator = New CollectionIterator(collection)
call doActionB( iterator )
Или так:
Код:
Sub doActionA(collection As NotesDocumentCollection)
Dim iterator As New CollectionIterator(collection)
Do while iterator.hasNext()
If IsSpecialDoc(iterator.getNext) Then MakeSignal : Exit Do
Loop
End Sub

Sub doActionB(collection As NotesDocumentCollection)
Dim iterator As New CollectionIterator(collection)
Do While iterator.hasNext()
...
Loop
End Sub
Причем второй вариант выглядит более естественно.
 

ToxaRat

Чёрный маг
Lotus team
06.11.2007
3 226
25
#20
Принимая, что "тормоза" будут расти линейно с ростом коллекции, получим, что на миллионе документов будет разница в 31,5 секунду. А весь цикл займет чуть более 2000 секунд. (На самом деле, много дольше. Вся база в моем случае была в кэше сервера, к диску обращений не было.)
тормоза в полминуты для меня уже критично, если учитывать что база может и не быть вся в кеше и обращения на диск начнутся это это может увеличиться в несколько раз и как результат можно вылететь за дефоултовые 10 минут для отработки агента
А ведь поймите меня правильно, мне же не только по колекции гулять нужно а еще что-то где-то смотреть/менять и возможно использовать еще какие-то колекции..

Классический пример:
канцелярист строит годовой отчет по входящим документам - милион(учитываем всё сами документы, резолюции к ним, поручения и прочее)
внутри кода естественно имеем колекцию по которой нужно пробежать от начала и до конца
плюс имеет то, что отчет скажем нужно выплюнуть в ексель
выполнение отчета обычно на милионе документов выполнится за 10 минут и это при условии:
- отчет с сортировками/категориями
- пробегаем по колекции только раз
- имеет числовые поля типа контроль исполнения и т.д.
- что-то сумируем, вычитаем, вычеркиваем, добавляем....

чтобы выполнить этот отчет как можно быстрее пришлось всё разложить по полочкам
даже такая мелочь как чтение документа и тут же запись в ексель уже не допустима по причине того, что быстрее оказалось собрать все нужные данные в класс, обработать его, а потом сразу всё выплевывать в ексель не переключаясь ни на что

в такой ситуации сэкономить 3 строчки кода и получив как результат тормоз в ХХ минут не допустим...