Skip to content

The Rust Engineering Handbook / Chapter 24

Conversion, Borrowing, Dereference, and View Traits

Choose standard conversion and view traits by ownership, failure, equivalence, and cost instead of accepting ambiguous generic inputs.

Audit the promise at one path boundary

A configuration loader begins with this signature:

fn load<P, S>(path: P) -> Result<Config, LoadError>
where
    P: Into<S>,
    S: AsRef<std::path::Path>,

It looks flexible, but callers and implementers must infer too much. Is conversion allowed to allocate? Why is the intermediate type caller-selected? Is the loader retaining ownership? Can conversion fail? The body only needs to inspect a path during the call.

The honest boundary is narrower:

fn load(path: impl AsRef<std::path::Path>) -> Result<Config, LoadError>

This is not merely cleaner syntax. It states a cheap temporary view, does not promise retention, and accepts Path, PathBuf, strings, and references through their established implementations. If the service queues the request, the contract should instead take an owned PathBuf or a named request type.

Choose a conversion or view trait only when its semantic promise matches the boundary: consumption versus borrowing, infallible versus fallible, projection versus equivalent key, smart-pointer dereference versus domain conversion, and borrowed versus owned retention.

Consuming value conversions: From and Into

From<T> for U consumes a T and produces a U. The standard-library guidance expects it to be infallible, non-lossy in the domain sense, value-preserving in meaning, and unsurprising. Allocation may still occur—String::from(&str) allocates—so infallible does not mean free.

Implement From on the destination when coherence permits. A blanket implementation then supplies Into<U> for T. Constructors often read clearly with U::from(value); generic input bounds may use T: Into<U> when the caller should provide any consumable representation.

Do not implement From for validation that can reject, narrowing that loses meaning, or policy-dependent interpretation. A cents amount can convert infallibly into a wider ledger amount; an arbitrary signed integer cannot become a positive amount without checking. Use a named operation such as truncate, normalize_with(policy), or parse_with_format when the transformation has meaningful choices.

Blanket implementations are part of the compatibility surface. Adding From<A> for B also adds an Into<B> route and may overlap with downstream generic implementations or change inference. Chapter 20’s coherence lesson applies: conversion convenience is a global trait relationship, not a private helper.

For public libraries, adding an implementation can be a SemVer concern even when it adds no method. Generic code may gain multiple inference candidates, or a downstream implementation may become overlapping. Review conversion impls with the same ownership matrix used for other traits. Prefer a local named constructor when the global relationship is not unquestionably canonical.

Fallible value conversions: TryFrom and TryInto

TryFrom<T> pairs consumption with an associated error. It fits checked numeric narrowing, validation into a domain type, or representation changes that may fail:

#[derive(Debug, Eq, PartialEq)]
struct PositiveCents(u64);

impl TryFrom<i64> for PositiveCents {
    type Error = AmountError;

    fn try_from(value: i64) -> Result<Self, Self::Error> {
        let value = u64::try_from(value).map_err(|_| AmountError::NotPositive)?;
        (value > 0).then_some(Self(value)).ok_or(AmountError::NotPositive)
    }
}

At generic call sites, a TryInto<T> bound can admit types that provide the reciprocal direction directly. Preserve useful error types instead of erasing every failure into a string. State whether the input is recoverable after failure; consuming conversion owns it, so an error that must return original data may need a custom error carrying that value or a borrowed validation method.

Parsing text is usually named FromStr, not TryFrom<&str>, because “parse” communicates grammar and integrates with .parse(). Protocol decoding often deserves a named function with version, framing, and resource-limit parameters. Standard traits are not a mandate to erase important context.

Cheap projections: AsRef and AsMut

AsRef<T> exposes a cheap shared reference-to-reference conversion; AsMut<T> does the mutable counterpart. They are well suited to parameters used transiently as paths, byte slices, OS strings, or another clear projection. Implementations should not fail or perform expensive work.

An AsRef<Path> bound says only that the function can view a Path during the borrow. It does not allow storing that reference beyond its lifetime, nor does it require cloning. It can accept owned values too, but passing ownership to such a function is a caller choice; the callee still receives only the view.

Avoid generic parameters when a plain &Path is enough. A library boundary called by many concrete path-like types may benefit from impl AsRef<Path>; internal functions often become simpler and compile faster with &Path after the edge performs path.as_ref() once. Generic convenience has monomorphization and diagnostic cost.

AsRef is a projection, not necessarily semantic equivalence. A record may expose one byte field with AsRef<[u8]>; hashing or ordering the record need not match hashing or ordering that slice. That difference separates it from Borrow.

Equivalent borrowed forms: Borrow

Borrow<Q> supports an owned type being treated as an equivalent borrowed key. The key requirement is stronger than obtaining a reference: equality, ordering, and hashing must behave equivalently for owned and borrowed forms. This is why a HashMap<String, V> can look up by &str without allocating.

Use Borrow for generic data structures and lookup relationships, not merely to expose a field. If a user record borrows its username but record equality also includes tenant and status, implementing Borrow<str> for username would violate the equivalence expected by keyed collections. Use an accessor or AsRef projection instead.

BorrowMut exists for mutable equivalent borrowing, but equivalent-key use cases are commonly shared. Mutation that changes hash or ordering while a value resides in a keyed collection can violate collection logic even in safe code; APIs should not expose such mutation through stored keys.

Deref is not inheritance

Deref<Target = T> describes smart-pointer-like access. Dereference coercion lets &Wrapper act as &T at coercion sites, and method lookup may find methods on the target. DerefMut adds mutable dereference when exclusive access to the wrapper can soundly expose exclusive target access.

That convenience makes Deref a major API commitment. Target methods become part of how users experience the wrapper; adding inherent methods can create resolution surprises; invariants must survive every exposed target operation. A validated Email(String) should not generally implement DerefMut<Target = String>, because callers could mutate it into invalid data. Even shared Deref<Target = str> may expose a broad string API when an explicit as_str better communicates domain boundaries.

Use Deref for pointer and guard types whose primary purpose is transparent access, such as boxes, reference-counted pointers, or lock guards. Do not use it to simulate subclassing, forward arbitrary methods, or advertise a conversion. AsRef, accessors, and explicit domain operations produce more stable contracts.

ToOwned and Cow: defer ownership, do not hide it

ToOwned generalizes cloning a borrowed value into its associated owned form. For str, the owned form is String; for Path, it is PathBuf. Cow<'a, B> holds either Borrowed(&'a B) or Owned(B::Owned) where B: ToOwned.

This is useful when most inputs pass through unchanged but a minority require normalization. The fixture’s path normalization makes the boundary visible:

pub fn with_toml_extension(mut path: Cow<'_, Path>) -> Cow<'_, Path> {
    if path.extension().is_none_or(|extension| extension != "toml") {
        path.to_mut().set_extension("toml");
    }
    path
}

An already-suffixed borrowed path remains borrowed. to_mut clones only when mutation is required. An owned input can be mutated without another clone.

Cow is not automatically faster. It adds a branch and lifetime/API complexity; mutation may allocate at surprising points; callers may immediately require ownership anyway. Use it when profiles show a meaningful pass-through path or when the ownership model itself is valuable. A simple owned return is often easier and sufficiently efficient.

Collection conversions: FromIterator and Extend

Chapter 23 ended at iterator materialization. FromIterator<A> defines how a new collection is built from A; .collect() calls it. Extend<A> appends items to an existing collection. They describe sequence ingestion, not arbitrary value conversion.

A domain collection implementing either trait must decide what repeated keys, ordering, invalid items, and partial mutation mean. Extend cannot return an error, so it is wrong for validation that may reject midway unless items encode failures or all inputs are valid by construction. A named try_extend can preserve transactional or partial-progress semantics.

Implementing FromIterator may make collect::<DomainType>() elegant, but only when construction is unambiguous and infallible. If callers must choose deduplication, capacity, authorization, or conflict policy, use a builder or named constructor.

Redesign the path API three ways

Consider a manifest service with three distinct jobs:

  1. Inspect now: fn is_manifest(path: impl AsRef<Path>) -> bool. It needs a cheap temporary view. Inside a large codebase, normalize immediately to &Path and keep the core concrete.
  2. Retain for later: fn enqueue(path: PathBuf). Ownership and likely allocation are explicit. An optional ergonomic edge can accept impl Into<PathBuf>, but document that conversion may allocate.
  3. Normalize conditionally: fn normalize(path: Cow<'_, Path>) -> Cow<'_, Path>. Borrowed pass-through is preserved, while mutation creates ownership.

Do not combine the three into one maximally generic function. Their lifecycle and cost promises differ. Also resist impl AsRef<str> for filesystem paths: Path supports platform-native representations that are not necessarily Unicode. Converting through text can reject valid paths or encourage lossy handling.

A public API should also define symlink policy, relative-path base, canonicalization, race resistance, and error context when those matter. No standard conversion trait answers those operational questions.

Flexibility stops at security and lifecycle policy

Path conversion is not path authorization. AsRef<Path> preserves platform representation, but it does not prevent traversal components, absolute-path escape, symlink races, device names, or changes between validation and opening. canonicalize performs filesystem work and changes error, permission, and race behavior; it is not a harmless conversion to hide behind From or AsRef.

For untrusted paths, define an allowed root and open policy. Prefer operations relative to a trusted directory handle where the platform and threat model require race resistance. Decide whether symlinks are followed and whether the final object must be a regular file. A retained path is a name, not a stable handle to the same file.

Text conversions have parallel risks. Lossy byte-to-string conversion changes data; normalization can collapse identifiers; case folding is domain-sensitive; arbitrary Into<String> can conceal large allocations. Names such as decode_utf8, normalize_identifier, or parse_region expose policy and provide a place for limits and typed errors.

Trait choice affects maintenance and observability

A public impl AsRef<Path> function may be instantiated for multiple caller types. A concrete &Path internal core gives one implementation body. Normalize once at the ergonomic edge rather than repeating generic parameters through every helper. This improves diagnostics and contains monomorphization without weakening the public view contract.

Conversions also affect logging and cost accounting. A .to_owned() inside a retry loop can dominate costs while remaining visually small. A Cow that becomes owned on every representative request is evidence that the API should likely accept ownership directly. Measure borrowed/owned ratios and error paths instead of assuming clone-on-write wins.

If conversion provenance matters for audit or migration, accept a named request carrying source and policy. Blanket traits intentionally erase that context; trying to recover it through type tricks produces brittle APIs.

Avoid generic “stringly” APIs

impl AsRef<str> is appropriate for transient textual inspection, but it does not create domain meaning. If a function accepts account IDs, regions, algorithms, and SQL fragments all as generic strings, the type system cannot prevent swaps or validate policies at construction.

Introduce validated newtypes for values with invariants. Provide TryFrom<String> when ownership can be reused during validation, FromStr for parsing, as_str for explicit access, and perhaps AsRef<str> when broad textual tooling is genuinely useful. Do not add every conversion pair. Each pair affects inference, coherence, and what users assume is cheap or lossless.

A decision table for review

Caller relationship Trait or form Failure Ownership and cost signal
canonical consumed conversion From<T> / Into<U> none may allocate
canonical checked conversion TryFrom<T> / TryInto<U> typed error consumes input
cheap temporary projection AsRef<T> / AsMut<T> none borrowed and cheap
equality-equivalent key Borrow<Q> none hash/order/equality law
pointer-like target access Deref / DerefMut none broad coercion surface
clone borrowed data on demand Cow<B> / ToOwned allocation possible ownership changes on mutation
build or append from items FromIterator / Extend no trait-level error policy must be unambiguous
domain transformation named method or type explicit name carries policy and cost

A decision map asks what the caller promises: consuming values selects From or TryFrom, cheap views select AsRef or AsMut, equivalent borrowed keys select Borrow, owned-or-borrowed data selects Cow and ToOwned, iterator items select FromIterator or Extend, and Deref is cautioned as smart-pointer behavior rather than general conversion.

Conversion naming should reveal policy:

  • from_ or into_ for one obvious representation transfer;
  • try_from for one obvious checked transfer;
  • as_ for a borrowed view;
  • to_ for producing an owned value, often with work;
  • parse, decode, normalize, resolve, or canonicalize when the operation has domain semantics.

Failure modes that compile

  • Using Into<String> for identifiers that require validation.
  • Accepting AsRef<str> for paths and losing non-Unicode inputs.
  • Implementing Borrow for a field whose equality or hashing differs from the owner.
  • Implementing Deref merely to forward methods or mimic inheritance.
  • Exposing DerefMut and allowing mutation around a wrapper invariant.
  • Assuming From or AsRef means allocation-free.
  • Adding broad blanket conversions that create coherence or inference conflicts.
  • Returning Cow when every caller immediately calls into_owned.
  • Using FromIterator where duplicate or invalid-item policy must be selected.
  • Hiding retention behind a borrowed-looking parameter and cloning internally without documentation.

Exercise: make path ownership reviewable

Redesign an API that accepts P: Into<String> for configuration paths and stores them in a background queue.

  1. Preserve platform-native paths and choose PathBuf at the retention boundary.
  2. Add an inspection helper using &Path internally and optionally impl AsRef<Path> at the public edge.
  3. Implement conditional extension normalization with Cow<Path> and test borrowed pass-through versus clone-on-write.
  4. Compare a generic Into<PathBuf> enqueue edge with an explicit PathBuf edge for allocation visibility, diagnostics, and monomorphization.
  5. Document relative path base, symlink/canonicalization policy, invalid input, and time-of-check/time-of-use behavior.
  6. Explain why Borrow<Path> is or is not appropriate for any keyed lookup, including equality and hashing.

The exercise is complete when each trait bound can be replaced by one sentence about ownership, failure, cost, lifetime, and equivalence—and when removing a bound that lacks such a sentence improves the API.

Review checklist

  • Does conversion consume, borrow, or conditionally acquire ownership?
  • Can it fail, lose information, allocate, or apply policy?
  • Is From genuinely infallible and unsurprising?
  • Is AsRef a cheap projection used only for the borrow?
  • Does Borrow preserve equality, ordering, and hashing?
  • Is Deref exposing true smart-pointer behavior and protected invariants?
  • Does Cow avoid meaningful ownership often enough to justify complexity?
  • Are FromIterator and Extend policies unambiguous and infallible?
  • Could a named constructor communicate domain semantics better?
  • Will a blanket implementation overlap or alter inference downstream?
  • Are path encoding, retention, security, and race policies explicit?

Durable takeaways

Rust’s standard conversion traits are a vocabulary of promises, not interchangeable ergonomic tricks. From and TryFrom consume; AsRef projects; Borrow asserts key equivalence; Deref exposes pointer-like access; ToOwned and Cow negotiate retention; FromIterator and Extend ingest sequences. Narrow APIs become flexible when they express the right promise, not when they accept the greatest number of types.

These choices prepare the advanced type-pattern chapter. Phantom ownership, variance, higher-ranked bounds, and associated relationships are safest when ordinary conversion and view boundaries are already explicit.

Sources and version notes