Klik na Prekliči med tiskanjem v pregledovalniku PDFium Component včasih ni naredil ničesar: zanka je še naprej izrisovala vse preostale strani in kopije, opravilo pa je vseeno prišlo do tiskalnika. Vzrok je bila zanka for v Free Pascalu, ki zgornjo mejo določi enkrat ob vstopu v zanko, zato nastavitev spremenljivke za CopyCount ali ToPage na nič sredi zanke ni vplivala na že tekoče izvajanje. Napaka z mejami zanke for je peta past iz iste revizije združljivosti PDFiumPas za Delphi/FPC, ki je odkrila štiri druge pasti med prevajalniki, vendar je v nasprotju z njimi v celoti skrita v rutini za tiskanje pregledovalnika — opaziti jo je bilo mogoče šele, ko je preizkuševalec med dolgim opravilom držal Prekliči in videl, da se tiskalnik nikoli ni ustavil
Zakaj se tiskalno opravilo nadaljuje po kliku na Prekliči?
Tiskalno opravilo se je nadaljevalo, ker je preverjanje preklica ponastavilo samo spremenljivke, ki so določale meje zank, ne pa samih zank, saj jih je Free Pascal ob vsakem začetku že zaklenil. Obdelovalnik SpeedButtonPrintClick za gumb Print v predstavitvah PDFViewer in MultiPageViewer sestavi vsako opravilo iz treh ugnezdenih zank: zunanje zanke čez združene sklope kopij, srednje zanke čez strani in notranje zanke čez neposamezno združene kopije iste strani. PrintDialog.Collate določi, kateri števec, CollateCopyCount ali CopyCount, dejansko vsebuje zahtevano število kopij, drugi pa ostane enak ena. Preklic mora doseči vse tri ravni hkrati, prva različica te kode pa je to poskušala doseči tako, da je ob nastavitvi Cancel na true ponastavila kar spremenljivke mej
for CollateCopy:= 1 to CollateCopyCount do
for Page:= FromPage to ToPage do
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
begin
CollateCopyCount:= 0;
ToPage:= 0;
CopyCount:= 0;
end;
end;
Printer.EndDoc; // runs whether or not Cancel fired
Namen je na prvi pogled dovolj jasen: če se števci, ki določajo preostalo število združevanj, strani in kopij, znižajo na nič, naj bi zankam zmanjkalo dela in bi se same končale. Printer.EndDoc se je nato brezpogojno izvedel za zanko, ne glede na način konca, zato je bilo tudi opravilo, za katero je uporabnik menil, da ga je ustavil, poslano v tiskalno vrsto, skupaj z vsemi stranmi, izrisanimi do trenutka, ko je bil klik opažen
Kaj Free Pascal zaklene ob začetku zanke for
Free Pascal končno vrednost zanke for izračuna natanko enkrat, v trenutku začetka zanke, nato pa je v času njenega trajanja nikoli več ne preveri. for Page := FromPage to ToPage do enkrat prebere ToPage, da izračuna število ponovitev, potem pa zanke ne zanima več spremenljivka z imenom ToPage, temveč samo že zajeto število ponovitev. Nastavitev ToPage := 0 znotraj telesa zanke spremeni spremenljivko, ki je tekoča zanka ne preverja več, zato je bil Cancel lahko true, tiskalnik pa je še naprej prejemal strani več ponovitev, včasih kar vse
Tak vzorec je povsem razumljiva navada, prenesena iz C ali C++, predstavitev pregledovalnika PDFium Component za C++Builder pa je dovolj podobna Pascalovi, da je primerjava neposredna. Njen for (Copy = 1; Copy <= CopyCount; Copy++) pri vsakem prehodu znova preveri Copy <= CopyCount glede na trenutno vrednost CopyCount, zato nastavitev števca na nič tam res konča zanko pri naslednjem preverjanju. Predstavitev C++Builder je vsebovala isto kodo za ponastavitev števcev in je delovala, zaradi česar se je ista zamisel zdela varna za ponovno uporabo v Pascalovi gradnji tik ob njej v istem repozitoriju
Zakaj enaka napaka ni prizadela gradnje za Delphi?
Predstavitev PDFViewer za Delphi ni nikoli temeljila na tem, da bi zanka opazila spremenjeno mejo, saj pot preklica izstopi z izjemo namesto s primerjavo. Njena notranja zanka ob spremembi Cancel na true pokliče proceduro Abort iz RTL, ki sproži tiho izjemo EAbort in se po vseh treh ugnezdenih zankah for razširi do obdelovalnika okoli celotnega bloka za tiskanje
Printer.BeginDoc;
try
for CollateCopy:= 1 to CollateCopyCount do
for Page:= FromPage to ToPage do
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
Abort; // raises EAbort, unwinds all three loops at once
end;
Printer.EndDoc;
except
on E: EAbort do
Printer.Abort;
else
begin
Printer.Abort;
raise;
end;
end;
Izjema ne skrbi za to, koliko zank for loči mesto sprožitve od obdelovalnika, ki jo ujame, kar je natanko lastnost, ki jo ta težava potrebuje. Ta robustnost ni bila namerna obramba pred zgoraj opisanim vedenjem zaklenjene meje — avtor predstavitve za Delphi je preprosto posegel po drugem orodju. Izstop na podlagi izjeme je vseeno vredno izpostaviti kot trdnejši vzorec: preživi tudi poznejšo dodano četrto raven gnezdenja, medtem ko si je treba verigo ročno postavljenih stavkov Break zapomniti in jo znova dodati ob vsaki spremembi gnezdenja zank
Popravek: Break na vsaki ravni gnezdenja, pod nadzorom zastavice PrintSucceeded
Popravek, izdan v PDFiumPas v2.27.0, ohranja strukturo treh zank v predstavitvah pregledovalnika za Lazarus in C++Builder, vendar preklic izrecno obravnava na vsaki ravni in loči ustavljanje zank od oddaje opravila z zastavico, ki se prebere šele po popolnem koncu zank
Printer.BeginDoc;
try
Cancel:= False;
for CollateCopy:= 1 to CollateCopyCount do
begin
for Page:= FromPage to ToPage do
begin
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
Break;
end;
if Cancel then
Break;
end;
if Cancel then
Break;
end;
PrintSucceeded:= not Cancel;
finally
if PrintSucceeded then
Printer.EndDoc
else
Printer.Abort;
end;
PrintSucceeded se namerno izračuna enkrat, takoj po izhodu iz trojno ugnezdenih zank, samo iz izraza not Cancel. Nič v telesu zanke ne odloča samostojno, ali je opravilo uspelo — zanka se lahko konča samo tako, da ji zmanjka združevanj, strani in kopij, ali pa doseže verigo Break, ki jo sproži Cancel, PrintSucceeded pa izid prebere naknadno, namesto da bi ga spremljala med izvajanjem zanke. Prav tak izračun zagotovi, da je izbira med Printer.EndDoc in Printer.Abort v bloku finally zanesljiva, saj se ne izvede, preden se zanka dejansko ne umiri
Pregled lastnih zank za tiskanje, ki jih lahko prekličete
Tri preverjanja so uporabna tudi zunaj te ene rutine za tiskanje. Nikoli ne predpostavite, da bo Pascalova zanka for opazila spremembo spremenljivke meje po začetku izvajanja; če se mora zanka končati predčasno, to povejte neposredno z Break na vsaki ravni gnezdenja, čez katero mora priti preklic, ne samo v notranji zanki. Ko je gnezdenje dovolj globoko, da je izpuščen Break verjeten, raje uporabite mehanizem, ki po zasnovi razplete izvajanje, na primer izjemo — predstavitev za Delphi je to brezplačno dobila s parom Abort/EAbort. Vsak korak oddaje, kot je EndDoc, naj bo pod zastavico, izračunano strogo po koncu zanke, nikoli znotraj nje, da opravila, ki se je predčasno ustavilo, ni mogoče zamenjati za dokončanega
Ista revizija, ki je odpravila to zanko, je zaostrila tudi obravnavo tipkovnice v devetih predstavitvah pregledovalnika, ko je viden gumb za preklic: zdaj pogoltnejo vsako tipko razen Esc, zato naključni Ctrl+P ali Ctrl+F med aktivnim tiskanjem ali iskanjem ne more več zagnati drugega opravila. Ta popravek ponovne vstopnosti je drugačna napaka z drugačnim mehanizmom, vendar je nastal iz istega pregleda tega, kaj Cancel dejansko naredi sredi operacije. Najpomembnejša navada, ki jo je vredno odnesti iz obeh popravkov, je bolj preizkusna kot programska: preverite preklic v vsakem prevajalniku, za katerega se skupna rutina za tiskanje dejansko izda, saj lahko zamisel o brisanju števcev in zaupanju, da bo zanka to opazila, prestane preizkus pod C++Builder, pride v gradnjo za Free Pascal brez preverjanja, je v izvorni kodi videti enaka in odpove tako, da tega nihče ne opazi, dokler nekdo dovolj dolgo ne drži Prekliči in opazuje, kako se opravilo vseeno konča. Za nastavitev tiskanja, na kateri temelji ta logika preklica, preostanek pojasnjuje vodnik za tiskanje dokumentov PDF s komponento PDFium VCL
Ta popravek preklica tiskanja je bil izdan kot del komponente PDFium Component za Delphi, C++Builder in FPC/Lazarus, ki ob vsaki izdaji zažene isti nabor predstavitev v vseh treh prevajalnikih, da takšne vrzeli odkrije matrika gradenj in ne zahtevek za podporo