在C#中数组的转换

6

我正在使用C#处理大型值类型数组。我希望能够将兼容值类型的数组进行强制转换,例如:

struct Color
{
    public byte R, G, B, A;
}

Color[] bitmap1 = ...;
uint[] bitmap2 = MagicCast(bitmap1);

我希望bitmap2能与bitmap1共享内存(它们具有相同的位表示)。我不想复制。是否有方法可以实现?

不行,即使在 C 语言中(你可以自由地处理内存和强制类型转换),这也是未定义行为。 - Adriano Repetti
@Adriano 只有在您取消引用转换后的数组时才会出现UB。如果在取消引用之前将数组转换回Color*,则是有效的。 - kaalus
@kaalus 确实,但相当没用 ;) - Adriano Repetti
请注意,内部使用“只读长整型值(readonly long value)”来表示“颜色(Color)”。 - John Alexiou
请注意,问题中包含4个字节字段的“Color”的定义。如果问题涉及“System.Drawing.Color”类型,则答案可能会有所不同,可能是不可能的。 - Lasse V. Karlsen
显示剩余2条评论
6个回答

4
你可以通过使用Structlayouts来覆盖这两个数组来实现这一点。 虽然这里有许多答案写了如何使颜色结构也有一个uint,但这是关于“数组”转换的: 这是邪恶的。
public static class ColorExtension
{
    public static uint[] GetUInts(this Color[] colors)
    {
        if(colors == null) throw new ArgumentNullException("colors");
        Evil e = new Evil { Colors = colors};
        return e.UInts;
    }

    [StructLayout(LayoutKind.Explicit)]
    struct Evil
    {
        [FieldOffset(0)]
        public Color[] Colors;
        [FieldOffset(0)]
        public uint[] UInts;
    }
}

这基本上有两个数组指向相同的内存位置。

所以如果你想从颜色数组中获取无符号整数数组,可以按照以下方式进行:

Color[] colors =  ...

uint[] uints = colors.GetUInts();

如评论中所提到的,您可能需要显式声明颜色结构,否则运行时可能会在顺序上出现问题导致一些“奇怪”的无符号整数值...

[StructLayout(LayoutKind.Sequential)] // Or explicit, but i bellieve Sequential is enough.
struct Color
{
      public byte R;

      public byte G;

      public byte B;

      public byte A;
}

不要试图调试它... 这是恶劣行为的其中一个原因:

+1 很好的发现,而且这是目前为止唯一一个给 OP 提供获取 uint[] 而不是 uint* 的答案! - Adriano Repetti
我一定会稍后测试这个。最初的结构布局很明显,但我从未想过在另一层上进行操作。赞。 - dodexahedron
我原以为当被覆盖的字段是对象引用时,这是不允许的,但它居然奏效了。太好了!(是的,邪恶,邪恶!) - Lasse V. Karlsen
1
调试器不太喜欢这种方法。在尝试评估时,它很难理解正在发生的事情。 - David Heffernan
我不确定为什么人们称它为邪恶。颜色是由字节组成的,因此有效地说,颜色数组就是一个字节数组,大小增加了4倍。我可以轻松地将单个颜色视为4个字节,并且在某些情况下非常有用,不确定为什么对整个数组执行相同操作会如此困难和邪恶。我认为语言应该具备这样的功能。任何没有私有成员且所有成员都是POD的结构体都应该能够转换为满足相同条件的任何其他结构体 - 只要大小匹配,这也应该扩展到数组。 - kaalus
显示剩余3条评论

1

是的,你可以这样做,但是根据发布的结构体,它可能只是偶然起作用。

你应该给你的结构体添加一些属性,以确保没有添加填充。

无论如何,这里有一个LINQPad程序来演示:

unsafe void Main()
{
    Color[] c = new Color[2]
    {
        new Color { R = 255, G = 0, B = 0, A = 0 },
        new Color { R = 0, G = 255, B = 0, A = 0 }
    };
    c.Dump();


    fixed (byte* p = &c[0].R)
    {
        uint* i = (uint*)p;
        *i = 0x11223344;
        *(i + 1) = 0x55667788;
    };
    c.Dump();
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Color
{
    public byte R, G, B, A;
}

这将会输出两个颜色,其中第二次输出时已经被修改过。

0

你不是想把你的颜色对象看作C联合体吗?

这样做:

[StructLayout(LayoutKind.Explicit)]
struct Color
{
   [FieldOffset(0)]
   byte R;
   [FieldOffset(1)]
   byte G;
   [FieldOffset(2)]
   byte B;
   [FieldOffset(3)]
   byte A;

   [FieldOffset(0)]
   uint Value;
}

如果你想从一个Color[]获取IEnumerable而不需要复制,只需执行以下操作:

var converted = colorArray.Select( c => c.Value );

不应该制作任何副本,因为它应该只是将值投影到数组中。

你甚至可以更进一步,为你的新类型添加显式和隐式转换运算符。

struct Color
{
   //...As before...

   public static implicit operator uint(Color input)
   {
      return input.Value;
   }

   public static explicit operator uint(Color input)
   {
      return input.Value;
   }
}

然后你就可以像下面这样做:

Color a = new Color();
uint b = a;        //Completely valid, with the implicit cast.
uint c = (uint)a;  //Also valid, but the implicit cast makes it unnecessary.

然而,这并不涵盖数组强制转换。


ToArray() 方法创建一个新的数组,可以看作是某种副本 ^^ - T_D
他想将其转换为uint[],这是一个指针。调用ToArray()会创建该指针。它与强制转换相同。任何linq方法都会导致额外的指针,无论如何。仅仅调用select而不使用ToArray()并不能节省任何东西,并且只会得到一个IEnumerable<uint>而不是uint[](当然,uint[]也是一个IEnumerable<uint>)。 - dodexahedron
我在这里看到了Clone的使用:http://referencesource.microsoft.com/#mscorlib/system/reflection/emit/methodbuilder.cs,感觉像是复制 :) 但我认为我们争论的是OP所指的复制意思。这取决于他/她。 - T_D
1
呸,这就是我因为假设它是一个简单的投影而得到的结果。无论如何,我已经添加了更多的答案,可能对OP的情况更有效。 - dodexahedron
问题在于,如果原帖作者需要将该uint[]传递给某个外部函数(看起来很可能),那么他很可能会被迫复制一份,除非他想编写一个使用指向原始uint的指针来写入数组的不安全方法。 - dodexahedron

0

这应该可以做到你想要的,你改变一个,它会同时改变引用。

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    [StructLayout(LayoutKind.Explicit)]
    struct Color
    {
        [FieldOffset(0)]
        public uint value;

        [FieldOffset(0)]
        public byte R;
        [FieldOffset(1)]
        public byte G;
        [FieldOffset(2)]
        public byte B;
        [FieldOffset(3)]
        public byte A;

        static public implicit operator uint(Color c)
        {
            return c.value;
        }
    }

    class Program
    {
        static unsafe void Main(string[] args)
        {
            Color[] colors = new Color[] { new Color { R = 255 }, new Color { B = 255 } };
            Console.WriteLine(colors[0]);
            Console.WriteLine(colors[1]);
            fixed (Color* thisPtr = &colors[0])
            {
                ((uint*)thisPtr)[0] = 2;
                ((uint*)thisPtr)[1] = 4;
                Console.WriteLine(colors[0]);
                Console.WriteLine(colors[1]);
            }
            Console.ReadKey();
        }
    }
}

-1

看起来你想要同一件事情的不同表示。4个字节的颜色或一个uint。这可以使用C语言中的union来完成。在C#中没有本地的union结构,但是可以通过StructLayout和FieldOffset属性重现功能。像这样:

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
struct Color
{
  [FieldOffset(0)]
  public byte R;

  [FieldOffset(1)]
  public byte G;

  [FieldOffset(2)]
  public byte B;

  [FieldOffset(3)]
  public byte A;

  [FieldOffset(0)]
  public uint value;
}

您可以使用Color数组bitmap1选择所需内容,例如:
var allRedBytes = bitmap1.Select(c => c.R);
var allUInts = bitmap1.Select(c => c.value);

只要您不在LINQ查询中调用.ToArray或.ToList,就不会产生位图数组的新副本。例如,当您在foreach循环中使用查询时,这些查询仅描述如何遍历数组。

FieldOffset可以模拟联合,但是由于它是值类型,所以这段代码会进行复制。 - Adriano Repetti
我不知道C#如何处理那些东西,但你有任何证据支持你的说法吗?为什么我是唯一一个被踩的人,尽管大多数人都给出了相同的答案^^开个玩笑。 - T_D
bitmap1.Select(c => c.R) 将创建源值的枚举。您无法修改它们(因为它们是值类型,那么您会得到一份副本)。只需要尝试。此外,它是 IEnumerable<uint> 而不是所需的 uint[](如果添加 ToArray(),则会创建一个全新的副本,与原始副本没有任何关系)。 - Adriano Repetti
“我希望bitmap2与bitmap1共享内存[...]我不想复制” OP所说的。因此,枚举是一个有效的选择。尽管我认为OP要求的强制转换并不是他的主要目标。 - T_D
但它们都被声明为数组。如果你必须使用这些数据(除非性能是一个问题和/或类型是固定的,因为它必须被另一个不可改变的函数使用),那么这不是什么问题,但如果你必须修改它们,那么它们(枚举)就不起作用了。 - Adriano Repetti
在这种情况下,您当然会修改原始的bitmap1数组。我的LINQ只是一个示例,因为我们不知道OP想要做什么 :) - T_D

-1

试试这个:

unit[] bitmap2 = bitmap1.Select((c)=>c.ToUint32()).ToArray();

你可能需要创建一个扩展方法来将Color转换为uint

public static uint ToUint32(this Color color) { ... }

它不起作用(他的结构体和uint之间没有转换)。即使我们有一个转换,它也会复制,我们可以假设OP知道如何做到这一点,但他正在要求避免这种情况。 - Adriano Repetti
根据定义,值类型在进行乘法运算时会创建一个副本。 - John Alexiou
不,如果(仅仅想象一下,实际上是不可能的)有一种方法可以将Color访问为UInt32,那么就不需要复制。这就是OP所要求的。 - Adriano Repetti
在内部,Color 是一个 long 而不是 uint。因此,您可以获取指向私有的 Color.value 字段的指针。 - John Alexiou

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