Ein eigener Datentyp (Teil 1)

Ein eigener Datentyp kann dann sinnvoll sein, wenn es Voraussetzung ist, Daten in ein Gerät zu schreiben bei denen der Speicherplatz begrenzt ist oder wenn es notwendig ist einen Typen mit einer ganz speziellen Interaktion zu besitzen. Aber wie kann man einen eigenen Datentyp mit sämtlichen Funktionalitäten wie z.B. die eines Integers erzeugen? Nun eigentlich ist das gar nicht so schwer wie man vielleicht denkt, denn .NET bietet dafür bereits einige Schnittstellen an und der andere Teil sind überwiegend die korrekten Operator-Überladungen. In Rahmen dieser Mini-Serie möchte ich einen eigenen 24-Bit großen Integer erstellen, wie man ihn z.B. bei einigen Feldgeräten benötigt. Am Ende des letzten Teils werde ich anschließend den Quellcode sowohl für die Signed, als auch für die Unsigned Variante dieses Typen anhängen. Aber beginnen wir nun mit den Int24-Typen.

Zunächst einmal muss klar sein, was der Unterschied zwischen einer Struktur und einer Klasse ist,  dies wird hier allerdings vorausgesetzt. Und das ist auch eigentlich schon alles was notwendig ist, um den Typen zu erstellen. Wir werden ebenfalls die selben Attribute sowie Interfaces wie ein Int32 verwenden, um die Kompatibilität zu maximieren. 

using System;
using System.Runtime.InteropServices;

[Serializable, StructLayout(LayoutKind.Sequential), ComVisible(true)]
public struct Int24 : IComparable, IFormattable, IConvertible, IComparable<Int24>, IEquatable<Int24>
{

}

Wie hier zu sehen, ist der Typ mit einigen Interfaces sowie drei Attributen bereits versehen. Wer an dieser Stelle den ReSharper nutzt kann nun alle Methoden mit Standardaktionen implementieren lassen, andernfalls wird dies nach und nach durch diese Serie erfolgen. Zunächst einmal benötigen wir nun das „Value“, da am Ende unsere Instanz repräsentiert. Da wir hier nicht unnötig Speicher verschwenden wollen – für den Fall, dass das ganze auch auf einem minimalisitischen System ausgeführt wird – verwenden wir hier also ein 3-Byte großes Array. Für einen vereinfachten Zugriff benötigen wir auch noch die Möglichkeit diesen Wert ggf. von außen einzusehen und zu modifizieren. Außerdem müssen wir noch den Minimal- sowie den Maximalwert unseres Int24 angeben (für die Useability).

private byte[] _value;

public const int IntMaxValue = 8388607;
public const int IntMinValue = -8388608;

public int Value
{
    get
    {
        if (_value == null)
            _value = new byte[] { 0, 0, 0 };

        return BitConverter.ToInt32(new byte[] { _value[0], _value[1], _value[2], 0 }, 0);
    }
    set
    {
        if (value > IntMaxValue || value < IntMinValue)
            throw new OverflowException("Value was either too large or too small for an Int24.");

        byte[] bytes = BitConverter.GetBytes(value);
        _value = new[] { bytes[0], bytes[1], bytes[2] };
    }
}

Weiter geht es mit den Konstruktoren unseres Datentypen, hier sollte ebenfalls die Möglichkeit bestehen ggf. einen Standardwert direkt festsetzen zu können. Sobald die Konstruktoren bestehen, könnte ggf. auch noch ein Int24MaxValue sowie Int24MinValue definiert werden.

public static Int24 MaxValue { get { return new Int24(IntMaxValue); } }
public static Int24 MinValue { get { return new Int24(IntMinValue); } }

public Int24(int value = 0) 
    : this()
{
    if (value > IntMaxValue)
        value = value - 16777216;

    if (value > IntMaxValue || value < IntMinValue)
        throw new OverflowException("Value was either too large or too small for an Int24.");

    byte[] bytes = BitConverter.GetBytes(value);
    _value = new[] { bytes[0], bytes[1], bytes[2] };
}

public Int24(byte[] value)
    : this()
{
    if (value == null || value.Length != 3)
        throw new OverflowException("An Int24 requires exactly 3 bytes.");

    _value = value;
}

Somit haben wir jetzt bereits ein solides Grundgerüst erstellt. Im nächsten Teil werden wir die Operatoren implementieren, um auch direkte Wertezuweisungen usw. vornehmen zu können.