A Cancel kattintása egy nyomtatási feladat közben egy PDFium Component-megjelenítőben néha semmit sem tett: a ciklus tovább rendereli minden hátralévő oldalt és példányt, és a feladat mégis eljutott a nyomtatóhoz. Az ok a Free Pascal for ciklusa volt, amely egyszer rögzíti a felső határát a ciklus indulásakor, így a CopyCount vagy ToPage mögötti változó nullázása ciklus közben semmit nem változtatott a már futón. A for-ciklus-határ hiba az ötödik buktató, amely ugyanabból a PDFiumPas Delphi/FPC-kompatibilitási auditból származott, amely négy másik cross-compiler csapdát is előhozott, és e néggyel ellentétben ez teljesen egy megjelenítő nyomtatási rutinján belül él — egy tesztelőnek végig kellett tartania a Cancel-t egy hosszú feladat alatt, hogy ténylegesen észrevegye, a nyomtató soha nem állt meg
Miért folytatódik a nyomtatási feladat a Cancel kattintása után is?
A nyomtatási feladat azért folytatódott, mert a megszakítás-ellenőrzés csak azokat a változókat állította vissza, amelyek a ciklushatárokat táplálták, nem magukat a ciklusokat, amiket a Free Pascal már rögzített, amikor mindegyik ciklus elindult. A SpeedButtonPrintClick kezelő, a Nyomtatás gomb mögött a PDFViewer és MultiPageViewer demókban, minden feladatot három egymásba ágyazott ciklusból épít fel: egy külső ciklus a rendezett (collated) példányszettek fölött, egy középső ciklus az oldalak fölött, és egy belső ciklus ugyanazon oldal rendezetlen (uncollated) példányai fölött. A PrintDialog.Collate dönti el, melyik számláló, a CollateCopyCount vagy a CopyCount, tartja ténylegesen a kért példányszámot, míg a másik egyen marad. A megszakításnak mindhárom szintet egyszerre kell elérnie, és e kód első verziója azzal próbálta ezt megtenni, hogy magukat a határváltozókat állította vissza abban a pillanatban, amikor a Cancel igazra váltott
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
A szándék elég tisztán olvasható: ha a számlálók, amelyek meghatározzák, hány rendezés, oldal, és példány van hátra, mind nullára esnek, a cikluséknak ki kellene fogyniuk munkából, és magukat kellene lezárniuk. A Printer.EndDoc ezután feltétel nélkül futott a ciklus után, függetlenül attól, hogyan ért véget, így még egy feladat is, amelyről a felhasználó azt hitte, leállította, teljesen renderelt, minden oldallal, mielőtt a kattintást észrevették volna, mégis benyújtásra került a spoolerhez
Mit rögzít a Free Pascal, amikor egy for-ciklus indul
A Free Pascal pontosan egyszer értékeli ki egy for ciklus végértékét, abban a pillanatban, amikor a ciklus elkezdődik, és soha többé annak a ciklusnak az élettartama alatt. A for Page := FromPage to ToPage do egyszer olvassa be a ToPage-et, hogy kiszámítsa, hány iterációt kell futtatnia, és ezután a ciklusnak nincs további érdeke egy ToPage nevű változóban, csak abban az iterációszámban, amit már befogott. A ToPage := 0 beállítása a ciklustesten belülről egy olyan változót változtat meg, amit a futó ciklus már nem konzultál, ami pontosan az oka annak, hogy a Cancel igaz lehetett, miközben a nyomtató még több iterációnyi oldalt kapott, néha az összeset
Ez a minta teljesen ésszerű szokás, amelyet C-ből vagy C++-ból hozunk át, és a PDFium Component C++Builder-megjelenítő demója elég közelről tükrözi a Pascal-verziót ahhoz, hogy az összehasonlítás közvetlen legyen. Az ő for (Copy = 1; Copy <= CopyCount; Copy++)-je minden átfutáskor újra teszteli a Copy <= CopyCount-ot a CopyCount élő értéke ellen, így a számláló ott történő nullázása valóban véget vet a ciklusnak a következő ellenőrzésnél. A C++Builder demó ugyanazt a nullázd-a-számlálókat kódot hordozta, és az működött, ami pontosan az, ami biztonságosnak tüntette fel ugyanazt az ötletet az azonos tárolóban, közvetlenül mellette ülő Pascal buildben való újrahasznosításra
Miért nem ütközött a Delphi build ugyanabba a hibába?
A Delphi PDFViewer demó soha nem függött attól, hogy egy ciklus észrevegyen egy megváltozott határt, mert a megszakítási útvonala egy kivételen keresztül tekeredik le, nem egy összehasonlításon. Legbelső ciklusa meghívja az RTL Abort eljárását abban a pillanatban, amikor a Cancel igazra vált, ami egy csendes EAbort-ot dob, amely egyenesen mindhárom egymásba ágyazott for cikluson át terjed egy kezelőhöz, amely a teljes nyomtatási blokk köré csomagolva
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;
Egy kivételt nem érdekel, hány for ciklus választja el azt a pontot, ahol dobódik, attól a kezelőtől, amely elkapja, és pontosan ez az a tulajdonság, amire ennek a problémának szüksége van. Ez a robusztusság nem volt szándékos védekezés a fent leírt határ-rögzítő viselkedés ellen — a Delphi demó szerzője egyszerűen egy másik eszközhöz nyúlt. A kivétel-alapú kiút mégis érdemes megnevezni erősebb mintaként: túléli, ha egy negyedik ágyazási szintet adnak hozzá később, míg egy kézzel elhelyezett Break utasítások lánca minden alkalommal újra kell emlékezni és újra hozzáadni, amikor a ciklus-ágyazás megváltozik
A javítás: Break minden ágyazási szinten, egy PrintSucceeded jelzővel kapuzva
A javítás, amely a PDFiumPas v2.27.0-jában érkezett, megtartja a háromciklusos struktúrát a Lazarus- és C++Builder-megjelenítő demókban, de explicitté teszi a megszakítást minden szinten, és elválasztja a ciklus leállítását a feladat benyújtásától, egy jelzővel, amelyet csak azután olvasnak, hogy a ciklus teljesen befejeződött
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;
A PrintSucceeded szándékosan egyszer kerül kiszámításra, közvetlenül a hármasan ágyazott ciklus kilépése után, semmi másból, mint a not Cancel-ből. A ciklustesten belül semmi nem dönthet önmagában arról, hogy a feladat sikeres volt-e — a ciklus csak két módon érhet véget, kifogyva a rendezésekből, oldalakból, és példányokból, vagy elérve a Break láncot, amit a Cancel kivált, és a PrintSucceeded az eredményt utólag olvassa be, ahelyett hogy nyomon követné, ahogy a ciklus halad. Az ilyen módon való kiszámítás teszi megbízhatóvá a finally blokk választását a Printer.EndDoc és a Printer.Abort között: soha nem sül el, mielőtt a ciklus ténylegesen le nem zárult
A saját Cancel-vezérelt nyomtatási ciklusaid auditálása
Három ellenőrzés messze túlmutat ezen az egy nyomtatási rutinon. Soha ne feltételezd, hogy egy Pascal for ciklus észreveszi egy határváltozó megváltozását, miután elkezdődött; ha egy ciklusnak korán véget kell érnie, mondd ki közvetlenül Break-kel, minden ágyazási szinten, amelyet a megszakításnak át kell lépnie, nem csak a legbelsőnél. Részesítsd előnyben egy olyan kilépési mechanizmust, amely felépítésénél fogva letekeredik, mint egy kivétel, amint az ágyazás elég mély ahhoz, hogy egy elmulasztott Break valószínű legyen — a Delphi demó Abort/EAbort párja ezt ingyen kapta. Kapuzz minden véglegesítő lépést, mint az EndDoc, egy jelző mögé, amelyet szigorúan a ciklus után számítasz ki, soha nem azon belül, így egy korán leállt feladatot soha nem lehet összekeverni eggyel, amely befejeződött
Ugyanaz az áttekintés, amely ezt a ciklust javította, megszigorította azt is, ahogyan a kilenc megjelenítő-demó kezeli a billentyűzetet, miközben egy megszakítás gomb látható: most minden billentyűt elnyelnek, kivéve az Esc-et, így egy eltévedt Ctrl+P vagy Ctrl+F egy aktív nyomtatás vagy keresés közben többé nem tud egy másodikat elindítani a tetején. Ez az újrabelépési (reentrancy) javítás egy másik hiba, más mechanizmussal, de ugyanabból az áttekintésből származott arról, mit is csinál valójában a Cancel egy művelet közepén. A mindkét javításból elvitel érdemes szokás inkább egy tesztelési szokás, mint egy kódolási: gyakorold a megszakítást minden fordítón, amelyre egy megosztott nyomtatási rutin ténylegesen kiszállításra kerül, mert egy olyan ötlet, mint a számlálók törlése, és bízni abban, hogy a ciklus észreveszi, túlélheti a tesztelést C++Builder alatt, elérhet egy ellenőrizetlen Free Pascal buildet, azonosan olvasható a forrásban, és olyan módon bukhat el, amit senki nem lát, amíg valaki elég sokáig nem tartja a Cancel-t ahhoz, hogy lássa, a feladat mégis befejeződik. A nyomtatási beállításhoz, amelyre ez a megszakítási logika épül, a PDF-dokumentumok nyomtatásáról a PDFium VCL komponenssel szóló bemutató tárgyalja a többit
Ez a nyomtatás-megszakítási javítás a Delphihez, C++Builderhez, és FPC/Lazarushoz készült PDFium Komponens részeként érkezett, amely ugyanazt a demókészletet futtatja mind a három fordítón minden kiadásnál, így az ehhez hasonló rések egy build-mátrix által kerülnek elkapásra, nem egy támogatási jegy által