Personalización de la serialización de estructuras

A veces, las reglas de serialización predeterminadas para las estructuras no es exactamente lo que necesita. Los entornos de ejecución de .NET proporcionan unos puntos de extensión que permiten personalizar el diseño de la estructura y cómo se serializan los campos. La personalización del diseño de la estructura es compatible con todos los escenarios, pero la personalización de la serialización de campos solo se admite en escenarios en los que la serialización en tiempo de ejecución está habilitada. Si la serialización en tiempo de ejecución está deshabilitada, cualquier serialización de campos debe realizarse manualmente.

Nota:

En este artículo no se trata la personalización de la serialización para la interoperabilidad generada por el origen. Si usa la interoperabilidad generada por el origen para P/Invokes o COM, consulte personalización de la serialización.

Personalizar el diseño de estructuras

.NET proporciona el atributo System.Runtime.InteropServices.StructLayoutAttribute y la enumeración System.Runtime.InteropServices.LayoutKind para poder personalizar cómo se colocan los campos en la memoria. Las siguientes instrucciones lo ayudarán a evitar problemas comunes.

✔️ PLANTÉESE usar LayoutKind.Sequential siempre que sea posible.

✔️ USAR solo LayoutKind.Explicit en la serialización cuando la estructura nativa tenga un diseño explícito, como una unión.

❌ EVITE el uso de clases para expresar tipos nativos complejos a través de la herencia.

❌ EVITA usar LayoutKind.Explicit al serializar estructuras en plataformas que no son de Windows si tienes como destino runtimes anteriores a .NET Core 3.0. El runtime de .NET Core anterior a 3.0 no admite pasar estructuras explícitas por valor a funciones nativas en sistemas que no sean Windows Intel o AMD de 64 bits. Sin embargo, el entorno de ejecución admite pasar estructuras explícitas por referencia en todas las plataformas.

Personalización de la serialización de campos Booleanos

El código nativo tiene muchas representaciones Booleanas diferentes. En Windows, hay tres formas de representar valores booleanos. El entorno de ejecución no sabe la definición nativa de la estructura, por lo que se recomienda realizar una estimación sobre cómo serializar los valores Booleanos. El entorno de ejecución de .NET proporciona una manera de indicar cómo serializar el campo Booleano. En los ejemplos siguientes se muestra cómo serializar el tipo bool de .NET a diferentes tipos nativos Booleanos.

Los valores booleanos predeterminados se ordenan como un valor nativo de 4 bytes de Win32 BOOL como se muestra en el siguiente ejemplo:

public struct WinBool
{
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

Si desea ser explícito, puede usar el valor UnmanagedType.Bool para obtener el mismo comportamiento que el anterior:

public struct WinBool
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

Mediante los valores UnmanagedType.U1 o UnmanagedType.I1, puede indicar el entorno de ejecución para serializar el campo b como un tipo bool nativo de 1 byte.

public struct CBool
{
    [MarshalAs(UnmanagedType.U1)]
    public bool b;
}
struct CBool
{
    public bool b;
};

En Windows, puede usar el valor UnmanagedType.VariantBool para indicar al entorno de ejecución que serialice el valor Booleano a un valor VARIANT_BOOL de 2 bytes:

public struct VariantBool
{
    [MarshalAs(UnmanagedType.VariantBool)]
    public bool b;
}
struct VariantBool
{
    public VARIANT_BOOL b;
};

Nota

VARIANT_BOOL es diferente de la mayoría de los tipos booleanos de VARIANT_TRUE = -1 y VARIANT_FALSE = 0. Además, todos los valores que no son iguales a VARIANT_TRUE se consideran false.

Personalización de la serialización de campos de matriz

.NET también incluye algunos ejemplos de cómo personalizar la serialización de matrices.

De forma predeterminada, .NET serializa las matrices como un puntero a una lista contigua de elementos:

public struct DefaultArray
{
    public int[] values;
}
struct DefaultArray
{
    int32_t* values;
};

Si está interactuando con API de COM, es posible que deba serializar las matrices como objetos SAFEARRAY*. Puede usar System.Runtime.InteropServices.MarshalAsAttribute y el valor UnmanagedType.SafeArray para indicar al entorno de ejecución que serialice una matriz como SAFEARRAY*:

public struct SafeArrayExample
{
    [MarshalAs(UnmanagedType.SafeArray)]
    public int[] values;
}
struct SafeArrayExample
{
    SAFEARRAY* values;
};

Si necesita personalizar qué tipo de elemento se encuentra en SAFEARRAY, puede usar los campos MarshalAsAttribute.SafeArraySubType y MarshalAsAttribute.SafeArrayUserDefinedSubType para personalizar el elemento exacto de SAFEARRAY.

Si necesita serializar la matriz en contexto, puede usar el valor UnmanagedType.ByValArray para indicar el serializador con el fin de serializar la matriz en contexto. Cuando se usa esta serialización, también debe proporcionar un valor para el campo MarshalAsAttribute.SizeConst para el número de elementos de la matriz para que el entorno de ejecución pueda asignar correctamente espacio para la estructura.

public struct InPlaceArray
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] values;
}
struct InPlaceArray
{
    int values[4];
};

Nota

.NET no admite la serialización de un campo de matriz de longitud variable como un miembro de matriz flexible C99.

Personalización de la serialización de campos de cadena

.NET también proporciona una amplia variedad de personalizaciones para serializar los campos de cadena.

De forma predeterminada, .NET serializa una cadena como un puntero a una cadena terminada en un valor NULL. La codificación depende del valor del campo StructLayoutAttribute.CharSet en System.Runtime.InteropServices.StructLayoutAttribute. Si no se especifica ningún atributo, la codificación predeterminada es una codificación ANSI.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char* str;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Si necesita utilizar codificaciones diferentes para los distintos campos o simplemente prefiere ser más explícito en la definición de estructuras, puede usar los valores UnmanagedType.LPStr o UnmanagedType.LPWStr en un atributo System.Runtime.InteropServices.MarshalAsAttribute.

public struct AnsiString
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string str;
}
struct AnsiString
{
    char* str;
};
public struct UnicodeString
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public string str;
}
struct UnicodeString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Si desea serializar las cadenas mediante la codificación UTF-8, puede usar el valor UnmanagedType.LPUTF8Str en MarshalAsAttribute.

public struct UTF8String
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string str;
}
struct UTF8String
{
    char* str;
};

Nota

El uso de UnmanagedType.LPUTF8Str requiere .NET Framework 4.7 (o versiones posteriores) o .NET Core 1.1 (o versiones posteriores). No está disponible en .NET Standard 2.0.

Si está trabajando con API de COM, es posible que deba serializar una cadena como BSTR. Mediante el valor UnmanagedType.BStr, se puede serializar una cadena como BSTR.

public struct BString
{
    [MarshalAs(UnmanagedType.BStr)]
    public string str;
}
struct BString
{
    BSTR str;
};

Cuando se usa una API basada en WinRT, es posible que deba serializar una cadena como HSTRING. Mediante el valor UnmanagedType.HString, se puede serializar una cadena como HSTRING. HSTRING La serialización solo se admite en entornos de ejecución con compatibilidad integrada con WinRT. La compatibilidad con WinRT se quitó en .NET 5, por lo que HSTRING no se admite la serialización en .NET 5 o versiones posteriores.

public struct HString
{
    [MarshalAs(UnmanagedType.HString)]
    public string str;
}
struct BString
{
    HSTRING str;
};

Si la API requiere que se pase la cadena en contexto en la estructura, puede usar el valor UnmanagedType.ByValTStr. Tenga en cuenta que la codificación de una cadena serializada por ByValTStr viene determinada por el atributo CharSet. Además, requiere que una longitud de cadena se pasa por el campo MarshalAsAttribute.SizeConst.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char str[4];
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char16_t str[4]; // Could also be wchar_t[4] on Windows.
};

Personalización de la serialización de campos de decimal

Si está trabajando en Windows, puede encontrar algunas API que usan la estructura CY o CURRENCY nativa. De forma predeterminada, el tipo decimal de .NET serializa a la estructura DECIMAL nativa. Sin embargo, puede usar MarshalAsAttribute con el valor UnmanagedType.Currency para indicar al serializador que convierta un valor decimal al valor CY nativo.

public struct Currency
{
    [MarshalAs(UnmanagedType.Currency)]
    public decimal dec;
}
struct Currency
{
    CY dec;
};

Uniones

Una unión es un tipo de datos que puede contener diferentes tipos de datos sobre la misma memoria. Es una forma común de datos en el lenguaje C. Una unión se puede expresar en .NET mediante LayoutKind.Explicit. Se recomienda usar estructuras al definir una unión en .NET. El uso de clases puede provocar problemas de diseño y producir un comportamiento impredecible.

struct device1_config
{
    void* a;
    void* b;
    void* c;
};
struct device2_config
{
    int32_t a;
    int32_t b;
};
struct config
{
    int32_t type;

    union
    {
        device1_config dev1;
        device2_config dev2;
    };
};
public unsafe struct Device1Config
{
    void* a;
    void* b;
    void* c;
}

public struct Device2Config
{
    int a;
    int b;
}

public struct Config
{
    public int Type;

    public _Union Anonymous;

    [StructLayout(LayoutKind.Explicit)]
    public struct _Union
    {
        [FieldOffset(0)]
        public Device1Config Dev1;

        [FieldOffset(0)]
        public Device2Config Dev2;
    }
}

Serialización de System.Object

En Windows, puede serializar campos con tipo object al código nativo. Puede serializar estos campos a uno de los tres tipos:

De forma predeterminada, un campo con tipo object se serializará a uno de tipo IUnknown* que encapsula el objeto.

public struct ObjectDefault
{
    public object obj;
}
struct ObjectDefault
{
    IUnknown* obj;
};

Si desea serializar un campo de objeto a IDispatch*, agregue MarshalAsAttribute con el valor UnmanagedType.IDispatch.

public struct ObjectDispatch
{
    [MarshalAs(UnmanagedType.IDispatch)]
    public object obj;
}
struct ObjectDispatch
{
    IDispatch* obj;
};

Si quiere serializarlo como VARIANT, agregue MarshalAsAttribute con el valor UnmanagedType.Struct.

public struct ObjectVariant
{
    [MarshalAs(UnmanagedType.Struct)]
    public object obj;
}
struct ObjectVariant
{
    VARIANT obj;
};

En la tabla siguiente se describen los diferentes tipos en tiempo de ejecución del campo obj se asignan a los distintos tipos que se almacenan en VARIANT:

Tipo de .NET Tipo de VARIANTE
byte VT_UI1
sbyte VT_I1
short VT_I2
ushort VT_UI2
int VT_I4
uint VT_UI4
long VT_I8
ulong VT_UI8
float VT_R4
double VT_R8
char VT_UI2
string VT_BSTR
System.Runtime.InteropServices.BStrWrapper VT_BSTR
object VT_DISPATCH
System.Runtime.InteropServices.UnknownWrapper VT_UNKNOWN
System.Runtime.InteropServices.DispatchWrapper VT_DISPATCH
System.Reflection.Missing VT_ERROR
(object)null VT_EMPTY
bool VT_BOOL
System.DateTime VT_DATE
decimal VT_DECIMAL
System.Runtime.InteropServices.CurrencyWrapper VT_CURRENCY
System.DBNull VT_NULL