Rasmus​.krats​.se

Destruktorer och defer

Publicerad 2014-11-09 00:10. Taggat , , , , , .

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

Skriv en kommentar

Enkel markdown tillåts.

Ditt namn (eller pseudonym).

Publiceras inte, utom som gravatar.

Din presentation / hem­sida (om du vill).