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++:
class Timer {
public :
Timer (const string n ) : name (n ), start (now ()) {}
~Timer () {
log << "Done with " << name << " after "
<< (now () - start ) << " ms." ;
}
private :
int start ;
string name ;
}
void foo () {
Timer timer ("foo" );
// do stuff here
}
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:
class Timer {
private int start = System .currentTimeMillis ();
private String name ;
public Timer (String name ) {
this .name = name ;
}
public void log () {
log .debug ("Done with {} after {} ms." , name ,
System .currentTimeMillis () - start );
}
}
void foo () {
Timer timer = new Timer ("foo" );
try {
// do stuff here
} finally {
timer .log ();
}
}
void otherFoo () {
int start = System .currentTimeMillis ();
try {
// do stuff here
} finally {
log .debug ("Done with foo after {} ms." ,
System .currentTimeMillis () - start );
}
}
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:
class Timer :
def __init__ (self , name ):
self .name = name
self .start = datetime .now ()
def __enter__ (self ):
pass # Nothing to do here
def __exit__ (self , exc_type , exc_value , traceback ):
# (parametrarna ger kännedom om eventuell exception)
log .debug ("Done with %s after %s." , self .name , datetime .now () - self .start )
return false # vi har inte hanterat eventuell exception
def foo ():
with Timer ("foo" ):
# 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.
func Timer (start time.Time , name string ) {
elapsed := time .Since (start )
fmt .Printf ("Done with %s after %s\n" , name , elapsed )
}
func foo () {
defer Timer (time .Now (), "foo" )
// do stuff here
}
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:
func Timer (name string ) func () {
start := time .Now ()
return func () {
fmt .Printf ("Done with %s after %s\n" , name , time .Since (start ))
}
}
func foo () {
defer Timer ("foo" )()
// do stuff here
}
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:
func foo () {
timer := Timer ("foo" )
defer timer ()
// do stuff here
}
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:
func bar () {
for i := 0 ; i < 5 ; i ++ {
defer Timer (fmt .Sprintf ("bar #%d" , i ))()
fmt .Printf ("Doing stuff in bar #%d\n" , i )
}
}
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 DbConnections
destruktor i alla lägen när konstruktorn har lyckas, även om query
kastar en exception:
list < string > simpleGet () {
DbConnection con (dbPool );
return con .query ("select foo from bar" );
}
list < string > evenSimplerGet () {
return DbConnection (dbPool ).query ("select foo from bar" );
}
I go går det såvitt jag begriper inte att göra enklare än de här alternativen:
func notSoSimpleGet () []string {
con := dbPool .getConnection ()
defer con .close ()
con .query ("select foo from bar" )
}
func otherNotSoSimpleGet () []string {
con , closer := dbPool .getConnectionAndCloser ()
defer closer ()
con .query ("select foo from bar" )
}
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
Det här inlägget är 11 år gammalt, det kan inte längre kommenteras.