-
Notifications
You must be signed in to change notification settings - Fork 4
Rust for ActionScript developers
This ActionScript 3 parser is written in the Rust language, so all its modules are structured in a way specific to the Rust language.
This article will highlight main differences you might see from using a parser written in Java or ActionScript 3 and a parser written in Rust.
Cargo is the package manager integrated in the Rust installation.
When you publish your Cargo package to the public (the crates.io website), documentation is automatically generated by the docs.rs service.
Rust features a type model that includes ownership rules.
The types &str
and String
are used to represent UTF-8 encoded text. The difference between them is:
-
&str
is a slice of an existing string somewhere in memory. It is a "borrow", or a reference. -
String
is a string you own.
Rust implicitly converts &String
to &str
, where &String
is a borrow of a String
.
You may convert a &str
to a String
with .into()
or .to_owned()
.
In Rust, a variable whose type does not implement Copy
is moved out of scope once it is passed to another function or variable.
Types that do not implement Copy
include String
, shared pointers such as Rc<T>
and Box<T>
and in general "owned" types.
To avoid moving unintentionally, you either borrow (&x
) or clone (x.clone()
) the variable.
let x = String::new();
let y = x;
let z = x; // ERROR
Borrow types, or reference "ref" types, are interchangeable terms in Rust. The borrow type &'my_lifetime T
, or simply &T
, represents a reference to data T
at the lifetime 'my_lifetime
.
Lifetimes are inferred or explicitly created in most places of Rust programs, thus omitted.
Note that the 'static
lifetime is reserved and has a special meaning: the entire execution of the program.
fn m1<'my_lifetime>(argument: &'my_lifetime str) {}
// Equivalent
fn m2(argument: &str) {}
There are "immutable" borrows and "mutable" borrows (&'my_lifetime mut T
, or simply &mut T
)
Variables are immutable by default. mut
is used to indicate they are immutable.
let x = 0;
let mut x = 0;
Based on explanations from the Rust community:
- When a variable is immutable, it is transitively immutable.
- When a variable is mutable, it is transitively mutable.
A mutable variable may be freely used as immutable variable. For example, &mut T
implicitly converts to &T
.
There are cases where the Rust's mutability concept is limited. You may sometimes use Cell<T>
or RefCell<T>
instead to contain your T
data.
- Use
Cell
for stack resources. - Use
RefCell
for heap resources.
use std::cell::Cell;
struct ExampleA {
x: Cell<f64>,
}
impl ExampleA {
pub fn new() -> Self {
Self {
x: Cell::new(0.0),
}
}
pub fn x(&self) -> f64 {
self.x.get()
}
}
There are several ways to do pattern matching.
let group: (f64, f64) = (0.0, 0.0);
let (x, y) = group;
#[derive(Copy, Clone)]
enum E1 {
M1(f64),
M2,
}
let m1 = E1::M1(0.0);
let E1::M1(x) = m1 else {
panic!();
};
if let E1::M1(x) = m1 {
println!("{x}");
}
match m1 {
E1::M1(x) => {
},
E1::M2 => {
},
}
let q: bool = matches!(m1, E1::M1(_));
When you need to match a pattern in a Rc<T>
type (that is, a reference counted type), you need to call .as_ref()
in the Rc
value.
// m1: Rc<E1>
match m1.as_ref() {
_ => {},
}
Rust favours composition over inheritance. Further, there is no equivalent of the Java classes in Rust.
The programming patterns to use instead of inheritance where it is required vary:
- Use a structure nested in another structure
struct ExampleA {
example_b: Option<Box<ExampleA>>,
}
struct ExampleB(f64);
- Use an enumeration whose variants may contain additional data
enum Example {
A(f64),
B {
x: f64,
y: f64,
},
}
- Use a trait. Traits are non opaque data types that may be implemented by opaque types (
struct
,enum
, and primitive types). They are similiar to Java interfaces in certain ways.
trait ExampleTrait {
fn common(&self) -> f64;
fn always_provided(&self) {
println!("Foo");
}
}
struct Example1;
impl ExampleTrait for Example1 {
fn common(&self) -> f64 {
0.0
}
}
Rc<T>
(use std::rc::Rc;
) represents a reference to T
managed by reference counting.
Box<T>
represents a heap reference to T
, similiar to C++'s unique_ptr
.
Traits may be automatically implemented according to a structure or enumeration through the #[derive(...)]
attribute.
struct ExampleStruct(f64);
impl ExampleStruct {
pub fn method(&self) {
println!("example_struct.0 = {}", self.0);
}
}
let o = ExampleStruct(10.0);
o.method(); // example_struct.0 = 10.0
Unchecked exceptions that you arbitrarily throw are not a thing in Rust.
In Rust, there are "panics" (fatal exceptions or crashes) and functions that return Result<T, E>
, where E
is the error type and T
is the result data type.
fn function_that_panics() {
panic!("Panic message");
}
enum MyError {
Common,
}
fn function_that_throws() -> Result<f64, MyError> {
if true {
Ok(10.0)
} else {
Err(MyError::Common)
}
}
Propagate the error from another function by using the ?
operator:
fn another_function() -> Result<f64, MyError> {
function_that_throws()? * 2
}
let mut x: Option<f64> = None;
x = Some(10.0);
if let Some(x) = x {
println!("{x}");
}
lib.rs
mod ns1;
ns1.rs
mod foo;
ns1/foo.rs
pub static BAR: &'static str = "Bar";
Everything is internal to the enclosing module by default, unless the pub
qualifier is used.
There is pub
, and pub(module_path)
.
crate // The main module of your Rust crate
self // The enclosing module
super // The parent of the enclosing module
use foo::bar::*;
pub use foo::qux::*;
use super::Something;
const
versus static
variables: there are certain differences between them. Also, const
may appear in impl
blocks, which allows for extra readability.
const EXAMPLE_1: f64 = 1_024.0 * 4;
struct ExampleStruct;
impl ExampleStruct {
pub const EXAMPLE_CONST: &'static str = "Example string";
}
fn main() {
println!("{}", ExampleStruct::EXAMPLE_CONST);
}
Constants are expanded ("inlined") wherever used, while static
s are single memory locations.
Example 1:
type F<'a> = &'a dyn Fn(f64) -> f64;
let callback: F = & |a| a * 2.0;
Example 2:
fn m(callback: impl Fn(f64) -> f64) {
println!("Produced: {}", callback(10.0));
}
fn main() {
m(|a| a * 2.0);
}
There are different function types in Rust:
-
Fn(...)
orFn(...) -> T
-
FnMut(...)
orFnMut(...) -> T
-
FnOnce(...)
orFnOnce(...) -> T
-
fn(...)
orfn(...) -> T
The first three of these are "traits". As you can see in the lambda examples, for a callback you often need a type parameter that implements Fn(...) -> T
, or you receive the callback as a trait object (&dyn T
, Box<dyn T>, or
Rc`).
The fn(...)
type is ultra rare for us: it is a function pointer, which I myself never used.
A trait object is a value that performs "type erasure", allowing to use dynamic dispatches in any value that implements a series of traits.
The type of trait objects is indicated by the dyn
keyword, allowed at certain contexts (such as &dyn T
and `Rc).
type T1<'a> = &'a dyn Trait1 + Trait2 + TraitN;