使用 LayoutKind.Explicit 进行布尔编组,这是否已损坏或按设计失败?
-
19-09-2019 - |
题
首先,布尔类型据说有一个四字节值的默认编组类型。所以下面的代码有效:
struct A
{
public bool bValue1;
public int iValue2;
}
struct B
{
public int iValue1;
public bool bValue2;
}
public static void Main()
{
int[] rawvalues = new int[] { 2, 4 };
A a = (A)Marshal.PtrToStructure(GCHandle.Alloc(rawvalues, GCHandleType.Pinned).AddrOfPinnedObject(), typeof(A));
Assert.IsTrue(a.bValue1 == true);
Assert.IsTrue(a.iValue2 == 4);
B b = (B)Marshal.PtrToStructure(GCHandle.Alloc(rawvalues, GCHandleType.Pinned).AddrOfPinnedObject(), typeof(B));
Assert.IsTrue(b.iValue1 == 2);
Assert.IsTrue(b.bValue2 == true);
}
显然,这些结构独立编组得很好。这些值按预期转换。然而,当我们通过像这样声明 LayoutKind.Explicit 将这些结构组合成一个“联合”时:
[StructLayout(LayoutKind.Explicit)]
struct Broken
{
[FieldOffset(0)]
public A a;
[FieldOffset(0)]
public B b;
}
我们突然发现自己无法正确地整理这些类型。以下是上述结构的测试代码以及它是如何失败的:
int[] rawvalues = new int[] { 2, 4 };
Broken broken = (Broken)Marshal.PtrToStructure(GCHandle.Alloc(rawvalues, GCHandleType.Pinned).AddrOfPinnedObject(), typeof(Broken));
Assert.IsTrue(broken.a.bValue1 != false);// pass, not false
Assert.IsTrue(broken.a.bValue1 == true);// pass, must be true?
Assert.IsTrue(true.Equals(broken.a.bValue1));// FAILS, WOW, WTF?
Assert.IsTrue(broken.a.iValue2 == 4);// FAILS, a.iValue1 == 1, What happened to 4?
Assert.IsTrue(broken.b.iValue1 == 2);// pass
Assert.IsTrue(broken.b.bValue2 == true);// pass
看到这句话是真的,真是太幽默了:(a.bValue1 != false && a.bValue1 == true && !true.Equals(a.bValue1))
当然,这里更大的问题是 a.iValue2 != 4,而不是 4 已更改为 1(大概是通过重叠的布尔值)。
所以问题是:这是一个错误,还是只是按照设计失败了?
背景:这是来自 使用 PInvoke 时,包含 bool 与 uint 的结构有什么区别?
更新:当您使用大整数值 (> 255) 时,这甚至更奇怪,因为只有用于布尔值的字节被修改为 1,从而将 b.bValue2 的 0x0f00 更改为 0x0f01。对于上面的 a.bValue1,它根本没有被转换,并且 0x0f00 为 a.bValue1 提供了一个错误值。
更新#2:
对于上述问题,最明显、最合理的解决方案是使用 uint 进行编组并公开布尔属性。真正通过“解决方法”解决问题是没有问题的。我主要想知道这是一个错误还是您期望的行为?
struct A
{
private uint _bValue1;
public bool bValue1 { get { return _bValue1 != 0; } }
public int iValue2;
}
struct B
{
public int iValue1;
private uint _bValue2;
public bool bValue2 { get { return _bValue2 != 0; } }
}
解决方案
它正在按设计工作。
这是正在发生的事情:
获取新的 int[] { 2, 4 } 并将其编组为 A、B、Broken 和 Broken2。最后一个与 Broken 相同,但字段顺序相反(先是 b,然后是 a)。
如果我们将整数编组到这些结构中,我们会在内存中得到以下值:
- A:1, 4
- 乙:2, 1
- 破碎的:2, 1
- 破碎2:1, 4
所以发生的事情如下:
- 当编组器遇到布尔值时,它的值为:布尔=(原始!= 0);
- 当有两个字段映射到同一个内存时,最后一个字段的规则获胜
因此,对于a,第一个int被转换为1,对于b,第二个int被转换为1,因为b是最后一个字段,其规则适用,因此第二个int被转换为1。《Broken2》的情况也是如此。
其他提示
带有注释的线'失败,WOW,WTF?失败的原因进行布尔比较的方式。据比较2至1:
IL_007e: ldc.i4.1
IL_007f: ldloca.s 3
IL_0081: ldflda valuetype Test/A Test/Broken::a
IL_0086: ldfld bool Test/A::bValue1
IL_008b: ceq
CEQ 的结束进行比较1〜bValue字节,其值为2。
有趣的是,的如果(broken.a.bValue1)的将测试 '真',因为它是不为零。
至于其他问题(broken.a.iValue2 == 4),它走了施用时I:
[MarshalAs (UnmanagedType.Bool)]
在结构既布尔字段。这可以确保在布尔值被编组为一个整数(在.NET 4个字节)。
这样看来earlNameless是正确的,如添加整数的另一种结构:
struct C
{
public int iValue1;
public int iValue2;
}
要联合的端似乎纠正问题的至少一部分。然而,这仍是有缺陷的,因为布尔将只考虑一个单字节值和证明是不可靠的。最后,最好的答案,我已经出来是使用自定义类型封送处理。
[Serializable]
[ComVisible(true)]
public struct BOOL : IComparable, IConvertible, IComparable<BOOL>, IEquatable<BOOL>, IComparable<bool>, IEquatable<bool>
{
private uint _data;
public BOOL(bool value) { _data = value ? 1u : 0u; }
public BOOL(int value) { _data = unchecked((uint)value); }
public BOOL(uint value) { _data = value; }
private bool Value { get { return _data != 0; } }
private IConvertible Convertible { get { return _data != 0; } }
#region IComparable Members
public int CompareTo(object obj) { return Value.CompareTo(obj); }
#endregion
#region IConvertible Members
public TypeCode GetTypeCode() { return Value.GetTypeCode(); }
public string ToString(IFormatProvider provider) { return Value.ToString(provider); }
bool IConvertible.ToBoolean(IFormatProvider provider) { return Convertible.ToBoolean(provider); }
byte IConvertible.ToByte(IFormatProvider provider) { return Convertible.ToByte(provider); }
char IConvertible.ToChar(IFormatProvider provider) { return Convertible.ToChar(provider); }
DateTime IConvertible.ToDateTime(IFormatProvider provider) { return Convertible.ToDateTime(provider); }
decimal IConvertible.ToDecimal(IFormatProvider provider) { return Convertible.ToDecimal(provider); }
double IConvertible.ToDouble(IFormatProvider provider) { return Convertible.ToDouble(provider); }
short IConvertible.ToInt16(IFormatProvider provider) { return Convertible.ToInt16(provider); }
int IConvertible.ToInt32(IFormatProvider provider) { return Convertible.ToInt32(provider); }
long IConvertible.ToInt64(IFormatProvider provider) { return Convertible.ToInt64(provider); }
sbyte IConvertible.ToSByte(IFormatProvider provider) { return Convertible.ToSByte(provider); }
float IConvertible.ToSingle(IFormatProvider provider) { return Convertible.ToSingle(provider); }
ushort IConvertible.ToUInt16(IFormatProvider provider) { return Convertible.ToUInt16(provider); }
uint IConvertible.ToUInt32(IFormatProvider provider) { return Convertible.ToUInt32(provider); }
ulong IConvertible.ToUInt64(IFormatProvider provider) { return Convertible.ToUInt64(provider); }
object IConvertible.ToType(Type conversionType, IFormatProvider provider) { return Convertible.ToType(conversionType, provider); }
#endregion
#region IComparable<bool> Members
public int CompareTo(BOOL other) { return Value.CompareTo(other.Value); }
public int CompareTo(bool other) { return Value.CompareTo(other); }
#endregion
#region IEquatable<bool> Members
public bool Equals(BOOL other) { return Value.Equals(other.Value); }
public bool Equals(bool other) { return Value.Equals(other); }
#endregion
#region Object Override
public override string ToString() { return Value.ToString(); }
public override int GetHashCode() { return Value.GetHashCode(); }
public override bool Equals(object obj) { return Value.Equals(obj); }
#endregion
#region implicit/explicit cast operators
public static implicit operator bool(BOOL value) { return value.Value; }
public static implicit operator BOOL(bool value) { return new BOOL(value); }
public static explicit operator int(BOOL value) { return unchecked((int)value._data); }
public static explicit operator BOOL(int value) { return new BOOL(value); }
public static explicit operator uint(BOOL value) { return value._data; }
public static explicit operator BOOL(uint value) { return new BOOL(value); }
#endregion
#region +, -, !, ~, ++, --, true, false unary operators overloaded.
public static BOOL operator !(BOOL b) { return new BOOL(!b.Value); }
public static bool operator true(BOOL b) { return b.Value; }
public static bool operator false(BOOL b) { return !b.Value; }
#endregion
#region +, -, *, /, %, &, |, ^, <<, >> binary operators overloaded.
public static BOOL operator &(BOOL b1, BOOL b2) { return new BOOL(b1.Value & b2.Value); }
public static BOOL operator |(BOOL b1, BOOL b2) { return new BOOL(b1.Value | b2.Value); }
#endregion
#region ==, !=, <, >, <=, >= comparison operators overloaded
public static bool operator ==(BOOL b1, BOOL b2) { return (b1.Value == b2.Value); }
public static bool operator !=(BOOL b1, BOOL b2) { return (b1.Value != b2.Value); }
#endregion
}