How to build a Rust API using the builder pattern

Let's face it; with its optional and named parameters, Python has an advantage over many other programming languages ​​like Rust. However, Rust library authors can very efficiently address this shortcoming using the builder pattern. The idea behind this pattern is deceptively simple: create an object that can, but need not, hold all values, and have it create our type when all required fields are present.

In this article, we'll explore Rust's builder pattern, 30 Best Code Editors for Windows covering the following:

  • builder mode

  • Constructors with mutable references

  • Into and AsRef traits

  • Defaults

  • Use type state to track collection fields

  • Rust builder pattern crate

builder mode

To get acquainted with the builder pattern in Rust, fix not being able to log into YouTube on Windows 10/11 let's first compare how the code looks with and without the builder:

// without a builder

let base_time = DateTime::now();

let flux_capacitor = FluxCapacitor::new();

let mut option_load = None;

let mut mr_fusion = None;

if get_energy_level(&plutonium) > 1.21e9 {

option_load = Some(plutonium.rods());

} else {

// need an energy source, can fail

mr_fusion = obtain_mr_fusion(base_time).ok();

}

TimeMachine::new(

flux_capacitor,

base_time,

option_load,

mr_fusion,

)

// with a builder

let builder = TimeMachineBuilder::new(base_time);

.flux_capacitor(FluxCapacitor::new());

if get_energy_level(&plutonium) > 1.21e9 {

builder.plutonium_rods(plutonium.rods())

} else {

builder.mr_fusion()

}

.build()

All examples in this book are designed to be simple. Actually, how to fix system interrupt 100CPU in Windows 10? How to reduce high cpu usage problem You would use builders for complex types with more dependencies.

The main function of the builder is to save the data needed to build the instance in one place . Just define the TimeMachineBuilder struct, put in a bunch of fields, add a with a and a method, and some setters, and you're done. That's it, you now know all there is to know about builders. See you next time! Option<_>implnewbuild

are you still here? Ah, I doubt you'll be fooled. Of course, there is more to how to fix Microsoft Store error 0x80073CFB for builders than the obvious data collection.

Constructors with mutable references

Unlike some garbage collected languages, in Rust we distinguish between owned and borrowed values. Therefore, there are various ways to set up builder methods. One is obtained by variable reference, and the other is obtained by value. The former has two sub-variants, return link or . allow link which is slightly more common. &mut selfself&mut self()

Our example uses links and thus a generator by value. The result of new is used directly to call another method.

The benefit of mutable borrowed builders is the ability to call multiple methods on the same builder while still allowing some chaining. However, this comes at the cost of requiring a binding to the builder settings. For example, how to fix Zoom not eligible to register? Zoom is not eligible to register error and the reason behind it The following code will fail with return method: &mut self

let builder= ByMutRefBuilder::new()

.with_favorite_number(42); // this drops the builder while borrowed

However, doing the full chain still works:

ByMutRefBuilder::new()

.with_favorite_number(42)

.with_favorite_programming_language("Rust")

.build()

If we want to reuse the builder, we need to bind the result of the call: new()

let mut builder = ByMutRefBuilder::new();

builder.with_favorite_number(42) // this returns `&mut builder` for further chaining

We could also ignore chaining and instead call the same binding multiple times:

let mut builder = ByMutRefBuilder::new();

builder.with_favorite_number(42);

builder.with_favorite_programming_language("Rust");

builder.build()

On the other hand, builders by value require rebinding in order not to discard their state:

let builder = ByValueBuilder::new();

builder.with_favorite_number(42); // this consumes the builder :-(

Therefore, they are usually chained:

ByValueBuilder::new()

.with_favorite_number(42)

.with_favorite_programming_language("Rust")

.build()

So for builders by value we need chaining. On the other hand, the 50 best free game downloads for Windows 10 builder for variable references will allow chaining as long as the builder itself is bound to some local variable. Also, builders for mutable references can be reused freely, since they are not consumed by their methods.

In general, chaining is the intended way to use builders, so this isn't a huge downside. Also, depending on the amount of data the builder contains, mobile builders may be visible in the performance profile, but this is rare.

What if Windows 10 keeps locking itself up if the builder will be used frequently on complex code with many branches ? Or it might be reused from intermediate state, I prefer mutable referenced builders. Otherwise, I would use a generator by value.

Into and AsRef traits

Of course, the builder methods can do some basic transformations. The most popular one uses the Into trait to bind inputs.

For example, you can index as anything that has an implementation, 12 ways to fix high CPU and memory usage in Google Chrome on a PC, or allow builders to reduce allocations by having a parameter, which makes the function accept both a and . Referencing the given arguments, traits can allow more freedom in the provided types. Into<usize>Into<Cow<'static, str>>&'static strStringAsRef

There are also specialized traits such as IntoIterator and ToString that can sometimes be useful to fix Skype high CPU usage in Windows 10 . For example, if we have an array of values, we can have add method that extends add_all inside each Vec :

impl FriendlyBuilder {

fn add(&mut self, value: impl Into<String>) -> &mut Self {

self.values.push(value.into())

self

}

fn add_all(

&mut self,

values: impl IntoIterator<Item = impl Into<String>>

) -> &mut Self {

self.values.extend(values.into_iter().map(Into::into));

self

}

}

Defaults

Types can often have viable default values. So, 9 Ways to Fix Spotify Error on Windows 10 Builder can pre-set these defaults and replace them only when requested by the user. In rare cases, getting the default value can be expensive. The builder can take an Option , which has its own default value of None , or perform another trick to keep track of which fields are set, which we explain in the next section.

Of course, we don't depend on anything the Default implementation gives us; we can set our own defaults. For example, we could decide that more is better, so the default number would be zero instead of zero. u32::MAXDefault

For more complex types involving reference counting, we may have a static default. For the small runtime overhead of reference counting, it gets it every time. Alternatively, if we allow static instances to be borrowed, 11 Solutions to Fix Outlook Error Cannot display this item in the reading pane we can use the default, avoiding the allocation, while still keeping the build simple: Arc::clone(_)Cow< 'static, T>

use std::sync::Arc;

static ARCD_BEHEMOTH: Arc<Behemoth> = Arc::new(Behemoth::dummy());

static DEFAULT_NAME: &str = "Fluffy";

impl WithDefaultsBuilder {

fn new() -> Self {

Self {

// we can simply use `Default`

some_defaulting_value: Default::default(),

// we can of course set our own defaults

number: 42,

// for values not needed for construction

optional_stuff: None,

// for `Cow`s, we can borrow a default

name: Cow::Borrowed(DEFAULT_NAME),

// we can clone a `static`

goliath: Arc::clone(ARCD_BEHHEMOTH),

}

}

}

Use type state to track collection fields

Tracking settings fields are only available for owned variants. The idea is to use generics to put information about which fields have been set into the type. Thus, we can avoid double settings, in fact, we can even disallow them and only allow the build if all required fields are set.

Let's take favorite numbers, programming languages ​​and colors as an example, of which only the first one is needed. Our type is, where indicates whether the number is set, whether the programming language is set, whether the color is set. TypeStateBuilder<N, P, C>NPC

We can then create Unset and Set types to populate our generics. Our new function will return, and only a has a method. TypeStateBuilder<Unset, Unset, Unset>TypeStateBuilder<Set, _, _>.build()

In our examples, we use defaults everywhere because using unsafe code doesn't help understanding the schema. However, unnecessary initialization can of course be avoided using this scheme:

use std::marker::PhantomData;

/// A type denoting a set field

enum Set {}

/// A type denoting an unset field

enum Unset {}

/// Our builder. In this case, I just used the bare types.

struct<N, P, C> TypeStateBuilder<N, P, C> {

number: u32,

programming_language: String,

color: Color,

typestate: PhantomData<(N, P, C)>,

}

/// The `new` function leaves all fields unset

impl TypeStateBuilder<Unset, Unset, Unset> {

fn new() -> Self {

Self {

number: 0,

programming_language: "",

color: Color::default(),

typestate: PhantomData,

}

}

}

/// We can only call `.with_favorite_number(_)` once

impl<P, C> TypeStateBuilder<Unset, P, C> {

fn with_favorite_number(

self,

number: u32,

) -> TypeStateBuilder<Set, P, C> {

TypeStateBuilder {

number,

programming_language: self.programming_language,

color: self.color,

typestate: PhantomData,

}

}

}

impl<N, C> TypeStateBuilder<N, Unset, C> {

fn with_favorite_programming_language(

self,

programming_language: &'static str,

) -> TypeStateBuilder<N, Set, C> {

TypeStateBuilder {

number: self.number,

programming_language,

color: self.color,

typestate: PhantomData,

}

}

}

impl<N, P> TypeStateBuilder<N, P, Unset> {

fn with_color(self, color: Color) -> TypeStateBuilder<N, P, Set> {

TypeStateBuilder {

number: self.number,

programming_language: self.programming_language,

color,

typestate: PhantomData,

}

}

}

/// in practice this would be specialized for all variants of

/// `Set`/`Unset` typestate

impl<P, C> TypeStateBuilder<Set, P, C> {

fn build(self) -> Favorites {

all!()

}

}

The interface works exactly like the by-value builder, except that the user can only set the field one or more times if added for impl these cases. We can even control which functions are called in which order. For example, we can only allow after has been called and the typestate compiles to empty. .with_favorite_programming_language( ).with_favorite_number( )

The downside of this is obviously complexity; someone needs to write the code, and the compiler has to parse and optimize it. So unless typestate is used to actually control the order of function calls or to allow optimized initialization, it's probably not a good investment.

Rust builder pattern crate

Since builders follow such a simple code pattern, there are many crates on crates.io that automatically generate them.

The derive_builder crate builds our standard mutable reference builder with arguments and optional default values ​​from a custom. You can also provide validation functionality. It is the most popular proc macro crate that automatically generates builders, and is a good choice. At the time of writing, the crate is six years old, so this is one of the first fork crates since fork-stable. Intostruct

The typed-builder crate handles the entire by-value type state implementation, as described above, so you can forget all about what you just read. Just type in your code and enjoy type-safe builders. It also has default and optional annotations, and there is an annotation that allows you to have a setter method that always takes any value and sets it. cargo add typed-builderintostrip_optionSome(value)

The safe-builder-derive crate also implements type state, however, it leads to exponentially growing code by generating a type state impl for each combination of set/unset fields. For small builders with up to three or four fields, this may still be an acceptable choice, otherwise, the compile time cost may not be worth it.

The tidy-builder crate is basically the same as typed-builder, but it's for typestate. The buildstructor crate is also inspired by typed-builder, but it uses annotated constructors instead of structs. The builder-pattern crate also uses the type state pattern and allows you to annotate lazy defaults and validation functions. ~const bool

No doubt there will be more in the future. If you want to use auto-generated builders in your code, I think most of them are good choices. As always, your mileage may vary. For example, requiring annotations for Into parameters may be more ergonomic to some, but reduces complexity to others. Some use cases require authentication while others are useless.

in conclusion

Builders can easily compensate for the lack of named and optional parameters in Rust, and even automatically convert and validate them at compile time and runtime. Plus, the pattern is familiar to most developers, so your users will feel right at home.

As always, the downside is extra code to maintain and compile. Derive crates can remove the maintenance burden at the cost of another fraction of compile time.

So, should you use builders for all types? I would personally only use them for types that have at least five parts or complex interdependencies, however, in these cases I consider them indispensable.

Guess you like

Origin blog.csdn.net/weixin_47967031/article/details/130163116