Peruuta-painikkeen klikkaaminen kesken tulostustyön PDFium-komponentin katseluohjelmassa ei joskus tehnyt mitään: silmukka jatkoi jokaisen jäljellä olevan sivun ja kopion renderöintiä, ja työ silti saavutti tulostimen. Syy oli Free Pascalin for-silmukka, joka kiinnittää ylärajansa kerran silmukan sisääntulossa, joten CopyCount- tai ToPage-muuttujan takana olevan muuttujan nollaaminen kesken silmukan ei muuttanut mitään jo käynnissä olevaa. For-silmukan rajabugi on viides sudenkuoppa, joka on tullut esiin samasta PDFiumPas Delphi/FPC-yhteensopivuustarkastuksesta, joka tuotti neljä muuta kääntäjien välistä sudenkuoppaa, ja toisin kuin nuo neljä, se asuu kokonaan katseluohjelman tulostusrutiinin sisällä — vaadittiin testaajaa, joka piti Peruuta-painiketta painettuna pitkän työn läpi, todella huomatakseen, ettei tulostin koskaan pysähtynyt
Miksi tulostustyö jatkuu Peruuta-klikkauksen jälkeen?
Tulostustyö jatkui, koska peruutuksen tarkistus vain nollasi muuttujat, jotka syöttivät silmukan rajoja, ei itse silmukoita, jotka Free Pascal oli jo lukinnut, kun kukin silmukka käynnistyi. SpeedButtonPrintClick-käsittelijä Print-painikkeen takana PDFViewer- ja MultiPageViewer-demoissa rakentaa jokaisen työn kolmesta sisäkkäisestä silmukasta: ulompi silmukka koostettujen kopiosarjojen yli, keskimmäinen silmukka sivujen yli, ja sisin silmukka saman sivun koostamattomien kopioiden yli. PrintDialog.Collate päättää, kumpi laskuri, CollateCopyCount vai CopyCount, todella kantaa pyydettyä kopiomäärää, kun taas toinen pysyy yhdessä. Peruutuksen on ulotuttava kaikkiin kolmeen tasoon kerralla, ja tämän koodin ensimmäinen versio yritti tehdä sen nollaamalla itse rajamuuttujat heti, kun Cancel muuttui todeksi
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
Aikomus lukee riittävän selvästi: jos laskurit, jotka määrittävät, kuinka monta koostetta, sivua ja kopiota on jäljellä, kaikki putoavat nollaan, silmukoiden pitäisi loppua työstä ja pudota itsestään läpi. Printer.EndDoc ajettiin sitten ehdoitta silmukan jälkeen riippumatta siitä, miten se päättyi, joten jopa työ, jonka käyttäjä uskoi pysäyttäneensä, silti lähetettiin taustatulostukseen jokaisen sivun renderöitynä ennen kuin klikkaus huomattiin
Mitä Free Pascal lukitsee, kun for-silmukka käynnistyy
Free Pascal arvioi for-silmukan loppuarvon täsmälleen kerran, sillä hetkellä kun silmukka alkaa, eikä koskaan enää sen silmukan elinajan aikana. for Page := FromPage to ToPage do lukee ToPage-arvon yhden kerran laskeakseen, kuinka monta iteraatiota ajaa, ja sen jälkeen silmukalla ei ole enää mitään kiinnostusta muuttujaan nimeltä ToPage, vain iteraatiomäärään, jonka se on jo napannut. ToPage := 0:n asettaminen silmukan rungon sisältä muuttaa muuttujaa, jota käynnissä oleva silmukka ei enää konsultoi, mikä on juuri se syy, miksi Cancel saattoi olla tosi, samalla kun tulostin jatkoi sivujen vastaanottamista vielä useamman iteraation ajan, joskus kaikkien niiden
Tuo malli on täysin järkevä tapa tuoda mukanaan C:stä tai C++:sta, ja PDFium-komponentin C++Builder-katseluohjelmademo peilaa Pascal-versiota tarpeeksi läheisesti tehdäkseen vertailusta suoran. Sen for (Copy = 1; Copy <= CopyCount; Copy++) testaa uudelleen Copy <= CopyCount-ehdon CopyCount:n elävää arvoa vasten jokaisella kierroksella, joten laskurin nollaaminen siellä todella päättää silmukan seuraavalla tarkistuksella. C++Builder-demo kantoi identtisen nollaa-laskurit-koodin, ja se toimi, mikä on juuri se, mikä sai saman idean näyttämään turvalliselta uudelleenkäytettäväksi Pascal-käännöksessä, joka istui aivan sen vieressä samassa repositoriossa
Miksei Delphi-käännös osunut samaan bugiin?
Delphi PDFViewer -demo ei koskaan riippunut silmukasta, joka huomaisi muuttuneen rajan, koska sen peruutuspolku purkautuu poikkeuksen kautta vertailun sijaan. Sen sisin silmukka kutsuu RTL:n Abort-proseduuria heti, kun Cancel muuttuu todeksi, mikä nostaa hiljaisen EAbort:n, joka etenee suoraan kaikkien kolmen sisäkkäisen for-silmukan läpi käsittelijään, joka on käärittynä koko tulostuslohkon ympärille
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;
Poikkeus ei välitä siitä, kuinka moni for-silmukka erottaa pisteen, jossa se nostetaan, käsittelijästä, joka nappaa sen, mikä on juuri se ominaisuus, jota tämä ongelma tarvitsee. Tuo vankkuus ei ollut tarkoituksellinen puolustus yllä kuvattua rajanlukituskäytöstä vastaan — Delphi-demon kirjoittaja vain turvautui eri työkaluun. Poikkeuspohjainen ulospääsy kannattaa silti nimetä vankempana mallina: se selviää neljännen sisäkkäisyystason lisäämisestä myöhemmin, kun taas ketju käsin sijoitettuja Break-lauseita on muistettava ja lisättävä uudelleen joka kerta, kun silmukan sisäkkäisyys muuttuu
Korjaus: Break jokaisella sisäkkäisyystasolla, portitettuna PrintSucceeded-lipulla
Korjaus, joka julkaistiin PDFiumPas v2.27.0:ssa, pitää kolmen silmukan rakenteen Lazarus- ja C++Builder-katseluohjelmademoissa, mutta tekee peruutuksesta nimenomaisen jokaisella tasolla, ja erottaa silmukan pysäyttämisen työn lähettämisestä lipuksi, joka luetaan vasta sen jälkeen, kun silmukka on kokonaan päättynyt
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 lasketaan tarkoituksella kerran, välittömästi kolmoissisäkkäisen silmukan poistumisen jälkeen, ei mistään muusta kuin not Cancel:sta. Mikään silmukan rungon sisällä ei saa päättää itsenäisesti, onnistuiko työ — silmukka voi päättyä vain kahdella tavalla, loppumalla koosteista, sivuista ja kopioista, tai osumalla Break-ketjuun, jonka Cancel laukaisee, ja PrintSucceeded lukee lopputuloksen jälkikäteen sen sijaan, että seuraisi sitä silmukan edetessä. Sen laskeminen tällä tavalla on se, mikä tekee finally-lohkon valinnasta Printer.EndDoc:n ja Printer.Abort:n välillä luotettavan: se ei koskaan laukea ennen kuin silmukka on todella asettunut
Omien peruutusohjattujen tulostussilmukoiden tarkastaminen
Kolme tarkistusta kulkevat paljon tätä yhtä tulostusrutiinia pidemmälle. Älä koskaan oleta, että Pascal-for-silmukka huomaa rajamuuttujan muuttuvan sen jälkeen, kun se on käynnistynyt; jos silmukan on päätyttävä aikaisin, sano se suoraan Break-lauseella, jokaisella sisäkkäisyystasolla, jonka peruutuksen on ylitettävä, ei vain sisimmällä. Suosi ulospääsymekanismia, joka purkautuu rakenteellisesti, kuten poikkeusta, heti kun sisäkkäisyys on tarpeeksi syvä, että puuttuva Break on uskottava — Delphi-demon Abort/EAbort-pari sai tämän ilmaiseksi. Portita mikä tahansa sitomisvaihe kuten EndDoc lipun taakse, joka lasketaan tiukasti silmukan jälkeen, ei koskaan sen sisällä, jotta työtä, joka pysähtyi aikaisin, ei koskaan voida sekoittaa sellaiseen, joka valmistui
Sama tarkastus, joka korjasi tämän silmukan, tiukensi myös, miten yhdeksän katseluohjelmademoa käsittelevät näppäimistöä, kun peruutuspainike on näkyvissä: ne nyt nielevät jokaisen näppäimen paitsi Esc:n, joten eksynyt Ctrl+P tai Ctrl+F aktiivisen tulostuksen tai haun aikana ei voi enää käynnistää toista sen päälle. Tämä uudelleenajautuvuuskorjaus on eri bugi eri mekanismilla, mutta se tuli samasta tarkastuksesta siitä, mitä Cancel todella tekee kesken toiminnon. Tapa, joka kannattaa viedä mukaan molemmista korjauksista, on enemmän testaustapa kuin koodaustapa: harjoita peruutusta jokaisella kääntäjällä, jolle jaettu tulostusrutiini todella toimitetaan, koska idea kuten laskureiden tyhjentäminen ja luottaminen siihen, että silmukka huomaa sen, voi selvitä testauksesta C++Builderin alla, saavuttaa Free Pascal -käännöksen vahvistamattomana, lukea identtisenä lähteessä, ja epäonnistua tavalla, jota kukaan ei näe, ennen kuin joku pitää Peruuta-painiketta painettuna tarpeeksi kauan nähdäkseen työn valmistuvan joka tapauksessa. Tulostusasetuksille, joiden päälle tämä peruutuslogiikka istuu, läpikäynti PDF-asiakirjojen tulostamisesta PDFium VCL -komponentilla kattaa loput
Tämä tulostuksen peruutuskorjaus julkaistiin osana PDFium-komponenttia Delphille, C++Builderille ja FPC/Lazarukselle, joka ajaa saman demosarjan kaikkien kolmen kääntäjän yli jokaisella julkaisulla, jotta tällaiset aukot nappaa käännösmatriisi eikä tukipyyntö