Read / Write / BufRead Traits
Read / Write / BufRead Traits
Level 4 — Error Handling & Generics The
std::iotraits behind all blocking byte-oriented I/O — files, sockets, stdin/stdout.
1. Prerequisites
Result<T, E>— Every I/O operation can fail, so every method here returnsio::Result<T>.?Operator — The idiomatic way to propagate I/O errors.- Trait Objects (
dyn Trait) —Box<dyn Read>/Box<dyn Write>are common ways to abstract over I/O sources.
2. Term Category
Standard Library Traits (the universal I/O interface): Read, Write, and BufRead are the trio of traits that make byte-oriented input/output generic in Rust. A function written against impl Read works identically whether the actual source is a file, a TCP socket, an in-memory Vec<u8>, or stdin — the caller decides what concrete type to plug in.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Every I/O source — files, sockets, in-memory buffers, standard input — fundamentally does the same two things: you can pull bytes out of it, or push bytes into it. Rather than writing separate file-reading code, socket-reading code, and buffer-reading code, Rust abstracts this into two core traits: Read (has a .read(&mut buf) -> io::Result<usize> method) and Write (has .write(&buf) -> io::Result<usize>). Any function written generically over R: Read or W: Write (or the trait-object forms &mut dyn Read) automatically works with any current or future type that implements them — you write the logic once. BufRead extends Read with line-oriented and buffered convenience methods (.read_line(), .lines()), which require an internal buffer that plain Read doesn't guarantee.
(2) Reality Metaphor
Imagine a universal electrical outlet adapter that works in any country.
Readis a plug shape that says "I can pull power out of any socket that speaks this protocol" — whether the socket is in a wall (a file), a power bank (an in-memory buffer), or a generator (a network stream), the appliance (your code) doesn't need to know or care.Writeis the same idea, reversed: "I can push power into any receptacle that accepts this plug shape."BufReadis an upgraded adapter with a built-in surge protector and a readout screen (internal buffering) that lets you ask higher-level questions like "give me the next full line," instead of managing raw voltage (bytes) yourself.
(3) Rust Code Examples
Short Snippet (Generic Over Any Write)
use std::io::{self, Write};
// This function works with a File, a TcpStream, stdout, or a Vec<u8> — unchanged.
fn log_message(destination: &mut impl Write, msg: &str) -> io::Result<()> {
writeln!(destination, "[LOG] {msg}")
}
fn main() -> io::Result<()> {
let mut buffer: Vec<u8> = Vec::new();
log_message(&mut buffer, "hello from an in-memory buffer")?;
log_message(&mut io::stdout(), "hello from real stdout")?;
println!("captured: {}", String::from_utf8_lossy(&buffer));
Ok(())
}
Fuller Example (Reading Lines with BufRead)
use std::io::{self, BufRead};
fn count_lines(source: impl BufRead) -> io::Result<usize> {
let mut count = 0;
for line in source.lines() {
let _line = line?; // Each line is its own io::Result<String>.
count += 1;
}
Ok(count)
}
fn main() -> io::Result<()> {
let text = "line one\nline two\nline three";
let lines = count_lines(text.as_bytes())?; // &[u8] implements Read; wrap for BufRead.
println!("{lines}"); // 3
Ok(())
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Read Write Bufread Scoping and Lifecycle Rules
The mistake: Assuming Read Write Bufread instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("read_write_bufread_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("read_write_bufread_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Read Write Bufread State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Read Write Bufread through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Read Write Bufread Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Read Write Bufread instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: High-Throughput Zero-Copy Stream Log Analyzer (BufRead::fill_buf & BufRead::consume)
Scenario:
In high-throughput logging systems, log indexers, or streaming data ingestors, allocating a new String or Vec<u8> for every line (as done by BufRead::lines() or BufRead::read_line()) introduces severe heap allocation overhead and memory fragmentation. Production I/O pipelines bypass heap allocation by directly inspecting the internal ring-buffer of a BufRead implementation via fill_buf() and advancing the buffer cursor via consume().
Task:
Implement a generic stream processor ChunkedLogAnalyzer<R> where R: BufRead.
- Define a
LogStatsstruct containing fields:total_lines: usize,non_empty_lines: usize,error_count: usize,warn_count: usize,info_count: usize, andtotal_bytes: usize. - Implement
scan_logs(&mut self) -> io::Result<LogStats>onChunkedLogAnalyzer<R>usingfill_buf()andconsume():- Search the buffer slice directly for
\nline delimiters. - For complete lines residing fully inside the buffer slice, inspect bytes zero-copy without allocating heap memory.
- If a line is split across
fill_buf()buffer chunks, accumulate bytes into a transient fallback vector only for that split line, process it upon finding\n, and clear the vector. - Handle both
\nand\r\nline endings. - Categorize lines starting with
ERRORor[ERROR],WARNor[WARN],INFOor[INFO]. - Correctly process any final line at EOF that lacks a trailing
\n.
- Search the buffer slice directly for
- Include unit tests demonstrating zero-copy execution with
Cursor<&[u8]>andBufReaderwith small capacity, asserting line metrics, severity counts, and byte counters using explicit test assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
use std::io::{self, BufRead, Cursor};
#[derive(Debug, Default, PartialEq, Eq)]
pub struct LogStats {
pub total_lines: usize,
pub non_empty_lines: usize,
pub error_count: usize,
pub warn_count: usize,
pub info_count: usize,
pub total_bytes: usize,
}
pub struct ChunkedLogAnalyzer<R> {
reader: R,
}
impl<R: BufRead> ChunkedLogAnalyzer<R> {
pub fn new(reader: R) -> Self {
Self { reader }
}
pub fn scan_logs(&mut self) -> io::Result<LogStats> {
let mut stats = LogStats::default();
let mut partial_line = Vec::new();
loop {
let buffer = self.reader.fill_buf()?;
if buffer.is_empty() {
// Process final line at EOF if stream ended without trailing '\n'
if !partial_line.is_empty() {
Self::process_line(&partial_line, &mut stats);
partial_line.clear();
}
break;
}
let mut consumed = 0;
let buf_len = buffer.len();
while consumed < buf_len {
let remaining = &buffer[consumed..];
if let Some(newline_pos) = remaining.iter().position(|&b| b == b'\n') {
let line_slice = &remaining[..newline_pos];
if partial_line.is_empty() {
// Zero-copy path: entire line resides within current buffer slice
Self::process_line(line_slice, &mut stats);
} else {
// Stitched line path: append chunk remainder and process
partial_line.extend_from_slice(line_slice);
Self::process_line(&partial_line, &mut stats);
partial_line.clear();
}
consumed += newline_pos + 1; // Advance cursor past '\n'
} else {
// No newline found in remaining slice; accumulate into transient buffer
partial_line.extend_from_slice(remaining);
consumed += remaining.len();
}
}
self.reader.consume(consumed);
}
Ok(stats)
}
fn process_line(raw_line: &[u8], stats: &mut LogStats) {
// Strip Windows carriage return '\r' if present
let line = if raw_line.ends_with(b"\r") {
&raw_line[..raw_line.len() - 1]
} else {
raw_line
};
stats.total_lines += 1;
stats.total_bytes += line.len();
if line.is_empty() {
return;
}
stats.non_empty_lines += 1;
if line.starts_with(b"ERROR") || line.starts_with(b"[ERROR]") {
stats.error_count += 1;
} else if line.starts_with(b"WARN") || line.starts_with(b"[WARN]") {
stats.warn_count += 1;
} else if line.starts_with(b"INFO") || line.starts_with(b"[INFO]") {
stats.info_count += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::BufReader;
#[test]
fn test_zero_copy_log_scanner_basic() {
let log_data = "INFO: System started\nWARN: High memory usage\nERROR: Out of disk space\n";
let cursor = Cursor::new(log_data);
let mut analyzer = ChunkedLogAnalyzer::new(cursor);
let stats = analyzer.scan_logs().unwrap();
assert_eq!(stats.total_lines, 3);
assert_eq!(stats.non_empty_lines, 3);
assert_eq!(stats.info_count, 1);
assert_eq!(stats.warn_count, 1);
assert_eq!(stats.error_count, 1);
assert_ne!(stats.total_bytes, 0);
}
#[test]
fn test_log_scanner_chunked_and_no_trailing_newline() {
let log_data = "[INFO] boot\n[WARN] temp high\n[ERROR] crash";
let cursor = Cursor::new(log_data);
// Intentionally small 8-byte buffer capacity forces multi-chunk split lines
let buffered = BufReader::with_capacity(8, cursor);
let mut analyzer = ChunkedLogAnalyzer::new(buffered);
let stats = analyzer.scan_logs().unwrap();
assert_eq!(stats.total_lines, 3);
assert_eq!(stats.non_empty_lines, 3);
assert_eq!(stats.info_count, 1);
assert_eq!(stats.warn_count, 1);
assert_eq!(stats.error_count, 1);
assert!(matches!(stats.error_count, 1));
}
#[test]
fn test_log_scanner_empty_and_whitespace() {
let log_data = "\n\r\nINFO ok\n";
let cursor = Cursor::new(log_data);
let mut analyzer = ChunkedLogAnalyzer::new(cursor);
let stats = analyzer.scan_logs().unwrap();
assert_eq!(stats.total_lines, 3);
assert_eq!(stats.non_empty_lines, 1);
assert_eq!(stats.info_count, 1);
assert_eq!(stats.warn_count, 0);
}
}
Technical Explanation
- Zero-Copy Inspection Mechanics:
- Standard
BufRead::lines()returns an iterator yieldingio::Result<String>, which forces heap allocation (String::with_capacity) for every line. - Calling
reader.fill_buf()accesses the internal buffer owned byRas a borrowed byte slice&[u8]. Because the data resides inside the buffer already read from OS file descriptors or network interfaces, we scan for\nwithout transferring memory into an intermediate owned container.
- Standard
- Buffer Lifecycle & Cursor Advancement:
fill_buf()returns&[u8]tied to the lifetime of&mut self.reader. Rust's borrow checker prevents calling any mutating methods onself.readerwhile holdingbuffer.consume(bytes)informs the underlying reader how many bytes were processed. On the next iteration,fill_buf()returns a fresh slice starting at the updated internal pointer.
- Handling Split Line Boundaries & EOF:
- When a line is longer than the buffer chunk size,
position(|&b| b == b'\n')returnsNone. The partial chunk is copied intopartial_line(transient allocation only for split lines). Once the delimiter\nis found in a subsequent chunk, the head slice is combined withpartial_line, processed, and cleared. - If
fill_buf()returns an empty slice&[], EOF has been reached. Ifpartial_linestill contains un-delimited bytes, it represents a line ending abruptly without\n, which is processed before exiting.
- When a line is longer than the buffer chunk size,
- Generic Monomorphization (
impl<R: BufRead>):- The implementation uses static dispatch generic
R: BufRead. Inlining by LLVM eliminates virtual call overhead forfill_buf()andconsume(), achieving native binary performance matching manual C buffer pointers.
- The implementation uses static dispatch generic
Exercise 2: Production Multi-Writer Tee & Inline Hashing Engine (std::io::Write)
Scenario: In storage proxies, cloud object storage splitters, or streaming replication engines, data written to a primary destination (e.g. disk file or TCP socket) must simultaneously be mirrored to a secondary stream (e.g. audit log or replica node) while computing an inline integrity digest (such as FNV-1a or CRC32) without storing the entire payload in memory.
Task:
Implement a generic dual-writer struct HashingTeeWriter<W1, W2> where W1: Write and W2: Write.
- Fields:
primary: W1,secondary: W2,checksum: u64(initialized using FNV offset basis0xcbf29ce484222325). - Implement
std::io::WriteforHashingTeeWriter<W1, W2>:write(&mut self, buf: &[u8]) -> io::Result<usize>: Writes the complete slice toprimaryviawrite_all(buf)and then tosecondaryviawrite_all(buf). If either write returns anio::Error, return that error immediately. Update the cumulative FNV-1a hash (hash = (hash ^ (byte as u64)).wrapping_mul(0x100000001b3)). ReturnOk(buf.len()).flush(&mut self) -> io::Result<()>: Flush both sinks sequentially, propagating errors if either fails.
- Provide helper methods:
new(primary: W1, secondary: W2) -> Self,checksum(&self) -> u64, andinto_inner(self) -> (W1, W2). - Include unit tests demonstrating tee operations into dual
Vec<u8>buffers, verifying byte output equality, hash accumulation across multiple writes, error propagation when a writer fails, using explicit test assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
use std::io::{self, Write};
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
pub struct HashingTeeWriter<W1, W2> {
primary: W1,
secondary: W2,
checksum: u64,
}
impl<W1: Write, W2: Write> HashingTeeWriter<W1, W2> {
pub fn new(primary: W1, secondary: W2) -> Self {
Self {
primary,
secondary,
checksum: FNV_OFFSET_BASIS,
}
}
pub fn checksum(&self) -> u64 {
self.checksum
}
pub fn into_inner(self) -> (W1, W2) {
(self.primary, self.secondary)
}
fn update_checksum(&mut self, bytes: &[u8]) {
for &byte in bytes {
self.checksum ^= byte as u64;
self.checksum = self.checksum.wrapping_mul(FNV_PRIME);
}
}
}
impl<W1: Write, W2: Write> Write for HashingTeeWriter<W1, W2> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
// Guarantee full delivery to both tee sinks before updating state
self.primary.write_all(buf)?;
self.secondary.write_all(buf)?;
self.update_checksum(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.primary.flush()?;
self.secondary.flush()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::WriteZero, "disk full failure"))
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Other, "flush failed"))
}
}
#[test]
fn test_tee_writer_dual_output_and_checksum() {
let primary_buf = Vec::new();
let secondary_buf = Vec::new();
let mut tee = HashingTeeWriter::new(primary_buf, secondary_buf);
let written = tee.write(b"Hello, Rust I/O!").unwrap();
tee.flush().unwrap();
assert_eq!(written, 16);
assert_ne!(tee.checksum(), FNV_OFFSET_BASIS);
let initial_checksum = tee.checksum();
tee.write_all(b" Streaming chunk.").unwrap();
assert_ne!(tee.checksum(), initial_checksum);
let (primary, secondary) = tee.into_inner();
assert_eq!(primary, b"Hello, Rust I/O! Streaming chunk.");
assert_eq!(secondary, b"Hello, Rust I/O! Streaming chunk.");
}
#[test]
fn test_tee_writer_failure_propagation() {
let primary_buf = Vec::new();
let failing = FailingWriter;
let mut tee = HashingTeeWriter::new(primary_buf, failing);
let res = tee.write(b"test data");
assert!(res.is_err());
assert!(matches!(
res.unwrap_err().kind(),
io::ErrorKind::WriteZero
));
}
}
Technical Explanation
- Short Write Prevention (
write_allvswrite):- The raw
Write::writemethod is allowed to perform a short write (writing fewer bytes than requested and returningOk(n)). Ifprimary.write(buf)wrote 5 bytes whilesecondary.write(buf)wrote 10 bytes, state synchronization between sinks would break. HashingTeeWriteruseswrite_all(buf), which loops internally until the entire buffer is written or an unrecoverable error occurs, guaranteeing atomic parity across both output streams.
- The raw
- Trait Implementation Invariants:
- Implements standard
std::io::Writecontract, makingHashingTeeWriterfully composable withstd::io::BufWriter,flate2::write::GzEncoder, or any other standard writer wrapper. - The
into_inner(self)method consumesselfby value, transferring ownership of both underlying writersW1andW2back to the caller without unnecessary heap allocations.
- Implements standard
- Error Propagation & Partial Writes:
- If
primary.write_all(buf)fails withio::Error, the?operator immediately returns early before modifyingsecondaryor mutatingchecksum. This preserves structural invariants and avoids computing checksums for incomplete or corrupted payloads.
- If
Exercise 3: Stream Transformation Copy Pipeline over Trait Objects (Box<dyn BufRead> & Box<dyn Write>)
Scenario:
In cloud streaming microservices and dynamic plugin architectures, input streams and target output sinks are often not known at compile time. Data pipelines receive heterogeneous sources wrapped as dynamic trait objects (Box<dyn BufRead> and Box<dyn Write>), transform payload chunks via stream processors (e.g. byte encryption, format transcoding, or header insertion), and stream output efficiently using fixed stack/heap transfer buffers.
Task:
Implement StreamTransformPipeline handling dynamic trait object streams.
- Define
StreamTransformPipelineholding:reader: Box<dyn BufRead>writer: Box<dyn Write>chunk_size: usize
- Implement methods:
pub fn new(reader: Box<dyn BufRead>, writer: Box<dyn Write>, chunk_size: usize) -> Selfpub fn process<F>(&mut self, mut transform: F) -> io::Result<usize>whereF: FnMut(&[u8]) -> Vec<u8>:- Read chunks of up to
chunk_sizebytes into a intermediate buffer usingself.reader.read(&mut buffer). - When
readreturns0, break (EOF reached). - Apply
transformto the read slice&buffer[..bytes_read]. - Write transformed bytes to
self.writer.write_all(&transformed). - Track total output bytes written and call
self.writer.flush()before returningOk(total_output_bytes).
- Read chunks of up to
- Write unit tests utilizing dynamic dispatch (
Box<dyn BufRead>wrappingBufReader<Cursor<&[u8]>>andBox<dyn Write>), verifying chunked processing, byte transformations, custom thread-safe shared sinks, using explicit test assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
use std::io::{self, BufRead, Write};
pub struct StreamTransformPipeline {
reader: Box<dyn BufRead>,
writer: Box<dyn Write>,
chunk_size: usize,
}
impl StreamTransformPipeline {
pub fn new(reader: Box<dyn BufRead>, writer: Box<dyn Write>, chunk_size: usize) -> Self {
let chunk_size = if chunk_size == 0 { 8192 } else { chunk_size };
Self {
reader,
writer,
chunk_size,
}
}
pub fn process<F>(&mut self, mut transform: F) -> io::Result<usize>
where
F: FnMut(&[u8]) -> Vec<u8>,
{
let mut buffer = vec![0u8; self.chunk_size];
let mut total_output_bytes = 0;
loop {
let bytes_read = self.reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
let transformed = transform(&buffer[..bytes_read]);
self.writer.write_all(&transformed)?;
total_output_bytes += transformed.len();
}
self.writer.flush()?;
Ok(total_output_bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{BufReader, Cursor};
use std::sync::{Arc, Mutex};
#[test]
fn test_pipeline_trait_objects_and_transformation() {
let input_data = b"abcdefghijklmnopqrstuvwxyz";
let reader: Box<dyn BufRead> = Box::new(BufReader::new(Cursor::new(input_data)));
let output_vec: Vec<u8> = Vec::new();
let writer: Box<dyn Write> = Box::new(output_vec);
// 5-byte chunk size tests multi-iteration execution
let mut pipeline = StreamTransformPipeline::new(reader, writer, 5);
let total_written = pipeline
.process(|chunk| chunk.iter().map(|b| b.to_ascii_uppercase()).collect())
.unwrap();
assert_eq!(total_written, 26);
assert_ne!(total_written, 0);
}
#[test]
fn test_pipeline_custom_sink_and_assertions() {
#[derive(Clone)]
struct SharedSink(Arc<Mutex<Vec<u8>>>);
impl Write for SharedSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let shared_output = Arc::new(Mutex::new(Vec::new()));
let sink = SharedSink(shared_output.clone());
let input_data = b"Hello World";
let reader: Box<dyn BufRead> = Box::new(BufReader::new(Cursor::new(input_data)));
let writer: Box<dyn Write> = Box::new(sink);
let mut pipeline = StreamTransformPipeline::new(reader, writer, 4);
let res = pipeline.process(|chunk| {
let mut v = Vec::new();
for &b in chunk {
v.push(b);
v.push(b); // Duplicate every byte
}
v
});
assert!(res.is_ok());
assert_eq!(res.unwrap(), 22);
let output_bytes = shared_output.lock().unwrap().clone();
assert_eq!(output_bytes, b"HHeelllloo WWoorrlldd");
assert!(matches!(output_bytes.len(), 22));
}
}
Technical Explanation
- Dynamic Dispatch (
dyn Trait) vs Static Dispatch:- By accepting
Box<dyn BufRead>andBox<dyn Write>,StreamTransformPipelineuses dynamic dispatch (vtables) to invokeread,write, andflush. - This allows runtime flexibility: plugins can inject network streams, file streams, or memory buffers into the exact same pipeline struct instance without requiring separate monomorphized type instantiations.
- By accepting
- Chunked Stream Buffer Mechanics:
- Reusing a single
Vec<u8>buffer of sizechunk_sizeacross all iterations prevents repeated allocations during streaming. self.reader.read(&mut buffer)returnsOk(0)exclusively on EOF. The pipeline carefully slices&buffer[..bytes_read]before passing data to the transformation closure to avoid processing leftover uninitialized or stale data from previous chunk iterations.
- Reusing a single
- Higher-Order Stream Transformation (
FnMut):- The
processmethod takes a genericFnMut(&[u8]) -> Vec<u8>closure. UsingFnMutpermits stateful transformations (e.g. keeping count of bytes, sliding window ciphers, or running compression state) across sequential chunk invocations.
- The
6. Related Terms
?Operator — The idiomatic propagation tool for theio::Result<T>every method here returns.- The Rust Standard Library (
std) —Read/Write/BufReadare specifically part of the OS-integration layer that onlystd(notcore/alloc) provides. - Trait Objects (
dyn Trait) —Box<dyn Read>/&mut dyn Writeare common ways to store a heterogeneous I/O source/sink. tokio— The async counterpart (AsyncRead/AsyncWrite) for non-blocking I/O.
7. Key Takeaways
ReadandWriteare the universal, generic byte-I/O traits — a single function written against them works with files, sockets, and in-memory buffers alike.BufReadextendsReadwith buffered, line-oriented convenience methods like.lines()and.read_line()..read()may perform a short read — use.read_exact()or.read_to_end()/.read_to_string()when you need guaranteed-complete reads.- Every method returns
io::Result<T>, making the?operator the natural way to write I/O code.