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 Text
s
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 ifb.IsEqual(a)
;none
is only equal tonone
;- Result of
a.IsEqual(b)
does not change unless one of the objects gets deallocated.
Because of the last rule, IsEqual()
cannot compare two MutableText
s 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 Text
s 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()
andGetHash()
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
string
s the role of boxes and references is performed byText
andMutableText
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 struct
s.
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:
Method | Description |
---|---|
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. |