Destructors - close() runs at the last release
A shared type's close() is its destructor - it runs exactly once at the last release, user code never calls it, and a binding that holds one is released at the end of its block, the last declared first.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
close() is the one destructor of the language. The runtime's reference count triggers it, exactly once, when the
last holder of an object goes away. A binding whose value holds such an object is released at the end of the block it
was declared in, the last declared first, so the moment an object closes is a line of the source.
Example#
shared type Connection with Close {
var open: Bool = true
fn send(message: String) {
print message
}
var fn close() {
open = false
print "closed"
}
}
fn greet() {
using connection = Connection()
connection.send "hello"
print "sent"
}greet() prints hello, sent and then closed: connection is released at the end of the block that bound it,
and the release runs close().
Syntax#
public shared trait Close { var fn close() }
using name = expression
using name: Type = expressionusing is a keyword only in front of a name and an = (or a : and a type); everywhere else it is an ordinary name.
Rules#
Only a
shared typemay implementClose. A value is copied on assignment, so two copies would give two closes of one resource; an object has one identity for the destructor to belong to. A capsule is a value too, and a trait that comeswith Close-Sink,Source- counts asClose.Kompiliert nichttype Ticket with Close { var used: Bool = false var fn close() { used = true } } // error: `Close` may only be implemented by a `shared type`close()runs exactly once, when the last reference to the object goes away, and user code cannot call it. Not from outside, not through a trait-typed value, and not from inside the type -close()of a field included, because the release takes the fields down right after the object's ownclose(). To release early, let the scope end - a nested block if it has to be mid-function - or bind the object withusing.Kompiliert nichtshared type Connection with Close { var fn close() {} } fn finish() { const connection = Connection() connection.close() } // error: `close` runs automatically when the last reference to this value goes away, and cannot be called directlyA
close()of a type that does not implementCloseis an ordinary method.A binding whose value may hold a
Closeobject is released at the end of its block, in reverse declaration order. Not at its last use: in the example belowfirstis used for the last time beforesecondand still closes after it. Every way out of the block ends it the same way - the end of the block, areturn, a?that hands a failure on, abreakor acontinue. The names a pattern binds belong to the block they are bound for: thethenblock of anif const, the arm of amatch, the body of aforor awhile const, which ends them every round.shared type Probe with Close { name: String fn touch() { print "touch {name}" } var fn close() { print "close {name}" } } fn twoBindings() { const first = Probe "first" const second = Probe "second" first.touch() second.touch() print "end of the block" } // touch first, touch second, end of the block, close second, close first"May hold" is a fact of the type: an object of a type that implements
Close, a record, a case, a tuple or a collection with one inside, and a trait-typed value that may carry one. Every other binding is released where the compiler finds its last use, which nothing can observe.A value that is moved out leaves nothing behind. A binding that is returned, or stored where it outlives the block, is not released at the end of the block: its new holder closes it, wherever its own last reference goes. Where one path moves the value and another does not, the other path releases it at the end of the block.
A temporary is released at the end of its statement, the last made first.
print(described(Probe("x")), "rest")closes the probe after the whole line was printed, not afterdescribedreturned.A released object runs its own
close()first and then releases its fields in reverse declaration order; a collection releases its elements from the last to the first. A value is taken down in the reverse of the order it was built, so something built on another thing is gone before the thing it was built on.close()may not keepself. Insideclose(),selfmay be read, may call the object's members and may be handed to a function that only uses it. Storing it, returning it, binding it to another name, handing it to avar fnor to a member of a shared object (either may keep it), or capturing it in a closure that outlives the call is an error, because nothing may hold an object that is being released.Kompiliert nichtshared type Node with Close { var others: List<Node> = [] var fn close() { others.append self } } // error: `close` may not keep `self`: the object is being releasedclose()returnsVoid, cannot fail and never awaits. A type whose graceful end can fail or has to wait offersend()beside it, and a program calls and awaits that where it wants it; afterend(),close()does nothing.using name = expressionbinds aconstthat cannot escape its block. It may be read, be the receiver of a member and be handed to a function that only uses it. Storing it in a field or a collection, returning it, binding it to another name, handing it to a constructor, a case, avar fnor a member of a shared object, or capturing it in a closure that may outlive the block is an error. What a call answers is not followed: a function that returns what it was handed can still carry the object out.usingnever awaits anything, not even inside a task.Kompiliert nichtshared type Connection with Close { var fn close() {} } type Holder { var held: Connection } fn keep(var holder: Holder) { using connection = Connection() holder.held = connection } // error: `connection` is bound by `using`, so it cannot be stored in a fieldusingbelongs to a block. At the top level of a file its block would be the whole program, so it is an error there; inside a function, a closure or any block of a script it is allowed. The top level of an entry file is a block of its own: its bindings end where the file does, the last declared first.Cancelling a task releases its frame, at its next suspension point or at the next turn of a loop, and that release closes every object the frame held, the last declared first, before anybody waiting for the task hears that it was cancelled. Dropping the
Taskhandle cancels nothing.A panic runs no
close(). The process ends, and the operating system takes back what the program held.
Pitfalls#
- A closure retains what it captures. A binding a closure captured is still held by its own slot until its block
ends, so a writing end captured by a
spawnclosure does not end its channel before the block that bound it ends. Bind it in a block of its own, or make it inside the task. - A temporary that only one path of its statement makes - an arm of an
ifexpression, the fallback of a??- is released where it was used for the last time, because the end of the statement is not a line every path has made it at.
What this is not#
Not a method to call. Rule 2 refuses every call, which is also why std has none: a wrapper such as a buffered
sink holds the sink below it in a field, and releasing the wrapper releases that field after the wrapper's own
close().
Not a garbage collector. There is reference counting and nothing else - no tracing pass and no cycle collector.
Two shared objects that hold each other are never released and never closed; trees and graphs hold handles such as an
Entity instead of references, and a stored callback takes its owner as a receiver instead of capturing it.
Not an implicit wait. The end of a using block runs close(), the abrupt end. A sink that has to be flushed is
ended by sink.end().await() on a line of its own, before the block ends.
Not a cost for anything else. A type without Close pays nothing: its release is what it always was, field by
field, and a binding of it is released at its last use.
Related#
- Shared types - the identity a destructor belongs to.
- What a copy costs - reference counting, the mechanism that triggers
close(). - Result - what a
panicskips on its way out.