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 APIFunction signature(s)
(T: Clone = custom type,
X: Clone = index type,
V: Clone = data type)
Can mutate T?
register_indexer_getFn(&mut T, X) -> Vyes, but not advised
register_indexer_setFn(&mut T, X, V)yes
register_indexer_get_setgetter: Fn(&mut T, X) -> V
setter: Fn(&mut T, X, V)
yes, but not advised in getter

No support for references

Rhai does NOT support normal references (i.e. &T) as parameters. All references must be mutable (i.e. &mut T).

Getters must be pure

By convention, index getters are not supposed to mutate the custom type, although there is nothing that prevents this mutation.

Tip: EvalAltResult::ErrorIndexNotFound

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

Plugins

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.

TypeIndex typeReturn typeDescription
ArrayINTDynamicaccess a particular element inside the array
BlobINTINTaccess a particular byte value inside the BLOB
MapImmutableString,
String, &str
Dynamicaccess a particular property inside the object map
ImmutableString,
String, &str
INTcharacteraccess a particular character inside the string
INTINTbooleanaccess a particular bit inside the integer number as a bit-field
INTrangeINTaccess a particular range of bits inside the integer number as a bit-field

Do not overload indexers for built-in standard types

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

Tip: Negative values

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 ")?;