Custom Type Indexers
A custom type can also expose an indexer by registering an indexer function.
A custom type with an indexer function defined can use the bracket notation to get/set a property value at a particular index:
object
[
index]
object
[
index]
=
value;
The Elvis notation is similar except that it returns ()
if the object itself is ()
.
// returns () if object is ()
object?[
index]
// no action if object is ()
object?[
index]
=
value;
Like property getters/setters, indexers take a &mut
reference to the first parameter.
They also take an additional parameter of any type that serves as the index within brackets.
Indexers are disabled when the no_index
and no_object
features are used together.
Engine API | Function signature(s) ( T: Clone = custom type,X: Clone = index type,V: Clone = data type) | Can mutate T ? |
---|---|---|
register_indexer_get | Fn(&mut T, X) -> V | yes, but not advised |
register_indexer_set | Fn(&mut T, X, V) | yes |
register_indexer_get_set | getter: Fn(&mut T, X) -> V setter: Fn(&mut T, X, V) | yes, but not advised in getter |
Rhai does NOT support normal references (i.e. &T
) as parameters.
All references must be mutable (i.e. &mut T
).
By convention, index getters are not supposed to mutate the custom type, although there is nothing that prevents this mutation.
For fallible indexers, it is customary to return
EvalAltResult::ErrorIndexNotFound
when called with an invalid index value.
Cannot Override Arrays, BLOB’s, Object Maps, Strings and Integers
They can be defined in a plugin module, but will be ignored.
For efficiency reasons, indexers cannot be used to overload (i.e. override) built-in indexing operations for arrays, object maps, strings and integers (acting as bit-field operation).
The following types have built-in indexer implementations that are fast and efficient.
Type | Index type | Return type | Description |
---|---|---|---|
Array | INT | Dynamic | access a particular element inside the array |
Blob | INT | INT | access a particular byte value inside the BLOB |
Map | ImmutableString ,String , &str | Dynamic | access a particular property inside the object map |
ImmutableString ,String , &str | INT | character | access a particular character inside the string |
INT | INT | boolean | access a particular bit inside the integer number as a bit-field |
INT | range | INT | access a particular range of bits inside the integer number as a bit-field |
In general, it is a bad idea to overload indexers for any of the standard types supported internally by Rhai, since built-in indexers may be added in future versions.
Examples
#[derive(Debug, Clone)]
struct TestStruct {
fields: Vec<i64>
}
impl TestStruct {
// Remember &mut must be used even for getters
fn get_field(&mut self, index: String) -> i64 {
self.fields[index.len()]
}
fn set_field(&mut self, index: String, value: i64) {
self.fields[index.len()] = value
}
fn new() -> Self {
Self { fields: vec![1, 2, 3, 4, 5] }
}
}
let mut engine = Engine::new();
engine.register_type::<TestStruct>()
.register_fn("new_ts", TestStruct::new)
// Short-hand: .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
.register_indexer_get(TestStruct::get_field)
.register_indexer_set(TestStruct::set_field);
let result = engine.eval::<i64>(
r#"
let a = new_ts();
a["xyz"] = 42; // these indexers use strings
a["xyz"] // as the index type
"#)?;
println!("Answer: {result}"); // prints 42
Convention for Negative Index
If the indexer takes a signed integer as an index (e.g. the standard INT
type), care should be
taken to handle negative values passed as the index.
It is a standard API convention for Rhai to assume that an index position counts backwards from the end if it is negative.
-1
as an index usually refers to the last item, -2
the second to last item, and so on.
Therefore, negative index values go from -1
(last item) to -length
(first item).
A typical implementation for negative index values is:
// The following assumes:
// 'index' is 'INT', 'items: usize' is the number of elements
let actual_index = if index < 0 {
index.checked_abs().map_or(0, |n| items - (n as usize).min(items))
} else {
index as usize
};
The end of a data type can be interpreted creatively. For example, in an integer used as a
bit-field, the start is the least-significant-bit (LSB) while the end
is the
most-significant-bit (MSB).
Convention for Range Index
By convention, negative values are not interpreted specially in indexers for ranges.
It is very common for ranges to be used as indexer parameters via the types
std::ops::Range<INT>
(exclusive) and std::ops::RangeInclusive<INT>
(inclusive).
One complication is that two versions of the same indexer must be defined to support exclusive and inclusive ranges respectively.
use std::ops::{Range, RangeInclusive};
let mut engine = Engine::new();
engine
/// Version of indexer that accepts an exclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
|obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
)
/// Version of indexer that accepts an inclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
|obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
);
engine.run(
"
let obj = new_ts();
let x = obj[0..12]; // use exclusive range
obj[0..=11] = !x; // use inclusive range
")?;