Guide to writing idiomatic code at PingCAP and in the TiKV project.
For generic data types, prefer to use the most flexible bounds. That usually means putting strict bounds only on impl
s, not on the concrete datatype, and may mean splitting impl
s into smaller blocks.
The generic parameters on the concrete type should only have bounds if they are intrinsic to the datatype itself (which includes lifetime bounds, ?Sized
, where the bound is required to name an associated type, or the bound is required by a Drop
impl
).
Generic parameters in concrete datatypes should never have derivable bounds.
Rationale: makes datatypes most flexible, reduces code duplication, and makes types more backwards compatible. See this section of the API guide for more details.
Avoid default generics, unless the default is used in the vast majority of situations (e.g., the default is only overridden for testing, or in very exceptional cases, such as supplying a hasher to a hashtable). Rationale: having an explicit type often improves comprehensability of code and outweighs the convenience of skipping a generic parameter).
Use types to enforce invariants. Take advantage of structs, tuples, and enums to define the valid states of an object. Use associated types to enforce relationships between types. Where possible, make invalid objects impossible.
Use type aliases (type
) to provide convenience versions of common generic types (e.g., type Result<T> = Result<T, MyErr>;
).
struct Foo;
, not struct Foo {}
or struct Foo();
(the latter two should only be used if necessary in macros).repr(C)
then the order of the fields is significant.
The usual motivation for repr(C)
objects is mapping them to objects in other languages.
If the order is not constrained, then the most efficient representation is usually to list the fields in descending order of size.Foo
to Foo()
or Foo {}
.None
variant, rather than wrapping it with an Option
.
Result
and an Err
variant.None
variant in Foo
has a different meaning to using Option<Foo>
(may always be null vs may sometimes be null).None
variant makes for simpler pattern matching and simpler types than using an Option
.Point { x: i32, y: i32 }
rather than (i32, i32)
.Rationale: named types and fields nearly always improve the readability of code.
Only use unions for FFI to mirror a C enum. In Rust code prefer an enum.
A common way to construct concrete data with complex internals is the builder pattern.
It is recommended to use the builder pattern to avoid a large number of constructor functions (or complex constructor functions with many Option
s), but avoid using them instead of one or two simple constructor functions.