NotDefine.dev

Defining the Undefined. Exploring system design, performance, and memory safety with Go and Rust.

into_inner in Rust

A pattern used in Rust, mainly in the standard library, is into_inner. What does this pattern allow? When is it used?

Imagine having a struct that wraps some data, for example, a map. You can interact with it through methods defined on the struct, which can enforce validation or limit concurrent access. But at some point you no longer need any of that, you just need the raw data. That’s when you can decide to unwrap it and use it directly, bypassing the struct’s methods entirely.

Take a look at this example:

use std::collections::HashMap;

struct KeyValueStore {
    data: HashMap<String, String>,
}

impl KeyValueStore {
    fn new() -> Self {
        Self {
            data: HashMap::new(),
        }
    }

    // Inserts a key-value pair, returning an error if the key is empty
    fn insert(&mut self, key: String, value: String) -> Result<(), String> {
        if key.is_empty() {
            return Err("Key cannot be empty".to_string());
        }
        self.data.insert(key, value);
        Ok(())
    }

    // Returns a clone of the value associated with the key, or None
    fn get(&self, key: &str) -> Option<String> {
        self.data.get(key).cloned()
    }

    // Removes and returns the value associated with the key, or None
    fn remove(&mut self, key: &str) -> Option<String> {
        self.data.remove(key)
    }

    // Full pattern: "inner" method trio

    /// Immutable borrow of the inner HashMap
    fn inner(&self) -> &HashMap<String, String> {
        &self.data
    }

    /// Mutable borrow of the inner HashMap
    fn inner_mut(&mut self) -> &mut HashMap<String, String> {
        &mut self.data
    }

    /// Consumes the KeyValueStore and returns the inner HashMap
    fn into_inner(self) -> HashMap<String, String> {
        self.data
    }
}

fn main() {
    println!("=== KeyValueStore: inner pattern ===\n");

    let mut store = KeyValueStore::new();

    // The wrapper validates — you cannot bypass it via insert()
    match store.insert("name".to_string(), "Alice".to_string()) {
        Ok(_) => println!("Inserted 'name'"),
        Err(e) => println!("Error: {}", e),
    }
    match store.insert("".to_string(), "invalid".to_string()) {
        Ok(_) => println!("Inserted"),
        Err(e) => println!("Error: {}", e), // Key cannot be empty
    }

    // inner() — read without consuming the store
    println!("\nUsing inner():");
    println!("Keys: {}", store.inner().len());
    println!("Contains 'name': {}", store.inner().contains_key("name"));

    // inner_mut() — intentionally bypasses validation
    println!("\nUsing inner_mut():");
    store
        .inner_mut()
        .insert("".to_string(), "bypass".to_string());
    println!("Inserted empty key via inner_mut() — validation bypassed");

    // into_inner() — consumes the wrapper, returns the raw HashMap
    println!("\nUsing into_inner():");
    let raw = store.into_inner();
    println!("Raw HashMap: {:?}", raw);

    // store is consumed
    // store.get("name"); // ERROR
    println!("\nstore consumed, use raw HashMap directly");
}

We have three “inner” methods:

  • inner() provides immutable (read-only) access to the inner data. It uses an immutable borrow (&self), so multiple callers can hold a reference at the same time.
  • inner_mut() provides mutable (read/write) access to the inner data. It uses a mutable borrow (&mut self), so only one caller can hold a reference at a time; Rust enforces this at compile time.
  • into_inner() consumes the struct and returns the inner HashMap. Once called, it is no longer possible to use the struct’s methods, because the wrapper no longer exists.

into_inner takes ownership of the wrapper and moves the inner data out. This is only possible when no other references to the wrapper exist, which Rust enforces at compile time.

To see a real-world example of this pattern, take a look at std::io::BufWriter.

When into_inner is a bad practice

into_inner is not always the right choice. Here are the cases where you should avoid it.

You only need to read the data Use inner() instead. Consuming the wrapper just to inspect a value makes no sense. It also signals to the reader that the wrapper is gone for good.

You break the wrapper’s invariants: The wrapper exists for a reason: validation, ordering, thread safety. Once you call into_inner, those guarantees are gone. If you put the raw data into a new wrapper, you may start with an inconsistent state.

You use it inside a loop: If you find yourself consuming and reconstructing a wrapper on every iteration, something is wrong with the design. Revisit the abstraction.

The wrapper has a meaningful Drop: This is the dangerous one. If the wrapper flushes a buffer, closes a connection, or releases a lock on drop, into_inner may skip all of that.

std::io::BufWriter is the classic example. Calling into_inner() returns the underlying writer but does not flush the buffer. Data still buffered is lost silently.

// NOOOO! buffer may not be flushed
let file = writer.into_inner().unwrap();

// flush first
writer.flush()?;
let file = writer.into_inner().unwrap();

Leave a Reply

Your email address will not be published. Required fields are marked *