ademar111190
ademar111190

Reputation: 14505

Is there any way to have a safe cast knowing all possible types at compile time?

I'm struggling with how to safely cast an argument and make sure all possibilities are handled.

As I have a Kotlin background, I'm writing samples in that language:

package question

class StateA(val message: String)
class StateB(val number: Int)
object StateC

fun <State> render(state: State) = when (state) {
    is StateA -> println("state is StateA, its message: ${state.message}")
    is StateB -> println("state is StateB, its number: ${state.number}")
    StateC -> println("state is StateC")
    else -> println("Unknown type")
}

fun main() {
    render(StateA("Hello world")) // prints: state is StateA, its message: Hello world
    render(StateB(42)) // prints: state is StateB, its number: 42
    render(StateC) // prints: state is StateC
    render("Other type") // prints: Unknown type
}

The when statement checks the generic type State and makes a safe cast when the type matches so I can access message if the type is StateA or I can access number if the state is an instance of StateB.

That code has an issue, the else branch, but I can get rid of it by using a language feature called sealed class and with that I know all possible types at compile time:

package question

import question.State.*

sealed class State {
    class StateA(val message: String) : State()
    class StateB(val number: Int) : State()
    object StateC : State()
}

fun render(state: State) = when (state) {
    is StateA -> println("state is StateA, its message: ${state.message}")
    is StateB -> println("state is StateB, its number: ${state.number}")
    StateC -> println("state is StateC")
}

fun main() {
    render(StateA("Hello world")) // prints: state is StateA, its message: Hello world
    render(StateB(42)) // prints: state is StateB, its number: 42
    render(StateC) // prints: state is StateC
}

The closest I got in Rust was using Any:

use std::any::Any;

struct StateA {
    message: String,
}
struct StateB {
    number: u8,
}
struct StateC {}

fn render(state: &dyn Any) {
    if let Some(state_a) = state.downcast_ref::<StateA>() {
        println!("state is StateA, its message: {}", state_a.message);
    } else if let Some(state_b) = state.downcast_ref::<StateB>() {
        println!("state is StateB, its number: {}", state_b.number);
    } else if let Some(_) = state.downcast_ref::<StateC>() {
        println!("state is StateC");
    } else {
        println!("Unknown type")
    }
}

fn main() {
    render(&StateA {
        message: String::from("Hello World"),
    }); // prints: state is StateA, its message: Hello World
    render(&StateB { number: 42 }); // prints: state is StateB, its number: 42
    render(&StateC {}); // prints: state is StateC
    render(&"Other type"); // prints: Unknown type
}

This solution is similar to the first one in Kotlin, so it has the same issue, the else branch

Is there any way to have a safe cast knowing all possible types at compile time? Something similar to Kotlin's sealed class solution, and get rid of the else branch?

Upvotes: 0

Views: 48

Answers (1)

ademar111190
ademar111190

Reputation: 14505

Using Shepmaster's tip to use enums, I managed to solve the issue.

The working code:

enum State {
    StateA { message: String },
    StateB { number: u8 },
    StateC,
}

fn render(state: &State) {
    match state {
        State::StateA { message } => println!("state is StateA, its message: {}", message),
        State::StateB { number } => println!("state is StateB, its number: {}", number),
        State::StateC => println!("state is StateC"),
    }
}

fn main() {
    render(&State::StateA {
        message: String::from("Hello World"),
    }); // prints: state is StateA, its message: Hello World
    render(&State::StateB { number: 42 }); // prints: state is StateB, its number: 42
    render(&State::StateC {}); // prints: state is StateC
}

Upvotes: 2

Related Questions