7.7 KiB
Custom Type Indexers
{{#include ../links.md}}
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][elvis] 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][fallible function] 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
")?;