Created
January 18, 2024 23:07
-
-
Save graydon/42778d8efdf1d3f1ae405e37086c4ef8 to your computer and use it in GitHub Desktop.
Ownership passing vs. borrowing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is just an elaboration of an off-hand toot I made earlier today | |
// concerning a coding pattern I find myself doing whenever possible: passing | |
// and returning owned values instead of borrowing references. | |
// | |
// I find it works well for long-lived values especially since the resulting | |
// composite objects have no lifetime qualifiers, so I don't have to plumb | |
// lifetimes through to all the code that uses them. | |
// Assumption: assume we have a few objects like this: a network connection, a | |
// database, some commands, etc. and we want to make a session type that uses | |
// both the network and the database... | |
use std::{collections::HashMap, net::TcpStream, io::Read, env}; | |
enum Command { | |
Get { key: String }, | |
Set { key: String, value: String }, | |
} | |
#[derive(Default)] | |
struct Database { | |
data: HashMap<String,String>, | |
} | |
struct Network { | |
stream: TcpStream, | |
} | |
impl Network { | |
fn new(stream: TcpStream) -> Self { | |
Self { stream } | |
} | |
fn next_command(&mut self) -> Option<Command> { | |
let mut buffer = [b' '; 64]; | |
if self.stream.read(&mut buffer).is_err() { | |
return None; | |
} | |
let Ok(buffer) = std::str::from_utf8(&buffer) else { return None }; | |
let parts: Vec<&str> = buffer.trim().splitn(3, ' ').collect(); | |
match &parts[..] { | |
["GET", k] => { | |
Some(Command::Get { key: k.to_string() }) | |
} | |
["SET", k, v] => { | |
Some(Command::Set { key: k.to_string(), value: v.to_string() }) | |
} | |
_ => None | |
} | |
} | |
} | |
// Now we compare two approaches to composing these objects into a session. | |
// Approach 1: use borrows for composition | |
mod borrow { | |
use std::io::Write; | |
use super::*; | |
pub struct Session<'db, 'net> { | |
database: &'db mut Database, | |
network: &'net mut Network, | |
} | |
impl<'db, 'net> Session<'db, 'net> { | |
pub fn new(database: &'db mut Database, network: &'net mut Network) -> Self { | |
Self { database, network } | |
} | |
pub fn process_commands(&mut self) { | |
loop { | |
match self.network.next_command() { | |
Some(Command::Get { key }) => { | |
if let Some(value) = self.database.data.get(&key) { | |
self.network.stream.write_all(value.as_bytes()).unwrap(); | |
} else { | |
self.network.stream.write_all(b"NOT_FOUND").unwrap(); | |
} | |
} | |
Some(Command::Set { key, value }) => { | |
self.database.data.insert(key, value); | |
self.network.stream.write_all(b"OK").unwrap(); | |
} | |
None => return, | |
} | |
} | |
} | |
} | |
} | |
// Approach 2: use owned values for composition | |
mod owned { | |
use std::io::Write; | |
use super::*; | |
pub struct Session { | |
database: Database, | |
network: Network, | |
} | |
impl Session { | |
pub fn new(database: Database, network: Network) -> Self { | |
Self { database, network } | |
} | |
pub fn process_commands(&mut self) { | |
loop { | |
match self.network.next_command() { | |
Some(Command::Get { key }) => { | |
if let Some(value) = self.database.data.get(&key) { | |
self.network.stream.write_all(value.as_bytes()).unwrap(); | |
} else { | |
self.network.stream.write_all(b"NOT_FOUND").unwrap(); | |
} | |
} | |
Some(Command::Set { key, value }) => { | |
self.database.data.insert(key, value); | |
self.network.stream.write_all(b"OK").unwrap(); | |
} | |
None => return, | |
} | |
} | |
} | |
pub fn finish(self) -> (Database, Network) { | |
(self.database, self.network) | |
} | |
} | |
} | |
// The two approaches are nearly identical in method bodies. They both _have | |
// exclusive ownership_ of the resources they're using, but one has taken that | |
// ownership in the form of a somewhat pointlessly noisy `&mut` along with | |
// lifetime qualifiers on the struct, its fields and impl, and the other has | |
// taken ownership in the form of a value moved-in that must later be moved-out | |
// to recover it. Given the option, I find I often prefer writing the one | |
// additional `finish` function to recover the values I want to move-out, and | |
// have no `&mut` in sight. | |
fn main() { | |
let mut database = Database::default(); | |
let mut network = Network::new(TcpStream::connect("localhost:8080").unwrap()); | |
if env::args().any(|a| a == "--borrowed") { | |
let mut session = borrow::Session::new(&mut database, &mut network); | |
session.process_commands(); | |
} else { | |
let mut session = owned::Session::new(database, network); | |
session.process_commands(); | |
let (database, network) = session.finish(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment