C# already has two bit array types, but both are lacking. BitArray is a class so it requires heap allocation and GC. BitVector32 is a struct, but it’s usage is bizzare, it’s implemented inefficiently, it’s not enumerable, and there’s no 64-bit version. Today we’ll create a new, simple type to remedy these issues and add a new tool to our toolbox!

Goals

Our goal is to create a better version of BitArray and BitVector32. We want a type with 32- and 64-bit versions, doesn’t require heap allocation, uses assertions for bounds checks, supports foreach, doesn’t trigger the GC, supports Unity jobs, and allows for direct access to the bits it holds.

We’ll call our types BitArray32 and BitArray64. The following table illustrates how these new types compare with the .NET types:

Type Length Allocation Bounds Checks Foreach GC Job Support Public Bits
BitArray Arbitrary Heap Exceptions Yes Yes No No
BitVector32 32 Stack or Heap Exceptions No No Yes No
BitArray32 32 Stack or Heap Assertions Yes if C# 7+ No Yes Yes
BitArray64 64 Stack or Heap Assertions Yes if C# 7+ No Yes Yes

If we can achieve this goal, we’ll have two excellent new types in our toolbox.

BitVector32

As a class, there’s really no hope for BitArray. On the other hand, BitVector32 is a struct and therefore might be a good option. Unfortunately, it suffers from some nasty issues that we’ll explore now.

First, and most prominently, is how its indexer works. Most users would expect an indexer to index into something. This is not how BitVector32 works. Instead, its indexer takes a mask parameter rather than an index. This leads to the following bizarre behavior:

// Initially all zeroes
BitVector32 vec = new BitVector32(0);
 
// This is unexpectedly true! Here's why:
//   return (bits & mask) == mask;
//   return (0 & 0) == 0;
//   return 0 == 0;
//   return true;
Debug.Log(vec[0]);
 
// False as expected
//   return (bits & mask) == mask;
//   return (0 & 1) == 1;
//   return 0 == 1;
//   return false;
Debug.Log(vec[1]);
 
// False as expected
//   return (bits & mask) == mask;
//   return (0 & 2) == 2;
//   return 0 == 2;
//   return false;
Debug.Log(vec[2]);
 
// Doesn't set the bit at index 7
// Sets the mask of 7: 00000000000000000000000000000111
// So the low three bits are now unexpectedly set
//   bits |= mask;
//   bits = 0 | 7;
//   bits = 7;
vec[7] = true;
 
// Unexpectedly true
//   return (bits & mask) == mask;
//   return (7 & 0) == 0;
//   return 0 == 0;
//   return true;
Debug.Log(vec[0]);
 
// Unexpectedly true
//   return (bits & mask) == mask;
//   return (7 & 1) == 1;
//   return 1 == 1;
//   return true;
Debug.Log(vec[1]);
 
// Unexpectedly true
//   return (bits & mask) == mask;
//   return (7 & 2) == 2;
//   return 2 == 2;
//   return true;
Debug.Log(vec[2]);
 
// This is true, but not because the bit at index 7 is set to 1
// Only true because the bits at index 0, 1, and 2 are set to 1
//   return (bits & mask) == mask;
//   return (7 & 7) == 7;
//   return 7 == 7;
//   return true;
Debug.Log(vec[7]);
 
// Unexpectedly set the bit at index 0 to 0:
//   bits &= ~mask;
//   bits = 7 & ~1;
//   bits = 7 & 0b11111111111111111111111111111110;
//   bits = 6;
vec[1] = false;
 
// This is now false because one of the bits at index 0, 1, and 2 is now 0:
//   return (bits & mask) == mask;
//   return (6 & 7) == 7;
//   return 6 == 7;
//   return false;
Debug.Log(vec[7]);

Other than that, we have the following issues with BitVector32:

  • No BitVector64 version for 64-bit long arrays
  • Bounds checks use exceptions, which don’t work in Burst-compiled jobs and aren’t stripped out of release builds
  • Doesn’t implement IEquatable<T>, so Equals always performs expensive type checks
  • Doesn’t have a GetEnumerator, so we can’t use foreach to iterate over it
  • ToString uses StringBuilder which creates an extra GC allocation. There’s no initial capacity passed to it, so StringBuilder has to do dynamic resizing as characters are added.
  • The bits isn’t directly accessible, which means they can’t be used as ref, out, or in parameters
Implementation

The basics of what we need are very simple. First, we start with a struct that simply wraps a uint for 32-bits or ulong for 64-bits and provides an indexer into its bits. Here’s how that looks for the 32-bit version:

public struct BitArray32
{
    public uint Bits;
 
    public bool this[int index]
    {
        get
        {
            uint mask = 1u << index;
            return (Bits & mask) == mask;
        }
        set
        {
            uint mask = 1u << index;
            if (value)
            {
                Bits |= mask;
            }
            else
            {
                Bits &= ~mask;
            }
        }
    }
}

Now we can start adding on to this core. First, we need assertion-based bounds checks so let’s add a method for that:

[BurstDiscard]
public void RequireIndexInBounds(int index)
{
    Assert.IsTrue(
        index >= 0 && index < 32,
        "Index out of bounds: " + index);
}

Normally we couldn’t use this method in Burst-compiled jobs, but adding [BurstDiscard] allows us to circumvent that restriction.

Now we can call this from the indexer:

public bool this[int index]
{
    get
    {
        RequireIndexInBounds(index);
        uint mask = 1u << index;
        return (Bits & mask) == mask;
    }
    set
    {
        RequireIndexInBounds(index);
        uint mask = 1u << index;
        if (value)
        {
            Bits |= mask;
        }
        else
        {
            Bits &= ~mask;
        }
    }
}

The next expansion is to provide a way to avoid that if in the set block of the indexer for when the value to set is known at compile time. Let’s create two methods: one to set to 1 and one to unset to 0.

public void SetBit(int index)
{
    RequireIndexInBounds(index);
    uint mask = 1u << index;
    Bits |= mask;
}
 
public void UnsetBit(int index)
{
    RequireIndexInBounds(index);
    uint mask = 1u << index;
    Bits &= ~mask;
}

Next, we’ll add methods to get and set multiple bits at once. These avoid the need to perform any bounds checking and to call the indexer over and over.

public uint GetBits(uint mask)
{
    return Bits & mask;
}
 
public void SetBits(uint mask)
{
    Bits |= mask;
}
 
public void UnsetBits(uint mask)
{
    Bits &= ~mask;
}

By default, the array is made up of all zeroes because the default value of uint and ulong is 0. Let’s provide a constructor to change that default:

public BitArray32(uint bits)
{
    Bits = bits;
}

Next, we’ll add a Length property to mimick the interface of C#’s managed arrays:

public int Length
{
    get
    {
        return 32;
    }
}

Now we can override object.Equals and provide a more efficient Equals using the IEquatable&lt;BitArray32&gt; interface. Both of them just compare the Bits field, but the object.Equals version also has to perform a type check and cast.

public struct BitArray32 : IEquatable<BitArray32>
{
    public override bool Equals(object obj)
    {
        return obj is BitArray32 && Bits == ((BitArray32)obj).Bits;
    }
 
    public bool Equals(BitArray32 arr)
    {
        return Bits == arr.Bits;
    }
}

We’ll also need a GetHashCode that just returns the result of GetHashCode on the underlying Bits:

public override int GetHashCode()
{
    return Bits.GetHashCode();
}

And the last piece of boilerplate is to provide a ToString that prints out the bits as 0 or 1 characters. Rather than using a StringBuilder like in BitVector32, we’ll simply using a char[] since we know exactly how long it’ll be:

public override string ToString()
{
    const string header = "BitArray32{";
    const int headerLen = 11; // must be header.Length
    char[] chars = new char[headerLen + 32 + 1];
    int i = 0;
    for (; i < headerLen; ++i)
    {
        chars[i] = header[i];
    }
    for (uint num = 1u << 31; num > 0; num >>= 1, ++i)
    {
        chars[i] = (Bits & num) != 0 ? '1' : '0';
    }
    chars[i] = '}';
    return new string(chars);
}

Finally, let’s add support for foreach loops. To do so, let’s create a nested Enumerator type with the required MoveNext method and Current property. This is a ref struct since it includes a pointer directly to the Bits field of the array and we don’t want that pointer to be invalidated by having the array go out of scope before the Enumerator does. Since ref struct is only available in C# 7 and later, we wrap this with #if CSHARP_7_OR_LATER to avoid compiler errors in older versions of Unity.

#if CSHARP_7_OR_LATER
public unsafe ref struct Enumerator
{
    private readonly uint* m_Bits;
 
    private int m_Index;
 
    public Enumerator(uint* bits)
    {
        m_Bits = bits;
        m_Index = -1;
    }
 
    public bool MoveNext()
    {
        m_Index++;
        return m_Index < 32;
    }
 
    public bool Current
    {
        get
        {
            RequireIndexInBounds();
            uint mask = 1u << m_Index;
            return (*m_Bits & mask) == mask;
        }
    }
 
    [BurstDiscard]
    public void RequireIndexInBounds()
    {
        Assert.IsTrue(
            m_Index >= 0 && m_Index < 32,
            "Index out of bounds: " + m_Index);
    }
}
#endif

The counterpart to this is to add a GetEnumerator method to BitArray32:

#if CSHARP_7_OR_LATER
public unsafe Enumerator GetEnumerator()
{
    // Safe because Enumerator is a 'ref struct'
    fixed (uint* bits = &Bits)
    {
        return new Enumerator(bits);
    }
}
#endif
Source

Here’s the full source for BitArray32, complete with thorough xml-doc comments:

using System;
using Unity.Burst;
using UnityEngine.Assertions;
 
/// <summary>
/// Array of 32 bits. Fully unmanaged. Defaults to zeroes. Enumerable in C# 7.
/// </summary>
/// 
/// <author>
/// Jackson Dunstan, https://JacksonDunstan.com/articles/5172
/// </author>
/// 
/// <license>
/// MIT
/// </license>
public struct BitArray32 : IEquatable<BitArray32>
{
#if CSHARP_7_OR_LATER
    /// <summary>
    /// Enumerates the bits of the array from least-significant to
    /// most-signficant. It's OK to change the array while enumerating.
    /// </summary>
    public unsafe ref struct Enumerator
    {
        /// <summary>
        /// Pointer to the bits
        /// </summary>
        private readonly uint* m_Bits;
 
        /// <summary>
        /// Index into the bits
        /// </summary>
        private int m_Index;
 
        /// <summary>
        /// Create the enumerator with index at -1
        /// </summary>
        /// 
        /// <param name="bits">
        /// Bits to enumerate
        /// </param>
        public Enumerator(uint* bits)
        {
            m_Bits = bits;
            m_Index = -1;
        }
 
        /// <summary>
        /// Move to the next bit
        /// </summary>
        /// 
        /// <returns>
        /// If a bit is available via <see cref="Current"/>. If not, enumeration
        /// is done.
        /// </returns>
        public bool MoveNext()
        {
            m_Index++;
            return m_Index < 32;
        }
 
        /// <summary>
        /// Get the current bit. If <see cref="MoveNext"/> has not been called
        /// or the last call of <see cref="MoveNext"/> returned false, this
        /// function asserts.
        /// </summary>
        /// 
        /// <value>
        /// The current bit
        /// </value>
        public bool Current
        {
            get
            {
                RequireIndexInBounds();
                uint mask = 1u << m_Index;
                return (*m_Bits & mask) == mask;
            }
        }
 
        /// <summary>
        /// Assert if <see cref="m_Index"/> isn't in bounds
        /// </summary>
        [BurstDiscard]
        public void RequireIndexInBounds()
        {
            Assert.IsTrue(
                m_Index >= 0 && m_Index < 32,
                "Index out of bounds: " + m_Index);
        }
    }
#endif
 
    /// <summary>
    /// Integer whose bits make up the array
    /// </summary>
    public uint Bits;
 
    /// <summary>
    /// Create the array with the given bits
    /// </summary>
    /// 
    /// <param name="bits">
    /// Bits to make up the array
    /// </param>
    public BitArray32(uint bits)
    {
        Bits = bits;
    }
 
    /// <summary>
    /// Get or set the bit at the given index. For faster getting of multiple
    /// bits, use <see cref="GetBits(uint)"/>. For faster setting of single
    /// bits, use <see cref="SetBit(int)"/> or <see cref="UnsetBit(int)"/>. For
    /// faster setting of multiple bits, use <see cref="SetBits(uint)"/> or
    /// <see cref="UnsetBits(uint)"/>.
    /// </summary>
    /// 
    /// <param name="index">
    /// Index of the bit to get or set
    /// </param>
    public bool this[int index]
    {
        get
        {
            RequireIndexInBounds(index);
            uint mask = 1u << index;
            return (Bits & mask) == mask;
        }
        set
        {
            RequireIndexInBounds(index);
            uint mask = 1u << index;
            if (value)
            {
                Bits |= mask;
            }
            else
            {
                Bits &= ~mask;
            }
        }
    }
 
    /// <summary>
    /// Get the length of the array
    /// </summary>
    /// 
    /// <value>
    /// The length of the array. Always 32.
    /// </value>
    public int Length
    {
        get
        {
            return 32;
        }
    }
 
    /// <summary>
    /// Set a single bit to 1
    /// </summary>
    /// 
    /// <param name="index">
    /// Index of the bit to set. Asserts if not on [0:31].
    /// </param>
    public void SetBit(int index)
    {
        RequireIndexInBounds(index);
        uint mask = 1u << index;
        Bits |= mask;
    }
 
    /// <summary>
    /// Set a single bit to 0
    /// </summary>
    /// 
    /// <param name="index">
    /// Index of the bit to unset. Asserts if not on [0:31].
    /// </param>
    public void UnsetBit(int index)
    {
        RequireIndexInBounds(index);
        uint mask = 1u << index;
        Bits &= ~mask;
    }
 
    /// <summary>
    /// Get all the bits that match a mask
    /// </summary>
    /// 
    /// <param name="mask">
    /// Mask of bits to get
    /// </param>
    /// 
    /// <returns>
    /// The bits that match the given mask
    /// </returns>
    public uint GetBits(uint mask)
    {
        return Bits & mask;
    }
 
    /// <summary>
    /// Set all the bits that match a mask to 1
    /// </summary>
    /// 
    /// <param name="mask">
    /// Mask of bits to set
    /// </param>
    public void SetBits(uint mask)
    {
        Bits |= mask;
    }
 
    /// <summary>
    /// Set all the bits that match a mask to 0
    /// </summary>
    /// 
    /// <param name="mask">
    /// Mask of bits to unset
    /// </param>
    public void UnsetBits(uint mask)
    {
        Bits &= ~mask;
    }
 
    /// <summary>
    /// Check if this array equals an object
    /// </summary>
    /// 
    /// <param name="obj">
    /// Object to check. May be null.
    /// </param>
    /// 
    /// <returns>
    /// If the given object is a BitArray32 and its bits are the same as this
    /// array's bits
    /// </returns>
    public override bool Equals(object obj)
    {
        return obj is BitArray32 && Bits == ((BitArray32)obj).Bits;
    }
 
    /// <summary>
    /// Check if this array equals another array
    /// </summary>
    /// 
    /// <param name="arr">
    /// Array to check
    /// </param>
    /// 
    /// <returns>
    /// If the given array's bits are the same as this array's bits
    /// </returns>
    public bool Equals(BitArray32 arr)
    {
        return Bits == arr.Bits;
    }
 
    /// <summary>
    /// Get the hash code of this array
    /// </summary>
    /// 
    /// <returns>
    /// The hash code of this array, which is the same as
    /// the hash code of <see cref="Bits"/>.
    /// </returns>
    public override int GetHashCode()
    {
        return Bits.GetHashCode();
    }
 
    /// <summary>
    /// Get a string representation of the array
    /// </summary>
    /// 
    /// <returns>
    /// A newly-allocated string representing the bits of the array.
    /// </returns>
    public override string ToString()
    {
        const string header = "BitArray32{";
        const int headerLen = 11; // must be header.Length
        char[] chars = new char[headerLen + 32 + 1];
        int i = 0;
        for (; i < headerLen; ++i)
        {
            chars[i] = header[i];
        }
        for (uint num = 1u << 31; num > 0; num >>= 1, ++i)
        {
            chars[i] = (Bits & num) != 0 ? '1' : '0';
        }
        chars[i] = '}';
        return new string(chars);
    }
 
    /// <summary>
    /// Assert if the given index isn't in bounds
    /// </summary>
    /// 
    /// <param name="index">
    /// Index to check
    /// </param>
    [BurstDiscard]
    public void RequireIndexInBounds(int index)
    {
        Assert.IsTrue(
            index >= 0 && index < 32,
            "Index out of bounds: " + index);
    }
 
#if CSHARP_7_OR_LATER
    /// <summary>
    /// Get an enumerator for this array's bits
    /// </summary>
    /// 
    /// <returns>
    /// An enumerator for this array's bits
    /// </returns>
    public unsafe Enumerator GetEnumerator()
    {
        // Safe because Enumerator is a 'ref struct'
        fixed (uint* bits = &Bits)
        {
            return new Enumerator(bits);
        }
    }
#endif
}

And the full source for BitArray64:

using System;
using Unity.Burst;
using UnityEngine.Assertions;
 
/// <summary>
/// Array of 64 bits. Fully unmanaged. Defaults to zeroes. Enumerable in C# 7.
/// </summary>
/// 
/// <author>
/// Jackson Dunstan, https://JacksonDunstan.com/articles/5172
/// </author>
/// 
/// <license>
/// MIT
/// </license>
public struct BitArray64 : IEquatable<BitArray64>
{
#if CSHARP_7_OR_LATER
    /// <summary>
    /// Enumerates the bits of the array from least-significant to
    /// most-signficant. It's OK to change the array while enumerating.
    /// </summary>
    public unsafe ref struct Enumerator
    {
        /// <summary>
        /// Pointer to the bits
        /// </summary>
        private readonly ulong* m_Bits;
 
        /// <summary>
        /// Index into the bits
        /// </summary>
        private int m_Index;
 
        /// <summary>
        /// Create the enumerator with index at -1
        /// </summary>
        /// 
        /// <param name="bits">
        /// Bits to enumerate
        /// </param>
        public Enumerator(ulong* bits)
        {
            m_Bits = bits;
            m_Index = -1;
        }
 
        /// <summary>
        /// Move to the next bit
        /// </summary>
        /// 
        /// <returns>
        /// If a bit is available via <see cref="Current"/>. If not, enumeration
        /// is done.
        /// </returns>
        public bool MoveNext()
        {
            m_Index++;
            return m_Index < 64;
        }
 
        /// <summary>
        /// Get the current bit. If <see cref="MoveNext"/> has not been called
        /// or the last call of <see cref="MoveNext"/> returned false, this
        /// function asserts.
        /// </summary>
        /// 
        /// <value>
        /// The current bit
        /// </value>
        public bool Current
        {
            get
            {
                RequireIndexInBounds();
                ulong mask = 1ul << m_Index;
                return (*m_Bits & mask) == mask;
            }
        }
 
        /// <summary>
        /// Assert if <see cref="m_Index"/> isn't in bounds
        /// </summary>
        [BurstDiscard]
        public void RequireIndexInBounds()
        {
            Assert.IsTrue(
                m_Index >= 0 && m_Index < 64,
                "Index out of bounds: " + m_Index);
        }
    }
#endif
 
    /// <summary>
    /// Integer whose bits make up the array
    /// </summary>
    public ulong Bits;
 
    /// <summary>
    /// Create the array with the given bits
    /// </summary>
    /// 
    /// <param name="bits">
    /// Bits to make up the array
    /// </param>
    public BitArray64(ulong bits)
    {
        Bits = bits;
    }
 
    /// <summary>
    /// Get or set the bit at the given index. For faster getting of multiple
    /// bits, use <see cref="GetBits(ulong)"/>. For faster setting of single
    /// bits, use <see cref="SetBit(int)"/> or <see cref="UnsetBit(int)"/>. For
    /// faster setting of multiple bits, use <see cref="SetBits(ulong)"/> or
    /// <see cref="UnsetBits(ulong)"/>.
    /// </summary>
    /// 
    /// <param name="index">
    /// Index of the bit to get or set
    /// </param>
    public bool this[int index]
    {
        get
        {
            RequireIndexInBounds(index);
            ulong mask = 1ul << index;
            return (Bits & mask) == mask;
        }
        set
        {
            RequireIndexInBounds(index);
            ulong mask = 1ul << index;
            if (value)
            {
                Bits |= mask;
            }
            else
            {
                Bits &= ~mask;
            }
        }
    }
 
    /// <summary>
    /// Get the length of the array
    /// </summary>
    /// 
    /// <value>
    /// The length of the array. Always 64.
    /// </value>
    public int Length
    {
        get
        {
            return 64;
        }
    }
 
    /// <summary>
    /// Set a single bit to 1
    /// </summary>
    /// 
    /// <param name="index">
    /// Index of the bit to set. Asserts if not on [0:31].
    /// </param>
    public void SetBit(int index)
    {
        RequireIndexInBounds(index);
        ulong mask = 1ul << index;
        Bits |= mask;
    }
 
    /// <summary>
    /// Set a single bit to 0
    /// </summary>
    /// 
    /// <param name="index">
    /// Index of the bit to unset. Asserts if not on [0:31].
    /// </param>
    public void UnsetBit(int index)
    {
        RequireIndexInBounds(index);
        ulong mask = 1ul << index;
        Bits &= ~mask;
    }
 
    /// <summary>
    /// Get all the bits that match a mask
    /// </summary>
    /// 
    /// <param name="mask">
    /// Mask of bits to get
    /// </param>
    /// 
    /// <returns>
    /// The bits that match the given mask
    /// </returns>
    public ulong GetBits(ulong mask)
    {
        return Bits & mask;
    }
 
    /// <summary>
    /// Set all the bits that match a mask to 1
    /// </summary>
    /// 
    /// <param name="mask">
    /// Mask of bits to set
    /// </param>
    public void SetBits(ulong mask)
    {
        Bits |= mask;
    }
 
    /// <summary>
    /// Set all the bits that match a mask to 0
    /// </summary>
    /// 
    /// <param name="mask">
    /// Mask of bits to unset
    /// </param>
    public void UnsetBits(ulong mask)
    {
        Bits &= ~mask;
    }
 
    /// <summary>
    /// Check if this array equals an object
    /// </summary>
    /// 
    /// <param name="obj">
    /// Object to check. May be null.
    /// </param>
    /// 
    /// <returns>
    /// If the given object is a BitArray64 and its bits are the same as this
    /// array's bits
    /// </returns>
    public override bool Equals(object obj)
    {
        return obj is BitArray64 && Bits == ((BitArray64)obj).Bits;
    }
 
    /// <summary>
    /// Check if this array equals another array
    /// </summary>
    /// 
    /// <param name="arr">
    /// Array to check
    /// </param>
    /// 
    /// <returns>
    /// If the given array's bits are the same as this array's bits
    /// </returns>
    public bool Equals(BitArray64 arr)
    {
        return Bits == arr.Bits;
    }
 
    /// <summary>
    /// Get the hash code of this array
    /// </summary>
    /// 
    /// <returns>
    /// The hash code of this array, which is the same as
    /// the hash code of <see cref="Bits"/>.
    /// </returns>
    public override int GetHashCode()
    {
        return Bits.GetHashCode();
    }
 
    /// <summary>
    /// Get a string representation of the array
    /// </summary>
    /// 
    /// <returns>
    /// A newly-allocated string representing the bits of the array.
    /// </returns>
    public override string ToString()
    {
        const string header = "BitArray64{";
        const int headerLen = 11; // must be header.Length
        char[] chars = new char[headerLen + 64 + 1];
        int i = 0;
        for (; i < headerLen; ++i)
        {
            chars[i] = header[i];
        }
        for (ulong num = 1ul << 63; num > 0; num >>= 1, ++i)
        {
            chars[i] = (Bits & num) != 0 ? '1' : '0';
        }
        chars[i] = '}';
        return new string(chars);
    }
 
    /// <summary>
    /// Assert if the given index isn't in bounds
    /// </summary>
    /// 
    /// <param name="index">
    /// Index to check
    /// </param>
    [BurstDiscard]
    public void RequireIndexInBounds(int index)
    {
        Assert.IsTrue(
            index >= 0 && index < 64,
            "Index out of bounds: " + index);
    }
 
#if CSHARP_7_OR_LATER
    /// <summary>
    /// Get an enumerator for this array's bits
    /// </summary>
    /// 
    /// <returns>
    /// An enumerator for this array's bits
    /// </returns>
    public unsafe Enumerator GetEnumerator()
    {
        // Safe because Enumerator is a 'ref struct'
        fixed (ulong* bits = &Bits)
        {
            return new Enumerator(bits);
        }
    }
#endif
}
Usage

Now that we have these types, let’s look at how to use them. In this example we’ll see how to use a BitArray32 in a Unity job. This isn’t possible with BitArray or BitVector32, so it’s a clear win for our newly-created type. In doing so we’ll also see a foreach loop, which isn’t supported by BitVector32. At no point in this example do we ever allocate managed memory for the GC to later collect.

// A Burst-compiled job that counts the number of bits set
[BurstCompile]
struct CountBitsJob : IJob
{
    // Bits to count
    [ReadOnly] public BitArray32 BitArray;
 
    // The count is written to the first element
    [WriteOnly] public NativeArray<int> NumBitsSet;
 
    public void Execute()
    {
        // Clear count
        NumBitsSet[0] = 0;
 
        // Loop over the bits in the array
        // Possible because we have GetEnumerator() and Enumerator
        // Burst unrolls this to 32 copies of the loop body
        foreach (bool bit in BitArray)
        {
            // Count the bit if it's set
            // Burst replaces this branch with an increment of 0 or 1
            if (bit)
            {
                NumBitsSet[0]++;
            }
        }
    }
}
 
// Create the array with the low bits set to 101
BitArray32 arr = new BitArray32(5);
 
// Create the output for the count
NativeArray<int> num = new NativeArray<int>(1, Allocator.TempJob);
 
// Create and run the job
CountBitsJob job = new CountBitsJob { BitArray = arr, NumBitsSet = num };
job.Execute();
 
// Get the result
int count = num[0];
 
// Dispose the count output
num.Dispose();

Hopefully the usage of BitArray32 here is clear and simple, unlike in BitVector32 with its masking indexer. Still, if we make a mistake then the bounds-check assertions will immediately alert us to the problem in debug builds. They’re stripped out of release builds, so we incur zero overhead in the shipping version of our game.

Conclusion

BitArray32 and BitArray64 are nice new additions to our toolbox. There are surely times where their usage doesn’t make sense, such as when we want more than 64 bits in the array or a dynamic number of bits, but in many common cases they can be quite handy. By supporting Unity jobs and foreach without the clumbsy interfaces and GC of .NET alternatives, they usually provide a better option than BitArray and BitVector32. Hopefully you’ll find them useful!