I have a project that I am working on where I require a data type that doesn't exist in the .NET BCL - an Unsigned Int24. For calculations that I am doing, the 4th byte in an int32, even when set to all zeroes, screws up my results.
EDIT: I'm doing bitwise circular shifts on a 24bit integer space that is limited to only 24bits. If the rotation is performed at 24bit level on a 32bit number the result is wildly incorrect.
Does anyone know of any third party implementations of this data type that are available?
Thanks!
Implementing Int24 isn't hard (honest!). But we need to know more about why you need to implement it. @nneonneo wonders if you're trying to interface with a native library that uses 24-bit integers. If that's the case then you can be done by doing something like this:
[StructLayout(LayoutKind.Sequential)]
public readonly unsafe struct UInt24
{
private readonly Byte b0;
private readonly Byte b1;
private readonly Byte b2;
public UInt24(UInt32 value)
{
this.b0 = (byte)( (value ) & 0xFF );
this.b1 = (byte)( (value >> 8) & 0xFF );
this.b2 = (byte)( (value >> 16) & 0xFF );
}
public unsafe Byte* Byte0 { get { return &_b0; } }
public UInt32 Value { get { return _b0 | ( _b1 << 8 ) | ( _b2 << 16 ); } }
}
This type really does occupy only 3 bytes, at least as far as .NET's concerned:
UInt24 u24 = new UInt24( 16_777_215 );
Console.WriteLine( "Marshal.SizeOf<UInt24>(u24) == {0:N0}", Marshal.SizeOf<UInt24>(u24) );
Console.WriteLine( "sizeof(UInt24) == {0:N0}", sizeof(UInt24) );
// Prints:
//
// Marshal.SizeOf<UInt24>(u24) == 3
// sizeof(UInt24) == 3
Anyway, you'd be using it like so:
[DllImport("foo.dll")]
private static unsafe void SomeImportedFunction(byte* uint24Value);
public static unsafe void Foo()
{
UInt24 uint24 = new UInt24( 123 );
SomeImportedFunction( uint24.Byte0 );
}
Modifying the class for big-endian, signed Int24, or the bitshifting operators that the OP is asking for, is an exercise left up to the reader.
I recently found myself needing to use a UInt24, so I took my existing impl and fleshed it out by adding (only safe) implicit
conversions (to wider types, from narrower types), IEquatable<UInt24>
, IComparable<UInt24>
, etc). My projects are (still) targeting only .NET Framework 4.8 and .NET 6, so I can't use INumber<T>
for now, but here:
[CLSCompliant( isCompliant: false )] // <-- Because UInt32 is not CLS-compliant, *grumble*.
[StructLayout( LayoutKind.Sequential )]
public readonly struct UInt24 : IEquatable<UInt24>, IComparable<UInt24>
// Implementing .NET 7's new `INumber<UInt24>` is an exercise for the reader.
{
public static readonly UInt24 MaxValue = new UInt24( 0xFF, 0xFF, 0xFF );
public static readonly UInt24 MinValue = new UInt24( 0x00, 0x00, 0x00 );
private const UInt32 _maxValue = 0x00FFFFFF; // 16,777,215 // This is the *inclusive* upper-bound (i.e. max-value).
//
public static implicit operator UInt32( UInt24 self ) => self.Value;
public static implicit operator UInt24( UInt16 u16 ) => new UInt24( value: u16 );
//
private readonly Byte b0;
private readonly Byte b1;
private readonly Byte b2;
public UInt24( Byte b0, Byte b1, Byte b2 )
{
this.b0 = b0;
this.b1 = b1;
this.b2 = b2;
}
public UInt24( UInt32 value )
{
if( value > _maxValue ) throw new ArgumentOutOfRangeException( paramName: nameof(value), actualValue: value, message: $"Value {value:N0} must be between 0 and {_maxValue:N0} (inclusive)." );
//
this.b0 = (Byte)( ( value ) & 0xFF );
this.b1 = (Byte)( ( value >> 8 ) & 0xFF );
this.b2 = (Byte)( ( value >> 16 ) & 0xFF );
}
#if UNSAFE
public unsafe Byte* Byte0 => &_b0;
#endif
private Int32 SignedValue => ( this.b0 | ( this.b1 << 8 ) | ( this.b2 << 16 ) );
public UInt32 Value => (UInt32)this.SignedValue;
//
#region Struct Tedium + IEquatable<UInt24> + IComparable<UInt24>
public override String ToString () => this.Value.ToString();
public override Int32 GetHashCode() => this.SignedValue;
public override Boolean Equals ( [NotNullWhen(true)] Object? obj ) => obj is UInt24 other && this.Equals( other: other );
public Boolean Equals ( UInt24 other ) => this.Value.Equals ( other.Value );
public Int32 CompareTo ( UInt24 other ) => this.Value.CompareTo( other.Value );
public static Int32 Compare ( UInt24 left, UInt24 right ) => left.Value.CompareTo( right.Value );
//
public static Boolean operator==( UInt24 left, UInt24 right ) => left.Equals( right );
public static Boolean operator!=( UInt24 left, UInt24 right ) => !left.Equals( right );
public static Boolean operator> ( UInt24 left, UInt24 right ) => Compare( left, right ) > 0;
public static Boolean operator>=( UInt24 left, UInt24 right ) => Compare( left, right ) >= 0;
public static Boolean operator< ( UInt24 left, UInt24 right ) => Compare( left, right ) < 0;
public static Boolean operator<=( UInt24 left, UInt24 right ) => Compare( left, right ) <= 0;
#endregion
}
Because it supports implicit conversions from UInt16
values - and implicit conversions to UInt32
values - it means you don't need to use ctor calls, observe:
UInt24 u24_1 = new UInt24( 16_777_215 ); // <-- This uses the UInt32 ctor overload, which is OK here because 16,777,215 is <= UInt24.MaxValue.
UInt24 u24_2 = 2_000_000_000; // <-- The compiler flags this with CS0266 because an integer literal value of 2 billion has a type of `Int32`, but `UInt24` does not define implicit conversion from `Int32` - only `UInt16`.
UInt24 u24_3 = 65535; // <-- This is fine, because a literal of 65535 is typed as UInt16 by the compiler, which is a defined implicit conversion.
UInt32 asUnsigned32BitInteger = u24_1; // <-- This is also OK, because an implicit conversion to UInt32 is defined on UInt24.
Implicit conversion from Int16
to UInt24
is not defined because Int16
represents -32768
to 32767
, whereas UInt24
represents 0
to 16777215
- so some Int16
values cannot be meaningfully represented by UInt24
hence why it's disallowed.
Implicit conversion from UInt24
to Int32
can be defined, because every UInt24
value (0
to 16.7m
) can also be represented by Int32
(-2bn
to +2bn
), however it made more sense to me to define UInt24
to UInt32
to retain the unsigned theme going on here.
C# 12 allows us to slap \[InlineArray(Int32)\]
onto a single-field struct
and the compiler/runtime will treat it as an array of that struct
...
...which got me thinking that an interopable UInt24
type could then be defined as an inline array of 3 bytes, thus allowing a struct
to be type-punnable as Byte[3]
or Span<Byte>(3)
- just like the OP's question.
(I should note that one of C#/.NET's leads, David Fowler, urges only doing this if you have a "very advanced scenario" such as demoing new language features on X/Twitter, I guess 😺)
Like so:
void Main()
{
UInt24 secretlyImAnArray = default;
for( Int32 i = 0; i < UInt24.Length; i++ )
{
secretlyImAnArray[i] = (Byte)i;
}
Console.WriteLine( "UInt24.ToString(): {0}", secretlyImAnArray ); // Prints only "0x00"
foreach( var b in secretlyImAnArray ) // `var b` is correctly inferred to `Byte b` by Roslyn.
{
Console.WriteLine( b ); // Prints "0", "1", "2"
}
//
Span<Byte> asSpan = secretlyImAnArray;
Console.WriteLine( "asSpan.ToString(): {0}", asSpan.ToString() ); // Prints "asSpan.ToString(): System.Span<Byte>[3]"
}
[InlineArray( length: UInt24.Length )]
public struct UInt24
{
public const Int32 Length = 3;
public Byte b;
//
public override String ToString() => "0x" + this.b.ToString( "X2", CultureInfo.CurrentCulture );
}