In python, I might have a class that looks like this:
class ClientObject(object):
def __init__(self):
connection = None
connected = False
def connect(self):
self.connection = new Connection('server')
self.connected = True
def disconnect(self):
self.connection.close()
self.connection = None
self.connected = False
I am trying to do something similar in rust. First off, I question if this is even a good pattern in rust--would you implement a client class with a connection this way? Second, I am getting an error I don't understand with my implementation.
pub struct Client {
seq: int,
connected: bool,
socket: Option<UdpSocket>
}
impl Client {
pub fn connect(&mut self, addr: &SocketAddr) -> ClientConnectionResult {
match self.socket {
Some(_) => self.disconnect(),
None => ()
};
self.socket = match UdpSocket::bind(*addr) {
Ok(s) => Some(s),
Err(e) => return Err(to_client_error(e))
};
self.connected = true;
Ok(())
}
pub fn disconnect(&mut self) {
match self.socket {
None => (),
Some(s) => drop(s)
};
self.socket = None;
self.connected = false;
}
}
In the disconnect function, the match generates a compile error because it attempts to move the ownership of self.socket. What I want to do is to set self.socket to None and allow it to be later reassigned to something if connect is called. How would I do that?
It may be not applicable for your use case, but Rust move semantics and strong typing allow cheap "state machines", when a method consumes self
(current state) and returns another object which represents another state. That's how TcpListener
is implemented: it has listen()
method which returns TcpAcceptor
, consuming original listener in process. This approach has advantage of typing: you just cannot call methods which does not make sense when the object is in invalid state.
In your case it could look like this:
use std::kinds::markers::NoCopy;
pub struct DisconnectedClient {
seq: int,
_no_copy: NoCopy
}
impl DisconnectedClient {
#[inline]
pub fn new(seq: int) -> DisconnectedClient {
DisconnectedClient { seq: seq, _no_copy: NoCopy }
}
// DisconnectedClient does not implement Copy due to NoCopy marker so you need
// to return it back in case of error, together with that error, otherwise
// it is consumed and can't be used again.
pub fn connect(self, addr: &SocketAddr) -> Result<ConnectedClient, (DisconnectedClient, IoError)> {
match UdpSocket::bind(*addr) {
Ok(s) => Ok(ConnectedClient { seq: self.seq, socket: s }),
Err(e) => Err((self, e))
}
}
}
pub struct ConnectedClient {
seq: int,
socket: UdpSocket
}
impl ConnectedClient {
#[inline]
pub fn disconnect(self) -> DisconnectedClient {
// self.socket will be dropped here
DisconnectedClient::new(self.seq)
}
// all operations on active connection are defined here
}
You first create DisconnectedClient
using DisconnectedClient::new()
method. Then, when you want to connect to something, you use connect()
method, which consumes DisconnectedClient
and returns new object, ConnectedClient
, which represents an established connection. When you finished working with this connection, disconnect()
method turns ConnectedClient
back to DisconnectedClient
.
This approach may be more complex, but it has advantage of static checks for incorrect state. You don't have to have connected()
-like methods; if your variable is of type ConnectedClient
, then you already know it is connected.
If in your Python code self.connected
is equivalent to self.connection is not None
, then the boolean is a complete waste of time and space; you’d be best omitting it or if it is valuable having it as a property:
@property
def connected(self):
return self.connection is not None
Similarly in Rust you can go storing the boolean separately, but there is absolutely no point.
impl Client {
pub fn connect(&mut self, addr: &SocketAddr) -> ClientConnectionResult {
self.disconnect();
self.socket = Some(try!(UdpSocket::bind(*addr)));
Ok(())
}
pub fn disconnect(&mut self) {
self.socket = None;
}
}
As far as the moving self.socket
part is concerned, you can’t really do it the way you’ve done it there, because it is moving it. You must put something in its place. This can be achieved with something like .take()
.
drop
is unnecessary; destructors are called when the value goes out of scope or is otherwise replaced, which self.socket = None
will cause to happen. Similarly, because disconnect()
works fine if socket
is already None
there is no need for an if self.socket.is_some()
check (or equivalent pattern matching) around the call of self.disconnect()
in connect
.
If you want someone to be able to check whether it’s connected, make a simple method for it:
pub fn connected(&self) -> bool {
self.socket.is_some()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With