将 sbyte[] 转换为 bool[],将 char[] 转换为 short[]

12

有没有一种明确的方法可以将 sbyte[]byte[] 强制转换为 bool[]

  • char[] 转换为 short[]/ushort[] 是否可行?

在CIL中,您经常会看到这样的内容:

stelem Type sbyte (ldloc pArray) ldc_i4 1 ldc_i4 0 

现在有一个一维数组 pArray,类型为 bool[],它执行了 pArray[1] = true 操作。我想在 C# 中复制这个操作,可以通过以下方式实现:

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

很遗憾,C#编译器不允许这样做。


6
@Nick,请下次花点时间格式化你的问题。 - Rand Random
“explicitly cast/coerce” 是什么意思?你有 Convert.ToBoolean(byte) - Rahul
@Rahul 引用类型之间的转换应该保持引用不变。在运行时,char[] 和 ushort[] 是相同的,因此你可以这样做(原则上)。 - Nick
5个回答

17

未记录的技巧,风险自负:

(例如在这里和许多其他地方显示)

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

然后:

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

请注意,这个技巧与泛型完全不兼容。没有Conv<T, U>
当源和目标元素的大小相同时(sizeof(sbyte) == sizeof(bool)),这个技巧效果最好。否则会有一些限制(在上面链接的问题中描述)。

1
我喜欢那个答案。我冒昧地重命名了字段,这样更清楚数组1和2是什么。 - Patrick Hofman
@PatrickHofman 从技术上讲,没有 InOut... 它们处于同一级别... 您可以执行 Out = bytes 然后 .In... 但没有问题... 我正在考虑将它们重命名为 SourceTarget - xanatos
那也可以 ;) - Patrick Hofman
泛型无论如何都会很疯狂吧!毕竟没有地方可以使 sizeof(T) == sizeof(U)... 对吧? - Nick
@Nick 可能是 JIT 编译器在某个地方注意到有一些“不好”的事情正在发生。 - xanatos
显示剩余3条评论

9
你可以使用 新的 Span<T>MemoryMarshal 类型 来实现此操作。
请注意,这仅适用于最近版本的 C#,目前您必须使用 NuGet 包来提供库,但这将会改变。
例如,要将 char 数组“转换”为 short 数组,您可以编写以下代码:
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'

这种方法已有记录,并且不会复制任何数据 - 由于设计的缘故,它也非常高效。
请注意,这也适用于结构体:
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

同时请注意,这也允许您做一些可疑的事情,比如:

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

啊,这个回答很有趣,谢谢。不过我宁愿不导入这个库。 - Nick
@Nick 在未来的 .Net 版本中,您将不需要导入它,但是现在需要使用 NuGet 包。 - Matthew Watson
马修,这个NuGet包的作者们是如何实现这个的? - Nick
@Nick,实际上这是由微软编写的 - 他们将其放在NuGet上,以便人们在它被添加到标准.Net库之前就可以使用它。因为它是由微软编写的,所以它可以做一些普通库无法做到的事情 - 如果您查看我在答案顶部链接的文章并搜索“How Is Span<T> Implemented?”,您会找到一些关于它的详细信息。 - Matthew Watson
很棒的文章,谢谢。我仍然不清楚特殊的JIT内置span是如何实现的。据我所知,在CIL规范中没有提到span。但这些span真的很棒,我想引用数组的一部分已经很久了! - Nick

1

这只是一个部分回答。

技巧:

C#编译器和运行时并不完全同意各种数组类型之间的可转换性(正如您在问题中暗示的那样)。因此,您可以避免向编译器询问并将强制转换推迟到运行时,通过使用 System.Array(或 System.ObjectSystem.Collections.IEnumerable 等)进行操作。

例如:

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));
  }
}

在线尝试(tio.run)

输出:

249
253
8
11

如果您避免在上面的代码中使用中间(Array),C#编译器将告诉您转换是不可能的。


这实际上非常有趣。你的例子是可以运行的,但如果你尝试将其转换为bool[]而不是byte[],它就会停止工作。我很惊讶你的版本竟然编译并运行了! - Nick
@Nick 你可以在为什么将C#数组转换为对象时会丢失类型信息?中了解更多。这已经是老问题了,但我同意当你看到它时可能会有点震惊。这种方法只适用于某些情况,特别是sbyte-byteshort-ushort等情况。因此我说这个答案是“部分的”。 - Jeppe Stig Nielsen

0
你可以使用扩展方法,像这样:
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;
        }
    }
}

然后就像这样使用它:

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

在C# 8中,可能会有所谓的“扩展一切”(Extension Everything),您将能够定义自己的扩展转换,包括显式和隐式转换。
语法将类似于以下内容:
public extension class ByteExt extends Byte[]
{
    public static explicit operator bool[](byte[] bytes)
    {
         // Same implementation as before
    }
}

可以像这样使用:

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

2
这将创建一个新的数组,我认为在引用类型之间进行转换时很奇怪(因为不应该改变引用的值)。这也是浪费的,因为运行时数组是相同的。 - Nick
在运行时,数组相同是什么意思? - AustinWBryan
1
一个字节数组(byte[])只是一个布尔数组(bool[]),但C#编译器添加了额外的检查,不允许你执行(byte[])(new bool[3])。 - Nick
主要问题在于,如果我执行mybytes [0] = 0;,那么mybools [0]仍然是true,而不是false。 - Nick
如果您查看我在问题中提供的示例,它们只能使用共享引用工作。无论如何,按定义,引用类型之间的转换不会改变引用。此外,我只要求处理 ushorts/chars 和 bytes/bools。请参考 xanatos 的好答案。 - Nick
显示剩余4条评论

-1

使用 Array.ConvertAll 如下:

// 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));

这其实不是一个类型转换,因为你要创建一个新数组(我想)。我想要使用同一个数组。 - Nick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接