4 ms·
Destructors and finally clauses serve different purposes IMO. Most of the languages that have finally clauses also have destructors. > Syntax is also less clut
by jchw 10mo ago
Destructors and finally clauses serve different purposes IMO. Most of the languages that have finally clauses also have destructors.
> Syntax is also less cluttered with less indentation, especially when multiple objects are created that require nested try... finally blocks.
I think that's more of a point against try...catch/maybe exceptions as a whole, rather than the finally block. (Though I do agree with that. I dislike that aspect of exceptions, and generally prefer something closer to std::expected or Rust Result.)
- mort96 10mo ago> Most of the languages that have finally clauses also have destructors. Hm, is that true? I know of finally from Java, JavaScript, C# and Python, and none of them have proper destructors. I mean some of them have object finalizers which can be used to clean up resources whenever the garbage collector comes around to collect the object, but those are not remotely similar to destructors which typically run deterministically at the end of a scope. Python's 'with' syntax comes to mind, but that's very different from C++ and Rust style destructors since you have to explicitly ask the language to clean up resources with special syntax. Which languages am I missing which have both try..finally and destructors?
- dist-epoch 10mo agoTechnically CPython has deterministic destructors, __del__ always gets called immediately when ref count goes to zero, but it's just an implementation detail, not a language spec thing.
- jchw 10mo agoI don't view finalizers and destructors as different concepts. The notion only matters if you actually need cleanup behavior to be deterministic rather than just eventual, or you are dealing with something like thread locals. (Historically, C# even simply called them destructors.)
- mort96 10mo agoThere's a huge difference in programming model. You can rely on C++ or Rust destructors to free GPU memory, close sockets, free memory owned through an opaque pointer obtained through FFI, implement reference counting, etc. I've had the displeasure of fixing a Go code base where finalizers were actively used to free opaque C memory and GPU memory. The Go garbage collector obviously didn't consider it high priority to free these 8-byte objects which just wrap a pointer, because it didn't know that the objects were keeping tens of megabytes of C or GPU memory alive. I had to touch so much code to explicitly call Destroy methods in defer blocks to avoid running out of memory.
- cogman10 10mo agoFor GCed languages, I think finalizers are a mistake. They only serve to make it harder to reason about the code while masking problems. They also have negative impacts on GC performance. Java is actively removing it's finalizers.
- adrianN 10mo agoSometimes „eventually“ is „At the end of the process“. For many resources this is not acceptable.
- rramadass 10mo ago> I don't view finalizers and destructors as different concepts. They are fundamentally different concepts. See Destructors, Finalizers, and Synchronization by Hans Boehm - https://dl.acm.org/doi/10.1145/604131.604153 https://dl.acm.org/doi/10.1145/604131.604153
- jchw 10mo agoIt would suffice to say I don't always agree with even some of the best in the field, and they don't always agree with each other, either. Anders Hejlsberg isn't exactly a random n00b when it comes to programming language design and still called the C# equivalent a "destructor", though it is now known as a finalizer in line with other programming languages. They are things that clean up resources at the end of the life of an object; the difference between GC'd languages and RAII languages is that in a GC'd runtime the lifespan of an object is non-deterministic. That may very well change the programming model, as it does in many other ways, but it doesn't make the two concepts "fundamentally different" by any means. They're certainly related concepts...
- brewmarche 10mo agoIn C# the closest analogue to a C++ destructor would probably be a `using` block. You’d have to remember to write `using` in front of it, but there are static analysers for this. It gets translated to a `try`–`finally` block under the hood, which calls `Dispose` in `finally`. using (var foo = new Foo()) { } // foo.Dispose() gets called here, even if there is an exception Or, to avoid nesting: using var foo = new Foo(); // same but scoped to closest current scope These also is `await using` in case the cleanup is async (`await foo.DisposeAsync()`) I think Java has something similar called try with resources.
- cogman10 10mo agoJava's is try (var foo = new Foo()) { } // foo.close() is called here. I like the Java method for things like files because if the there's an exception during the close of a file, the regular `IOException` block handles that error the same as it handles a read or write error.
- mort96 10mo agoWhat do you do if you wanna return the file (or an object containing the file) in the happy path but close it in the error path?
- cogman10 10mo agoYou'd write it like this void bar() { try (var f = foo()) { doMoreHappyPath(f); } catch(IOException ex) { handleErrors(); } } File foo() throws IOException { File f = openFile(); doHappyPath(f); if (badThing) { throw new IOException("Bad thing"); } return f; } That said, I think this is a bad practice (IMO). Generally speaking I think the opening and closing of a resource should happen at the same scope. Making it non-local is a recipe for an accident. *EDIT* I've made a mistake while writing this, but I'll leave it up there because it demonstrates my point. The file is left open if a bad thing happens.