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