Destruktorer och defer
Publicerad 2014-11-09 00:10. Taggat python, java, c++, defer, destructor, golang.
En av de saker jag verkligen gillar i c++ är att man kan ha lokala variabler som är objekt i kombination med att objekt har destruktorer. När man lämnar ett scope där det finns lokala variabler kommer deras destruktorer omedelbart att köras, oavsett hur man lämnar scopet. Det ger ett trevligt sätt att hantera öppna filer, databaskoppel, mutexar och annat som behöver stängas. Andra språk har andra, ofta krångligare, sätt att få motsvarande resultat.
Språket go har en helt annan aproach, som är värd att titta lite närmare på.
Eftersom hantering av databaskoppel och filer har en hel del andra komplikationer och genvägar i olika språk så tar jag ett annat, enklare, exempel: att mäta och logga exekveringstid för en funktion. Här är ett sätt att göra det i c++:
void
Hur enkel eller jobbig koden i class Timer
behöver vara spelar inte
så stor roll, men koden för att använda klassen vill jag ha så enkel
som möjligt (foo()
är ju bara en av dussintals funktioner man vill
mäta på), och det tycker jag den är här.
Objekt i java har också en sorts destruktorer, finalize()
,
men eftersom man inte kan skapa objekt lokalt i java så anropas de
inte förrän vm-systemet tycker det är dags att städa undan objektet,
vilket är helt otilräckligt för till exempel att stänga databaskoppel
eller mäta funktionens exekveringstid. Då måste man i stället göra så
här:
void
void
Betydligt mer kod i foo
.
Faktiskt så mycket att klassen Timer knappast gör någon nytta alls,
man kan lika gärna lägga den implementationen direkt i varje funktion
man mäter på, som i otherFoo
.
Så här illa såg det ut i all fungerande javakod som använde databaser
innan spring JdbcTemplate
kom.
Java 8 har ett lite trevligare sätt, men det har varit det vanliga
sättet i python långt innan det kom i java, så jag visar det exemplet
i python i stället:
=
=
pass # Nothing to do here
# (parametrarna ger kännedom om eventuell exception)
return # vi har inte hanterat eventuell exception
# do stuff here
Språket go har en helt annan aproach.
Go har visserligen ganska objekt-aktiga structar, men fokuserar mer på
funktioner.
Ett speciellt uttryck i go är defer
.
Det tar ett funktionsanrop, evaluerar parametrarna till värden direkt
och sparar undan funktionen och parametervärdena, samt anropar
funktionen när man lämnar funktionen som defer
anropades i.
Men om man vill anropa en funktion som returnerar en funktion och defera anropet till den returnerade funktionen då? I go är funktioner data lika väl som strängar eller structar, så det går utmärkt:
Här kan man lägga märke till hur den inre (anonyma) funktionen i Timer
använder en parameter och en lokal variabel direkt från den yttre
funktionen utan problem.
Det är inte bara en funktion som returneras, utan en closure,
kombinationen av en funktion och det funktionen behöver veta ur det
state den skapades i.
En annan sak att lägga märke till är hur det första anropet
Timer("foo")
närmast behandlas som en parameter till det anrop som
deferas.
Funktionen foo
ovan är identisk med om man hade skrivit
såhär:
En väsentlig skillnad mellan defer i go och destruktorer i c++ är att defer lägger saker när man lämnar funtionen, inte blocket. Följande kod kommer inte att göra det man kanske hade tänkt sig när man skrev den:
En annan hake med defer är just att det är ett funktionsanrop man senarelägger.
Funktioner i go kan visserligen returnera mer än en sak, men jag
kommer inte på något sätt att göra defer på den ena och
tillgängliggöra den andra som en variabel, annat än att lägga båda
värdena i variabler och sedan göra defer på det ena.
I c++ kommer båda de här metoderna att anropa DbConnection
s
destruktor i alla lägen när konstruktorn har lyckas, även om query
kastar en exception:
list<string>
list<string>
I go går det såvitt jag begriper inte att göra enklare än de här alternativen:
Det jag har sett i verkligheten (i mgo.v2
) ser ut som det första
alternativet.
Jag kan hålla med om att det ser snyggare ut, men det andra har fördelen
att det är svårare att missa att man behöver anropa stängaren, eftersom go
klagar om man deklarerar variabler som man inte använder.
Eller går det att göra snyggare? Jag har nyss börjat med go, så det är mycket möjligt att det finns bättre sätt som jag har missat? Skriv en kommentar och upplys mig!
Kommentarer
för att mäta tiden av en funktion hade jag gjort något sånt här... (bra snippet att ha i ett samlat paket)
//timeing the program func timeTrack(start time.Time, name string) { elapsed := time.Since(start) fmt.Printf("%s took %s", name, elapsed) }
func main() { defer timeTrack(time.Now(),"Complex work") someComplexHardWork() }
Undertecknat, Christopher Lillthors
2014-11-09 13:08
Du föredrar defer timeTrack(time.Now(), "Complex work") framför defer Timer("Complex work")() alltså? Varför det?
Undertecknat, Rasmus Kaj
2014-11-10 06:38
Den här posten skrev jag ungefär ett halvår innan jag upptäckte språket Rust, så det är inte med i jämförelsen. Kort kan man säga att Rust bygger vidare på RAII (Resource acquisition is initialization) från C++. Synnerligen trevligt för saker som databasconnections och upplåsta mutexar.
Undertecknat, Rasmus Kaj
2023-02-10 12:18
Skriv en kommentar