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 |
register_indexer_get_result (fallible) | Fn(&mut T, X) -> Result<V, Box<EvalAltResult>> | yes, but not advised |
register_indexer_set_result (fallible) | Fn(&mut T, X, V) -> Result<(), Box<EvalAltResult>> | yes |
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
")?;
Indexer as Property Access Fallback
Such an indexer allows easy creation of _property bags_ (similar to [object maps])
which can dynamically add/remove properties.
An indexer taking a string index is a special case – it acts as a fallback to property getters/setters.
During a property access, if the appropriate property getter/setter is not defined, an indexer is called and passed the string name of the property.
This is also extremely useful as a short-hand for indexers, when the string keys conform to property name syntax.
// Assume 'obj' has an indexer defined with string parameters...
// Let's create a new key...
obj.hello_world = 42;
// The above is equivalent to this:
obj["hello_world"] = 42;
// You can write this...
let x = obj["hello_world"];
// but it is easier with this...
let x = obj.hello_world;
Caveat – reverse is NOT true
The reverse, however, is not true – when an indexer fails or doesn’t exist, the corresponding property getter/setter, if any, is not called.
type MyType = HashMap<String, i64>;
let mut engine = Engine::new();
// Define custom type, property getter and string indexers
engine.register_type::<MyType>()
.register_fn("new_ts", || {
let mut obj = MyType::new();
obj.insert("foo".to_string(), 1);
obj.insert("bar".to_string(), 42);
obj.insert("baz".to_string(), 123);
obj
})
// Property 'hello'
.register_get("hello", |obj: &mut MyType| obj.len() as i64)
// Index getter/setter
.register_indexer_get_result(|obj: &mut MyType, prop: &str|
obj.get(index).cloned().ok_or_else(|| "not found".into())
).register_indexer_set(|obj: &mut MyType, prop: &str, value: i64|
obj.insert(prop.to_string(), value)
);
engine.run("let ts = new_ts(); print(ts.foo);");
// ^^^^^^
// Calls ts["foo"] - getter for 'foo' does not exist
engine.run("let ts = new_ts(); print(ts.bar);");
// ^^^^^^
// Calls ts["bar"] - getter for 'bar' does not exist
engine.run("let ts = new_ts(); ts.baz = 999;");
// ^^^^^^^^^^^^
// Calls ts["baz"] = 999 - setter for 'baz' does not exist
engine.run(r#"let ts = new_ts(); print(ts["hello"]);"#);
// ^^^^^^^^^^^
// Error: Property getter for 'hello' not a fallback for indexer