Just some stuff about me.
Here's my dotfiles repository.
&dyn Trait
) in binaries, because cleaner code with fewer generic params at usually marginal performance cost.impl AsRef<str>
instead of &str
). Start with fully generic argument with no bounds, then follow compiler errors to add bounds.impl AsRef<str>
, use &dyn AsRef<str>
). But users cannot opt out and must always provide trait object, so not a great idea if code may be performance-sensitive.impl Trait
to provide only the very minimal contract to the caller. By promising less, you make fewer changes breaking.!
type. If you write a function that returns Result<T, !>
, you will be unable to ever return Err, since the only way to do so is to enter code that will never return.always implement Debug, Clone, Default
PartialEq
trait is particularly desirable, because users will at some point inevitably have two instances of your type that they wish to compare with == or assert_eq!
. Even if your type would compare equal for only the same instance of the type, it’s worth implementing PartialEq to enable your users to use assert_eq!
.
serde Serialize/Deserialize: these can be easily derived, and the serde_derive
crate even comes with mechanisms for overwriting the serialization for just one field or enum variant. Since serde is a third-party crate, you may not wish to add a required dependency on it. Most libraries therefore choose to provide a serde feature that adds support for serde only when the user opts into it.
Deref: If you provide a relatively transparent wrapper type (like Arc), there’s a good chance you’ll want to implement Deref so that users can call methods on the inner type by just using the .
operator
It’s good practice to include some simple tests in your test suite that check that all your types implement these traits the way you expect. This does not run any code, but if it doesn’t compile, auto-trait implementations are broken.
fn is_normal<T: Sized + Send + Sync + Unpin>() {}
#[test]
fn normal_types() {
is_normal::<MyType>();
}
std::error::Error
. The main method of interest is Error::source, which provides a mechanism to find the underlying cause of an error.'static
. Allows caller to more easily propagate error type without lifetime issues, and enabled more easy usage with type-erased error types.
'static
, allows user to turn dyn Error
into concrete underlying type, using Error::downcast_ref
#[derive(Debug)]
is usually sufficient for.Box<dyn Error>
, you leave your users with little option but to bubble up your errorResult<T, ()>
to Option<T>
. An Err(())
indicates that an operation failed and should be retried, reported, or otherwise handled exceptionally. None, on the other hand, conveys only that the function has nothing to return; it is usually not considered an exceptional case or something that should be handled. You can see this in the #[must_use]
annotation on the Result type—when you get a Result, the language expects that it is important to handle both cases, whereas with an Option, neither case actually needs to be handled.#[doc(cfg(..))]
to highlight items that are available only under certain configurations so the user quickly realizes why some method that’s listed in the documentation isn’t available.#[doc(alias = "...")]
to make types and methods discoverable under other names that users may search for them bytracing crate
If your code changes based on type, use generics. Otherwise, use macros.
cargo miri test
rust_2018_idioms
, missing_docs
, missing_debug_implementation
Standardized polling:
Future<Output = Foo>
as “a type that will produce a Foo in the future.” Types like this are often referred to in other languages as promisesWhy not threads: hard to keep track, threads add up fast and complexity to keep track does too, switching between many threads is costly (each switch is round-trip to OS scheduler), introduce parallelism
Concurrency: execution of tasks is interleaved
Parallelism: multiple tasks executing at the same time
Declarative macros:
mod
is important. If you mark macro with #[macro_export]
, the macro is hoisted to root of crate and marked as pub, so it can be used anywhere.
Procedural macros:#[derive(Serialize)]
): automate implementation of a trait where possible (only adds to target of macro)#[test]
): replaces item, takes as input the token tree in the attribute and the token tree of the entire itemSpan::call_site
) and which should be treated as private to the macro (using Span::mixed_site
)"".parse::<TokenStream>()
).Orphan rule: you can implement a trait for a type only if the trait or the type is local to your crate. You can’t implement Debug for bool.
Static dispatch (fn x(a: impl Pattern)
, fn x<T: Pattern>(a: T)
): need different copy of x
for each impl Pattern
type – need to know address of instance functions to call them. For each of the types, create a copy of the method with right addresses.
Dynamic dispatch (fn x(a: &dyn Pattern)
): the caller must give address of pattern and address of any methods called on it, via vtable.
&
? We no longer know at compile time the size of the pattern type (dyn Trait
is not Sized
), but we know the size of pointers/references.Rust traits can be generic: with type parameters (trait Foo<T>
) or with associated types (trait Foo { type Bar;}
). Use associated type if you only expect one implementation of the trait for a given type, use generic type parameter otherwise. Just use associated types whenever you can.
Trait object: type that implements a trait, and its vtable. So, dyn Trait
is a trait object.
Marker types: consider a type like SshConnection
, which may or may not have been authenticated yet. You could add a generic type argument to SshConnection
and then create two marker types: Unauthenticated and Authenticated. When the user first connects, they get SshConnection<Unauthenticated>
. In its impl
block, you provide only a single method: connect. The connect method returns a SshConnection<Authenticated>
, and it’s only in that impl
block that you provide the remaining methods for running commands and such.
Use an equal sign in the type parameter for default
We introduce unit types to represent each stage of the rocket. We don’t actually need to store the stage—only the meta-information it provides—so we store it behind a PhantomData to guarantee that it is eliminated at compile time. Then, we write implementation blocks for Rocket only when it holds a particular type parameter.
struct Grounded;
struct Launched;
struct Rocket<Stage = Grounded> {
stage: std::marker::PhantomData<Stage>,
}
impl Default for Rocket<Grounded> {}
impl Rocket<Grounded> {
pub fn launch(self) -> Rocket<Launched> { }
}
impl Rocket<Launched> {
pub fn accelerate(&mut self) { }
pub fn decelerate(&mut self) { }
}
impl<Stage> Rocket<Stage> {
pub fn color(&self) -> Color { }
pub fn weight(&self) -> Kilograms { }
}
with #[must_use]
on type/trait/function, compiler issues a warning if user code receives an element of that type/trait or calls function, and does not explicitly handle it (like unhandled Result). Add it only if the user is very likely to make a mistake if they are not using the return value.
Testing:
tests/
) follow the same process as unit tests, with the one exception that they are each compiled as their own separate crate, meaning they can access only the main crate’s public interface and are run against the main crate compiled without #[cfg(test)]
.cargo miri test
runs through mid-level intermediate representation, which is slower, but reports if program exhibits undefined behavior (uninit memory reads, use after drop, out-of-bounds pointer access).Not all compiler warnings are enabled by default. Those disabled by default are usually still being refined, or are more about style than content. A good example of this is the “idiomatic Rust 2018 edition” lint, which you can enable with #![warn(rust_2018_idioms)]
. When this lint is enabled, the compiler will tell you if you’re failing to take advantage of changes brought by the Rust 2018 edition. Some other lints that you may want to get into the habit of enabling when you start a new project are missing_docs
and missing_debug_implementations
, which warn you if you’ve forgotten to document any public items in your crate or add Debug implementations for any public types, respectively.
With std::hint::black_box(x)
, you can tell compiler to assume that x
is used in arbitrary ways that can’t be optimized out.