12

Is there anyway to explicitly cast/coerce

  • sbyte[] or byte[] to a bool[]
  • char[] to a short[]/ushort[]

In CIL you regularly see something such as

stelem Type sbyte (ldloc pArray) ldc_i4 1 ldc_i4 0 

which is doing pArray[1] = true where pArray is a one-dimensional array of type bool[]. I want to replicate this in c# by doing

(sbyte[])pArray[1] = 1;

Unfortunately this is not allowed by the C# compiler.

Nick
  • 920
  • 1
  • 7
  • 21
  • 6
    @Nick pls for the next time invest some time to format your question – Rand Random May 23 '18 at 09:21
  • what you mean by `explicitly cast/coerce`? You have `Convert.ToBoolean(byte)` – Rahul May 23 '18 at 09:22
  • @Rahul a cast between reference types should keep the reference the same. At runtime a char[] and a ushort[] are identical, so you can do this (in principle). – Nick May 23 '18 at 09:59

5 Answers5

17

Undocumented trick, play at your own risk:

(shown for example here and in many other places)

[StructLayout(LayoutKind.Explicit)]
public struct ConvSByteBool
{
    [FieldOffset(0)]
    public sbyte[] In;
    [FieldOffset(0)]
    public bool[] Out;
}

and then:

var bytes = new sbyte[] { -2, -1, 0, 1, 2 };
var conv = new ConvSByteBool { In = bytes }.Out;
bool b1 = conv[0]; // true
bool b2 = conv[1]; // true
bool b3 = conv[2]; // false
bool b4 = conv[3]; // true
bool b5 = conv[4]; // true

Note that this trick is tottally incompatible with generics. No Conv<T, U>!

The trick works best when the size of the element in source and target is the same (sizeof(sbyte) == sizeof(bool)). Otherwise there are some limitations (described in the linked question above).

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • 1
    I love that answer. I took the liberty to rename the fields, so it is more clear what array 1 and 2 are. – Patrick Hofman May 23 '18 at 09:32
  • @PatrickHofman Technically there is no `In` and `Out`... They live at the same level... You could do `Out = bytes` and then `.In`... but no problem... I was thinking of renaming them to `Source` and `Target` – xanatos May 23 '18 at 09:33
  • That will do too ;) – Patrick Hofman May 23 '18 at 09:33
  • Generics would be insane anyway surely! There's no where sizeof(T) == sizeof(U)... right? – Nick May 23 '18 at 09:55
  • @Nick The first version was a `struct Conv`, then `new Conv { In = myArray }.Out`, but the program explodes in a strange way. And as written the code works even for types of different size. You can read a `byte[]` as if it was a `double[]` for example. The `.Length` is wrong (because it is the `.Length` of the `byte[]`) – xanatos May 23 '18 at 09:56
  • I remember doing something similar years ago and totally forgot. +1 for your awesome reminder =) – Danielle Summers May 23 '18 at 09:57
  • Ah good point, you could convert from int32 to int64, and half the length. Why does the Conv original not work? – Nick May 23 '18 at 10:00
  • @Nick Probably somewhere the JIT compiler notices that there is something "bad" going on – xanatos May 23 '18 at 10:03
9

You can use the new Span<T> and MemoryMarshal types to do this.

Note that this is only available with recent versions of C#, and you have to use a NuGet package to provide the library at the moment, but that will change.

So for example to "cast" a char array to a short array, you can write code like this:

var         charArray  = new char[100];
Span<short> shortArray = MemoryMarshal.Cast<char, short>(charArray);

charArray[0] = 'X';
Console.WriteLine(charArray[0]); // Prints 'X'
++shortArray[0];
Console.WriteLine(charArray[0]); // Prints 'Y'

This approach is documented and does not make any copies of any data - and it's also extremely performant (by design).

Note that this also works with structs:

struct Test
{
    public int X;
    public int Y;

    public override string ToString()
    {
        return $"X={X}, Y={Y}";
    }
}

...

var testArray = new Test[100];
Span<long> longArray = MemoryMarshal.Cast<Test, long>(testArray);

testArray[0].X = 1;
testArray[0].Y = 2;

Console.WriteLine(testArray[0]); // Prints X=1, Y=2

longArray[0] = 0x0000000300000004;

Console.WriteLine(testArray[0]); // Prints X=4, Y=3

Also note that this allows you to do some suspect things, like this:

struct Test1
{
    public int X;
    public int Y;

    public override string ToString()
    {
        return $"X={X}, Y={Y}";
    }
}

struct Test2
{
    public int X;
    public int Y;
    public int Z;

    public override string ToString()
    {
        return $"X={X}, Y={Y}, Z={Z}";
    }
}

...

var         test1 = new Test1[100];
Span<Test2> test2 = MemoryMarshal.Cast<Test1, Test2>(test1);

test1[1].X = 1;
test1[1].Y = 2;

Console.WriteLine(test1[1]); // Prints X=1, Y=2

test2[0].Z = 10; // Actually sets test1[1].X.

Console.WriteLine(test1[1]); // Prints X=10, Y=2
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Ah that's interesting answer, thank you. Would rather not import this library however. – Nick May 23 '18 at 10:03
  • @Nick You will not need to import it in a future version of .Net, but yes, for now it needs the NuGet package. – Matthew Watson May 23 '18 at 10:04
  • Matthew how do the writers of this nuget package achieve this? – Nick May 24 '18 at 08:57
  • @Nick It's actually written by Microsoft - they have put it on NuGet so people can use it before it's added to the standard .Net libraries. Because it's written by Microsoft, it can do things that normal libraries cannot - if you look at the article I linked at the top of my answer and search for `How Is Span Implemented?` you'll find some details about it. – Matthew Watson May 24 '18 at 09:03
  • Cool article thanks, I'm still confused how the special JIT intrinsic span is implemented. There is no mention of span in the CIL spec (as far as I know). These spans are totally awesome though, I've wanted to reference a segment of an array for ages! – Nick May 24 '18 at 15:39
1

This is a partial answer only.

Hack:

The C# compiler and the run-time do not agree entirely on what array types are convertible to each other (as you are hinting at in your question). So you can avoid asking the compiler and defer the cast to run-time, by going through System.Array (or System.Object or System.Collections.IEnumerable etc.).

An example:

using System;

static class P
{
  static void Main()
  {
    var a = new sbyte[] { -7, -3, 8, 11, };
    var hack = (byte[])(Array)a;
    Console.WriteLine(string.Join("\r\n", hack));
  }
}

Try it online (tio.run)

Output:

249
253
8
11

If you were to avoid the intermediate (Array) in the code above, the C# compiler would tell you that the cast is impossible.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • This is really interesting actually. Your example works, but if you try to cast to a bool[] rather than a byte[], it stops working. I'm shocked your version actually compiles and runs!. – Nick May 23 '18 at 19:27
  • @Nick You can read more at [Why does my C# array lose type sign information when cast to object?](https://stackoverflow.com/questions/1178973/) It is old news, but I agree you can get a bit shocked when you see it. This hack works only for some cases, in particular with `sbyte`-`byte`, `short`-`ushort`, etc. Hence I said this answer was "partial". – Jeppe Stig Nielsen May 23 '18 at 20:20
0

You can use an extension method, like this:

namespace ExtensionMethods
{
    public static class ByteExt
    {
        public static bool ToBool(this byte b) => b != 0;
        public static bool[] ToBoolArray(this byte[] bytes)
        {
            bool[] returnValues = new bool[bytes.Length];

            for (int i = 0; i < bytes.Length; i++)
                returnValues[i] = bytes[i].ToBool();

            return returnValues;
        }

        // Do same for sbyte
    }
    public static class CharExt
    {
        public static short[] ToBoolArray(this char[] chars)
        {
            short[] returnValues = new bool[chars.Length];

            for (int i = 0; i < chars.Length; i++)
                returnValues[0] = (short)chars[0];

            return returnValues;
        }
    }
}

Then just use it like this:

byte[] myBytes = new[] {1, 2, 5};
bool[] myBools = myBytes.ToBoolArray();

In C# 8, there will probably be what's called "Extension Everything", where you'll be able to define your own extension casts, both explicit and implicit.

The syntax will be something like this:

public extension class ByteExt extends Byte[]
{
    public static explicit operator bool[](byte[] bytes)
    {
         // Same implementation as before
    }
}

And can use it like this:

byte[] myBytes = new[] {1, 2, 5};
bool[] myBools = (bool[])myBytes;
AustinWBryan
  • 3,249
  • 3
  • 24
  • 42
  • 2
    This creates a new array which I think is strange when casting between reference types (which shouldn't change the value of the reference). It's also wasteful since the arrays are identical at runtime. – Nick May 23 '18 at 09:57
  • What do you mean by the arrays are identical at runtime? – AustinWBryan May 23 '18 at 10:02
  • 1
    an array of bytes[] is just an array of bool[], but the c# compiler has added additional checks which don't allow you to do (byte[])(new bool[3]) – Nick May 23 '18 at 10:05
  • Main problem here is that if I do mybytes[0] = 0; mybools[0] is still going to be true, not false. – Nick May 23 '18 at 10:07
  • Main problem here is that you never mentioned that you wanted the two arrays to share a reference in your question – AustinWBryan May 23 '18 at 11:32
  • If you look at the examples i gave in the question, they would only work with a shared reference. Anyway a cast between reference types doesn't change the reference by definition. Plus I only asked for ushorts/chars and bytes/bools. Look at xanatos for a good answer. – Nick May 23 '18 at 11:39
  • What examples? You showed the types you wanted to convert, some snippet from the CIL that I don't know how to read because why would I, and then `pArray[0] = true` where `pArray` is a `bool[]`, so that doesn't even involve casting. You actually asked for `sbyte`/`bool`, `byte`/`bool`, `char`/`short`, and `char`/`ushort`. I put that extra `ToBool` method as a might-as-well, and it's useful in it's own right. I already did see his answer and it is really good. Congrats. – AustinWBryan May 23 '18 at 11:50
  • https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/casting-and-type-conversions – Nick May 23 '18 at 11:56
  • I've added stuff to the question to make it clearer. But if you didn't understand the CIL, probably best not answer the question. The link I showed you explains that an explicit cast between reference types preserves the reference "A cast operation between reference types does not change the run-time type of the underlying object; it only changes the type of the value that is being used as a reference to that object. For more information, see Polymorphism. " – Nick May 23 '18 at 11:57
-1

Use Array.ConvertAll as follow:

// Input
sbyte[] sbyteArray = { 0, 1, 2, 0 };
byte[] byteArray = { 0, 1, 2, 0 };
// Result
bool[] boolArray1 = Array.ConvertAll(sbyteArray, (item) => Convert.ToBoolean(item));
bool[] boolArray2 = Array.ConvertAll(byteArray, (item) => Convert.ToBoolean(item));

// Input
char[] charArray = { 'A', 'B', 'C' };
// Result
short[] shortArray = Array.ConvertAll(charArray, (item) => Convert.ToInt16(item));
ushort[] ushortArray = Array.ConvertAll(charArray, (item) => Convert.ToUInt16(item));
Hossein Golshani
  • 1,847
  • 5
  • 16
  • 27
  • Not really a cast since you're making a new array (*I think*). I want to use the same array. – Nick May 23 '18 at 09:58