|
With good design, the range of invalidations that Rust can prevent go well beyond memory invalidation. For example, a serialization/deserialization library I'm working on has the following trait: pub trait Decode : Sized {
fn decode<D: Decoder>(decoder: D) -> Result<(Self, D::Done), D::Error>;
}
Basically, types that can be decoded implement this trait.The trick here is that the decode() method consumes the decoder, and then returns it in the output. In the case of decoding an IO stream, the D::Done type is the IO stream itself, which means that in the event of an error, we ensure that the user can't accidentally use the IO stream again in an incompletely decoded state because all they have is the error type, D::Error (they can intentionally use the IO stream again by recovering the IO stream handle from the D::Error type). In practice, the above results in decode() implementations that look like the following: fn decode<D: Decoder>(decoder: D) -> Result<(Foo, D::Done), D::Error> {
let (v0, decoder) = decoder.decode()?;
let (v1, decoder) = decoder.decode()?;
let (v2, decoder) = decoder.decode()?;
Ok((Foo(v0, v1, v2),
decoder.done()?))
}
This is a bit more verbose, but as I also make use of Rust's procedural macros you'd also never actually write the above code; it's auto-derived/auto-generated for you 99% of the time. Equally, if I ever do make a mistake in the auto-generation this state-machine-like approach makes it very likely that the resulting auto-generated code won't even compile. |