简体   繁体   中英

Custom 4 bit data type in C#

I want to create a custom data type which is 4 bits (nibble).

One option is this -

byte source = 0xAD;
var hiNybble = (source & 0xF0) >> 4; //Left hand nybble = A
var loNyblle = (source & 0x0F);      //Right hand nybble = D

However, I want to store each 4 bits in an array.

So for example, a 2 byte array,

00001111 01010000

would be stored in the custom data type array as 4 nibbles -

0000
1111
0101
0000

Essentially I want to operate on 4 bit types.

Is there any way I can convert the array of bytes into array of nibbles?

Appreciate an example.

Thanks.

You can encapsulate a stream returning 4-bit samples by reading then converting (written from a phone without a compiler to test. Expect typos and off-by-one errors):

public static int ReadNibbles(this Stream s, byte[] data, int offset, int count)
{
    if (s == null)
    {
        throw new ArgumentNullException(nameof(s));
    }
    if (data == null)
    {
        throw new ArgumentNullException(nameof(data));
    }
    if (data.Length < offset + length)
    {
        throw new ArgumentOutOfRangeException(nameof(length));
    }

    var readBytes = s.Read(data, offset, length / 2);
    for (int n = readBytes * 2 - 1, k = readBytes - 1; k >= 0; k--)
    {
        data[offset + n--] = data[offset + k] & 0xf;
        data[offset + n--] = data[offset + k] >> 4;
    }
    return readBytes * 2;
}

To do the same for 12-bit integers (assuming MSB nibble ordering):

public static int Read(this Stream stream, ushort[] data, int offset, int length)
{
    if (stream == null)
    {
        throw new ArgumentNullException(nameof(stream));
    }
    if (data == null)
    {
        throw new ArgumentNullException(nameof(data));
    }
    if (data.Length < offset + length)
    {
        throw new ArgumentOutOfRangeException(nameof(length));
    }
    if (length < 2)
    {
        throw new ArgumentOutOfRangeException(nameof(length), "Cannot read fewer than two samples at a time");
    }
        
    // we always need a multiple of two
    length -= length % 2;

    // 3 bytes     length samples
    // --------- * -------------- = N bytes
    // 2 samples         1
    int rawCount = (length / 2) * 3;

    // This will place GC load.  Something like a buffer pool or keeping
    // the allocation as a field on the reader would be a good idea.
    var rawData = new byte[rawCount];
    int readBytes = 0;
    // if the underlying stream returns an even number of bytes, we will need to try again
    while (readBytes < data.Length)
    {
        int c = stream.Read(rawData, readBytes, rawCount - readBytes);
        if (c <= 0)
        {
            // End of stream
            break;
        }
        readBytes += c;
    }

    // unpack
    int k = 0;
    for (int i = 0; i < readBytes; i += 3)
    {
        // EOF in second byte is undefined
        if (i + 1 >= readBytes)
        {
            throw new InvalidOperationException("Unexpected EOF");
        }

        data[(k++) + offset] = (ushort)((rawData[i + 0] << 4) | (rawData[i + 1] >> 4));

        // EOF in third byte == only one sample
        if (i + 2 < readBytes)
        {
            data[(k++) + offset] = (ushort)(((rawData[i + 1] & 0xf) << 8) | rawData[i + 2]);
        }
    }
    return k;
}

The best way to do this would be to look at the source for one of the existing integral data types. For example Int16 .

If you look a that type, you can see that it implements a handful of interfaces:

[Serializable]
public struct Int16 : IComparable, IFormattable, IConvertible, IComparable<short>, IEquatable<short> { /* ... */ }

The implementation of the type isn't very complicated. It has a MaxValue a MinValue , a couple of CompareTo overloads, a couple of Equals overloads, the System.Object overrides ( GetHashCode , GetType , ToString (plus some overloads)), a handful of Parse and ToParse overloads and a range of IConvertible implementations.

In other places, you can find things like arithmetic, comparison and conversion operators.

BUT :

What System.Int16 has that you can't have is this:

internal short m_value;

That's a native type (16-bit integer) member that holds the value. There is no 4-bit native type. The best you are going to be able to do is have a native byte in your implementation that will hold the value. You can write accessors that constrain it to the lower 4 bits, but you can't do much more than that. If someone creates a Nibble array, it will be implemented as an array of those values. As far as I know, there's no way to inject your implementation into that array. Similarly, if someone creates some other collection (eg, List<Nibble> ), then the collection will be of instances of your type, each of which will take up 8 bits.

However

You can create specialized collection classes, NibbleArray , NibbleList , etc. C#'s syntax allows you to provide your own collection initialization implementation for a collection, your own indexing method, etc.

So, if someone does something like this:

var nyblArray = new NibbleArray(32);
nyblArray[4] = 0xd;

Then your code can, under the covers, create a 16-element byte array, set the low nibble of the third byte to 0xd.

Similarly, you can implement code to allow:

var newArray = new NibbleArray { 0x1, 0x3, 0x5, 0x7, 0x9, 0xa};

or

var nyblList = new NibbleList { 0x0, 0x2, 0xe};

A normal array will waste space, but your specialized collection classes will do what you are talking about (with the expense of some bit-twizzling).

The closest you can get to what you want is to use an indexer:

// Indexer declaration
public int this[int index]
{
    // get and set accessors
}

Within the body of the indexer you can translate the index to the actual byte that contains your 4 bits.

The next thing you can do is operator overloading. You can redefine +, -, *...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM