Boxing

Boxing is a process of turning value types such as bool, byte, int or float into objects. The concept is very simple: to store a value as a reference type, we create a box - an object that stores a single primitive value. It could be implemented like that:

class MyBox extends Object;
var float value;

Since Acedia's boxes are immutable (their value cannot change once the box was created), they store their value in the private field and provide access to it through the appropriate getter method.

Boxes were introduced because they allowed creation of general collections: Acedia's collections can only store AcediaObject, but, thanks to boxing, any value can be turned into an AcediaObject and stored in the collection. For native primitive types boxes can be created with either BoxAPI or manually:

local IntBox    box1;
local FloatBox  box2;
//  Created with `BoxAPI`
box1 = _.box.int(7);
//  Allocated and initialized manually
box2 = FloatBox(_.memory.Allocate(class'FloatBox'));
box2.Initialize(-2.48); // Must be done immediately after allocation!
//  Works the same
Log("Int value:" @ box1.Get());     // Int value: 7
Log("Float value:" @ box2.Get());   // Float value: -2.48

Immutable boxes also have a counterpart - mutable references that also provide Set() method:

local IntRef    ref1;
local FloatRef  ref2;
//  Created with `BoxAPI`
ref1 = _.ref.int(7);
//  Allocated and initialized manually
ref2 = FloatRef(_.memory.Allocate(class'FloatRef'));
ref2.Initialize(-2.48); // Must be done immediately after allocation!
//  Change values
ref1.Set(-89);
ref2.Set(0.56);
Log("Int value:" @ ref1.Get());     // Int value: -89
Log("Float value:" @ ref2.Get());   // Float value: 0.56

This makes it sound like reference are just more functional boxes! But the guarantee of unchanged value has its own perks and the most important difference between boxes and references concerns implementation of their IsEqual() and GetHash() methods. Let's talk about them a bit more.

Object equality and object hash

Comparing object variables with == operator checks reference equality: whether variables refer to the exact same object. But sometimes we want to implement value equality check - a comparison for the contents of two objects, e.g. checking that two different Texts store the exact same data. Acedia provides an alternative way to compare two objects - IsEqual() method. Its default implementation corresponds to that of == operator:

public function bool IsEqual(Object other)
{
    return (self == other);
}

But it can be redefined, as long as it obeys following rules:

  • a.IsEqual(a) == true;
  • a.IsEqual(b) if and only if b.IsEqual(a);
  • none is only equal to none;
  • Result of a.IsEqual(b) does not change unless one of the objects gets deallocated.

Because of the last rule, IsEqual() cannot compare two MutableTexts based on their contents, since they can change without deallocation (unlike contents of an immutable Text).

Reimplementing IsEqual() method also requires you to reimplement how object's hash value is calculated. Hash value is a an int value associated with an object. Equal objects must have the same hash value, but, while two different objects are also allowed to share the same hash, such a collision should be highly unlikely. Essentially, object hash value should be 100% determined by that object's contents, but behave as if it was a uniformly randomly generated int. This latter quality is necessary for hash tables to efficiently function.

By default, Acedia's objects simply use randomly generated value, determined at the moment of their allocation, as their hash (this means their hash value fully depends on their reference). This can be changed by reimplementing CalculateHashCode() method. Every object will only call it once to cache it for GetHashCode():

public final function int GetHashCode()
{
    if (_hashCodeWasCached) {
        return _cachedHashCode;
    }
    _hashCodeWasCached = true;
    _cachedHashCode = CalculateHashCode();
    return _cachedHashCode;
}

Important requirement placed on hash value is that it should never change for an allocated object. Unlike mutable objects, immutable ones, such as Text, can benefit from redefining their hash calculation to be fully dependent on their contents instead of their reference:

protected function int CalculateHashCode()
{
    local int i;
    local int hash;
    hash = 5381;
    for (i = 0; i < codePoints.length; i += 1)
    {
        //  hash * 33 + codePoints[i]
        hash = ((hash << 5) + hash) + codePoints[i];
    }
    return hash;
}

This makes sure that two Texts with equal contents have the same hash value and that is what makes them usable as keys inside the HashTable collection.

Equality and hashing for boxes and references

  • Boxes redefine IsEqual() and GetHash() to depend on the stored value. Since value inside the box cannot change, then there is no problem to base equality and hash on it.
  • References do not redefine IsEqual() / GetHash() and behave like any other object - their hash is random and they are only equal to themselves.
local ByteBox box1, box2;
local ByteRef ref1, ref2;
box1 = _.box.byte(56);
box2 = _.box.byte(56);
ref1 = _.ref.byte(247);
ref2 = _.ref.byte(247);
// Boxes equality: true
Log("Boxes equality:" @ (box1.IsEqual(box2)));
// Boxes hash equality: true
Log("Boxes hash equality:" @ (box1.GetHash() == box2.GetHash()));
// Refs equality: false
Log("Refs equality:" @ (ref1.IsEqual(ref2)));
// Refs hash equality: false
// (that's the most likely result, but it can actually be `true` by pure chance)
Log("Refs hash equality:" @ (ref1.GetHash() == ref2.GetHash()));

NOTE: For strings the role of boxes and references is performed by Text and MutableText classes that are discussed elsewhere.

Actor references with NativeActorRef

As was explained in object problem, storing actor references directly inside objects is a bad idea. The safe way to do it are special actor reference objects: ActorRef for Acedia's actors and NativeActorRef for any kind of actors. They themselves are not actors, but Actor returned by their Get() method is guaranteed to be safe to use:

class MyObject extends AcediaObject;

var NativeActorRef pawnReference;
// ...

protected function Finalizer()
{
    _.memory.Free(pawnReference); // This does not destroy stored pawn!
    pawnReference = none;
}

function Pawn GetMyPawn()
{
    if (pawnReference == none) {
        return none;
    }
    return Pawn(pawnReference.Get());
}

function SetMyPawn(Pawn newPawn)
{
    if (pawnReference == none)
    {
        //  `UnrealAPI` deals with storing non-Acedia actors such as `Pawn`.
        //  For `AcediaActor`s you can also use `_.ref.Actor()`.
        pawnReference = _.unreal.ActorRef(newPawn);
    }
    else {
        pawnReference.Set(newPawn);
    }
}

function DoWork()
{
    local Pawn myPawn;
    myPawn = GetMyPawn();
    if (myPawn == none) {
        return;
    }
    // <Some code that might `Destroy()` our pawn>
    // ^ After destroying a pawn,
    //  `myPawn` local variable might go "bad" and cause crashes,
    //  so it's a good idea to "update" it from the safe `pawnReference`:
    myPawn = GetMyPawn();
    if (myPawn != none) {
        myPawn.health += 10;
    }
}

NOTE: Actor boxes do not exist, since we cannot guarantee that value inside them will never change - destroying stored actor will always reset it to none.

Array boxes and references

If necessary, box and reference classes can be manually created for any type of value, including array<...>s and structs. Acedia provides such classes for arrays of primitive value types out of the box. They can be useful for passing huge arrays between objects and functions by reference, without copying their entire data every time. They also provide several convenience methods - here is a list for FloatArrayRef's methods as an example:

MethodDescription
Get()Returns the whole stored array as array<float>.
Set(array<float>)Sets the whole array value.
GetItem(int, optional float)Returns item at specified index. If index is invalid, returns passed default value.
SetItem(int, float)Changes array's value at specified index.
GetLength()Returns length of the array. ref.GetLength() is faster than ref.Get().length, since latter will make a copy of the whole array first
SetLength(int)Resizes stored array, doing nothing on negative input.
Empty()Empties stored array.
Add(int)Increases length of the array by adding specified amount of new elements at the end.
Insert(int index, int count)Inserts count zeroes into the array at specified position. The indices of the following elements are increased by count in order to make room for the new elements.
Remove(int index, int count)Removes number elements from the array, starting at index. All elements before position and from index + count on are not changed, but the element indices change, - they shift to close the gap, created by removed elements.
RemoveIndex(int)Removes value at a given index, shifting all the elements that come after one place backwards.
AddItem(float)Adds given float at the end of the array, expanding it by 1 element.
InsertItem(int, float)Inserts given item at index of the array, shifting all the elements starting from index one position to the right.
AddArray(array<float>) / AddArrayRef(FloatArrayRef)Adds given array of items at the end of the array, expanding it by inserted amount.
InsertArray(array<float>) / InsertArrayRef(FloatArrayRef)Inserts items array at specified index of the array, shifting all the elements starting from index by inserted amount to the right.
RemoveItem(float, bool)Returns all occurrences of item in the caller float (optionally only first one).
Find(float)Finds first occurrence of specified item in caller FloatArrayRef and returns its index.
Replace(float search, float replacement)Replaces any occurrence of search with replacement.
Sort(optional bool descending)Sorts array in either ascending or descending order.