
C#
es un lenguaje increíblemente flexible. En él puede escribir no solo el backend o las aplicaciones de escritorio. Utilizo C#
para trabajar con datos científicos, que imponen ciertos requisitos sobre las herramientas disponibles en el lenguaje. Aunque netcore
toma el orden del día (teniendo en cuenta que después de netstandard2.0
mayoría de las características de los dos idiomas y el tiempo de ejecución no son netframework
con netframework
), sigo trabajando con proyectos netframework
.
En este artículo, considero una aplicación no obvia (¿pero probablemente deseada?) De Span<T>
y la diferencia entre la implementación de Span<T>
en netframework
y netcore
debido a los detalles de clr
.
Descargo de responsabilidad 1Los fragmentos de código de este artículo de ninguna manera están destinados a su uso en proyectos del mundo real.
La solución propuesta para el problema (¿exagerado?) Es más bien una prueba de concepto.
En cualquier caso, al implementar esto en su proyecto, lo hace bajo su propio riesgo y riesgo.
Descargo de responsabilidad 2Estoy absolutamente seguro de que en algún lugar, en algún caso, esto definitivamente disparará a alguien en la rodilla.
El bypass de seguridad de tipo en C#
poco probable que conduzca a algo bueno.
Por razones obvias, no probé este código en todas las situaciones posibles, sin embargo, los resultados preliminares parecen prometedores.
¿Por qué necesito Span<T>
?
Spen le permite trabajar con matrices de tipos unmanaged
en una forma más conveniente, reduciendo el número de asignaciones necesarias. A pesar de que el soporte de span en el netframework
BCL
netframework
casi ausente, se pueden obtener varias herramientas usando System.Memory
, System.Buffers
y System.Runtime.CompilerServices.Unsafe
.
El uso de vanos en mi proyecto heredado es limitado, sin embargo, me pareció un uso no obvio, mientras escupía en seguridad de tipo.
¿Qué es esta aplicación? En mi proyecto trabajo con datos obtenidos de una herramienta científica. Estas son imágenes que, en general, son una matriz de T[]
, donde T
es uno de los tipos primitivos unmanaged
, por ejemplo Int32
(también conocido como int
). Para serializar correctamente estas imágenes en el disco, necesito admitir el formato heredado increíblemente inconveniente, que se propuso en 1981 , y desde entonces ha cambiado poco. El principal problema de este formato es que es BigEndian . Por lo tanto, para escribir (o leer) una matriz sin comprimir de T[]
, debe cambiar la endianess de cada elemento. La tarea trivial.
¿Cuáles son algunas soluciones obvias?
- Repetimos la matriz
T[]
, llamamos a BitConverter.GetBytes(T)
, expandimos estos pocos bytes, copiamos a la matriz de destino. - Repetimos la matriz
T[]
, realizamos fraudes de la forma new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)};
(debería funcionar en tipos de doble byte), escriba en la matriz de destino. - * ¿ Pero es
T[]
una matriz? Los elementos están en una fila, ¿verdad? Entonces puede ir hasta el final, por ejemplo, Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int));
. El método copia la matriz a la matriz ignorando la comprobación de tipo. Solo es necesario no perderse los límites y la asignación. Mezclamos los bytes como resultado. - * Dicen que
C#
es (C++)++
. Por lo tanto, active /unsafe
, fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p;
y ahora puede ejecutar la representación de bytes de la matriz de origen, cambiar la endianess sobre la marcha y escribir bloques en el disco (agregando stackalloc byte[]
o ArrayPool<byte>.Shared
para el búfer intermedio) sin asignar memoria para una matriz de bytes completamente nueva.
Parece que el punto 4 le permite resolver todos los problemas, pero el uso explícito del contexto unsafe
y el trabajo con punteros es de alguna manera completamente diferente. Entonces Span<T>
viene en nuestra ayuda.
Span<T>
Span<T>
debería proporcionar técnicamente herramientas para trabajar con gráficos de memoria casi como trabajar con punteros, al tiempo que elimina la necesidad de "arreglar" la matriz en la memoria. Tal puntero compatible con GC
con límites de matriz. Todo está bien y seguro.
Una cosa, pero a pesar de la riqueza de System.Runtime.CompilerServices.Unsafe
, Span<T>
clavado en el tipo T
Dado que spen es esencialmente un puntero de longitud 1 +, ¿qué sucede si saca su puntero, lo convierte a otro tipo, recalcula la longitud y crea un nuevo tramo? Afortunadamente, tenemos public Span<T>(void* pointer, int length)
.
Escribamos una prueba simple:
[Test] public void Test() { void Flip(Span<byte> span) {} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]); }
Desarrolladores más avanzados de los que debería darme cuenta inmediatamente de lo que está mal aquí. ¿La prueba fallará? La respuesta, como suele suceder, depende .
En este caso, depende principalmente del tiempo de ejecución. En netcore
prueba debería funcionar, pero en netframework
, cómo funciona.
Curiosamente, si elimina algunos de los ensayos, la prueba comienza a funcionar correctamente en el 100% de los casos.
Vamos a hacerlo bien.
1 Estaba equivocado .
Respuesta correcta: depende
¿Por qué depende el resultado?
Eliminemos todo lo innecesario y escriba aquí dicho código:
private static void Main() => Check(); private static void Check() { Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y); }
El método Write<T, U>
acepta un intervalo de tipo T
, lee la dirección del primer elemento y lee a través de este puntero un elemento de tipo U
En otras palabras, Write<int, int>(ref x)
generará la dirección en la memoria + el número 999.
Normal Write
imprime una matriz.
Ahora sobre el método As<,>
:
private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); }
C#
sintaxis de C#
ahora admite este registro de estado Span<T>.GetPinnableReference()
al llamar implícitamente al método Span<T>.GetPinnableReference()
.
Ejecute este método en netframework4.8
en modo x64
. Nos fijamos en lo que sucede:
LEGACY [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0] 0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t 0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC 0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
Inicialmente, ambos tramos (a pesar de los diferentes tipos) se comportan de manera idéntica, y el Span<byte>
, en esencia, representa una vista de bytes de la matriz original. Lo que necesitas
Bien, intentemos cambiar el comienzo del lapso al tamaño de un IntPtr
(o 2 X int
en x64
) y leer. Obtenemos el tercer elemento de la matriz y la dirección correcta. Y luego recogeremos la basura ...
GC.Collect(0, GCCollectionMode.Forced, true, true);
El último indicador en este método le pide al GC
compacte el montón. Después de llamar a GC.Collect
GC
mueve la matriz local original. Span<int>
refleja estos cambios, pero nuestro Span<byte>
continúa apuntando a la dirección anterior, donde ahora no está claro qué. ¡Una excelente manera de dispararte todas las rodillas a la vez!
Ahora veamos el resultado del mismo fragmento de código llamado en netcore3.0.100-preview8
.
CORE [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t 0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC 0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ]
Todo funciona, y funciona de manera estable , por lo que puedo ver. Después de la compactación, ambos spains cambian su puntero. Genial Pero, ¿cómo hacer que funcione en un proyecto heredado?
Jit intrínseco
Olvidé por netcore
que el soporte para los tramos se implementa en netcore
través de intrinsik . En otras palabras, netcore
puede crear punteros internos incluso para un fragmento de matriz y actualizar correctamente los enlaces cuando el GC
mueve. En netframework
, la implementación nuget
de un span es una muleta. De hecho, tenemos dos spen diferentes: uno se crea a partir de la matriz y rastrea sus enlaces, el segundo desde el puntero y no tiene idea de a qué apunta. Después de mover la matriz original, el puntero span continúa apuntando hacia donde el puntero pasó a su constructor apuntado. A modo de comparación, este es un ejemplo de implementación de span en netcore
:
readonly ref struct Span<T> where T : unmanaged { private readonly ByReference<T> _pointer;
y en netframework
:
readonly ref struct Span<T> where T : unmanaged { private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; }
_pinnable
contiene una referencia a la matriz, si una se pasó al constructor, _byteOffset
contiene un cambio (incluso el intervalo en toda la matriz tiene algún cambio distinto de cero relacionado con la forma en que la matriz se representa en la memoria, probablemente ). Si pasa el puntero void*
al constructor, simplemente se convierte en un _byteOffset
absoluto. Span se enclavará estrechamente al área de memoria, y todos los métodos de instancia abundan en condiciones como if(_pinnable is null) {/* */} else {/* _pinnable */}
. ¿Qué hacer en tal situación?
Cómo hacerlo no vale la pena, pero todavía lo hice
Esta sección está dedicada a varias implementaciones compatibles con netframework
, que permiten netframework
Span<T> -> Span<U>
, manteniendo todos los enlaces necesarios.
Te advierto: esta es una zona de programación anormal con posibles errores fundamentales y un comportamiento indefinido al final
Método 1: ingenuo
Como muestra el ejemplo, la conversión de punteros no dará el resultado deseado en netframework
. Necesitamos el valor _pinnable
. Bien, descubriremos el reflejo sacando los campos privados (muy mal y no siempre es posible), lo escribiremos en una nueva inversión, estaremos felices. Solo hay un pequeño problema: spen es una ref struct
, no puede ser un argumento genérico, ni puede empaquetarse en un object
. Los métodos estándar de reflexión requerirán, de una forma u otra, empujar la luz al tipo de referencia. No encontré una manera simple (incluso considerando la reflexión en campos privados).
Método 2: necesitamos profundizar
Todo ya se ha hecho antes que yo ( [1] , [2] , [3] ). Spen es una estructura, independientemente de T
tres campos ocupan la misma cantidad de memoria ( en la misma arquitectura ). ¿Qué pasa si [FieldOffset(0)]
? Apenas dicho que hecho.
[StructLayout(LayoutKind.Explicit)] ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; }
Pero cuando inicia el programa (o más bien, cuando intenta usar un tipo), TypeLoadException
una TypeLoadException
: un genérico no puede ser LayoutKind.Explicit
. Bien, no importa, sigamos el camino difícil:
[StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan; }
Ahora puedes hacer esto:
private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan; }
El método funciona con un solo problema: el campo _length
copia tal cual, por lo que al convertir int
-> byte
el intervalo de bytes es 4 veces más pequeño que la matriz real.
No hay problema
[StructLayout(LayoutKind.Sequential)] public ref struct Raw { public object Pinnable; public IntPtr Pointer; public int Length; } [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Raw RawView; }
Ahora a través de RawView
puede acceder a cada campo de extensión individual.
private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan; }
Y funciona como debería , si ignoras el uso de trucos sucios. Menos: la versión genérica del convertidor no se puede crear, debe contentarse con tipos predefinidos.
Método 3: loco
Como cualquier programador normal, me gusta automatizar las cosas. La necesidad de escribir convertidores para cualquier par de tipos no unmanaged
no me agradó. ¿Qué solución se puede ofrecer? Así es, obtenga el CLR
para escribir el código por usted .
¿Cómo lograr esto? Hay diferentes formas, hay artículos . En resumen, el proceso se ve así:
Cree un generador de compilación -> cree un generador de módulos -> cree un tipo -> {Campos, métodos, etc.} -> en la salida obtenemos una instancia de Type
.
Para comprender exactamente cómo debería verse el tipo (es una ref struct
), utilizamos cualquier herramienta del tipo ildasm
. En mi caso, era dotPeek .
Crear un generador de tipos se parece a esto:
var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout
Ahora los campos. Como no podemos copiar directamente Span<T>
a Span<U>
debido a la diferencia en las longitudes, necesitamos crear dos tipos de cada conversión
[StructLayout(LayoutKind.Explicit)] ref struct Generated_Int32 { [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw; }
Aquí Raw
podemos declarar con nuestras manos y reutilizar. No te olvides de IsByRefLikeAttribute
. Con los campos, todo es simple:
var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private); spanField.SetOffset(0); var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private); rawField.SetOffset(0);
Eso es todo, el tipo más simple está listo. Ahora guarde en caché el módulo de ensamblaje. Los tipos personalizados se almacenan en caché, por ejemplo, en el diccionario ( T -> Generated_{nameof(T)}
). Creamos un contenedor que, de acuerdo con los dos tipos TIn
y TOut
genera dos tipos de ayudantes y realiza las operaciones necesarias en los tramos. Hay uno pero. Como en el caso de la reflexión, es casi imposible usarlo en tramos (o en otras ref struct
). O no encontré una solución simple . Como ser
Delegados al rescate
Los métodos de reflexión generalmente se ven así:
object Invoke(this MethodInfo mi, object @this, object[] otherArgs)
No llevan información sobre los tipos, por lo que si el boxeo (= embalaje) es aceptable para usted, no hay problemas.
En nuestro caso, @this
y otherArgs
deben contener una ref struct
, que no pude evitar.
Sin embargo, hay una manera más simple. Imaginemos que un tipo tiene métodos getter y setter (no propiedades, sino métodos simples creados manualmente).
Por ejemplo:
void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span;
Además del método, podemos declarar un tipo de delegado (explícitamente en el código):
delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged;
Tenemos que hacer esto porque la acción estándar debería tener una firma de Action<Span<T>>
, pero los spenes no pueden usarse como argumentos genéricos. SpanSetterDelegate
, sin embargo, es un delegado absolutamente válido.
Crea los delegados necesarios. Para hacer esto, realice manipulaciones estándar:
var mi = type.GetMethod("Method_Name");
Ahora spanSetter
se puede usar como, por ejemplo, spanSetter(Span<T>.Empty);
. En cuanto a @this
2 , esta es una instancia de nuestro tipo dinámico, creado, por supuesto, a través de Activator.CreateInstance(type)
, porque la estructura tiene un constructor predeterminado sin argumentos.
Entonces, la última frontera: necesitamos generar dinámicamente métodos.
2 Puede notar que algo va mal aquí: Activator.CreateInstance()
empacando una instancia de ref struct
. Ver el final de la siguiente sección.
Meet Reflection.Emit
Creo que los métodos podrían generarse usando Expression
, como Los cuerpos de nuestros captadores / setters triviales consisten literalmente en un par de expresiones. Elegí un enfoque diferente y más directo.
Si observa el código IL de un captador trivial, puede ver algo como ( Debug
, X86
, netframework4.8
)
nop ldarg.0 ldfld /* - */ stloc.0 br.s /* */ ldloc.0 ret
Hay toneladas de lugares para detenerse y depurar.
En la versión de lanzamiento, solo queda lo más importante:
ldarg.0 ldfld /* - */ ret
El argumento nulo del método de instancia es ... this
. Por lo tanto, lo siguiente está escrito en IL :
1) Descargar this
2) Cargue el valor del campo
3) traerlo de vuelta
Solo eh? Reflection.Emit
tiene una sobrecarga especial que toma, además del código operativo, también un parámetro descriptor de campo. Justo lo mismo que recibimos anteriormente, por ejemplo, spanField
.
var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldfld, spanField); gen.Emit(OpCodes.Ret);
Para el setter, es un poco más complicado, debe cargar esto en la pila, cargar el primer argumento de la función, luego llamar a la instrucción de escritura en el campo y no devolver nada:
ldarg.0 ldarg.1 stfld ret
Una vez realizado este procedimiento para el campo Raw
, declarando los delegados necesarios (o utilizando los estándares), obtenemos un tipo dinámico y cuatro métodos de acceso a partir de los cuales se generan los delegados genéricos correctos.
Escribimos una clase de contenedor que, utilizando dos parámetros genéricos ( TIn
, TOut
), recibe instancias de Type
que hacen referencia a los tipos dinámicos (en caché) correspondientes, después de lo cual crea un objeto de cada tipo y genera cuatro delegados genéricos, a saber
void SetSpan(Span<TIn> span)
para escribir el span de origen en la estructuraRaw GetRaw()
para leer el contenido de un span como una estructura Raw
void SetRaw(Raw raw)
para escribir la estructura Raw
modificada en el segundo objetoSpan<TOut> GetSpan()
para devolver el span del tipo deseado con los campos correctamente establecidos y recalculados.
Curiosamente, las instancias de tipo dinámico deben crearse una vez. Al crear un delegado, se pasa una referencia a estos objetos como un parámetro @this
. Aquí hay una violación de las reglas. Activator.CreateInstance
devuelve el object
. Aparentemente, esto se debe al hecho de que el tipo dinámico en sí no resultó ref
- type.IsByRef
( type.IsByRef
Like == false
), pero fue posible crear campos ref
like. Aparentemente, dicha restricción está presente en el lenguaje, pero el CLR
digiere. Quizás es aquí donde se dispararán las rodillas en el caso de un uso no estándar. 3
Entonces, obtenemos una instancia de tipo genérico que contiene cuatro delegados y dos referencias implícitas a instancias de clases dinámicas. Los delegados y las estructuras se pueden reutilizar al realizar las mismas castas en una fila. Para mejorar el rendimiento, volvemos a almacenar en caché (ya un convertidor de tipos) para un par (TIn, TOut) -> Generator<TIn, TOut>
.
El trazo es el último: le damos tipos, Span<TIn> -> Span<TOut>
public Span<TOut> Cast(Span<TIn> span) {
Conclusión
A veces, en aras del interés deportivo, puede omitir algunas de las limitaciones del idioma e implementar una funcionalidad no estándar. Por supuesto, bajo su propio riesgo y riesgo. Vale la pena señalar que el método dinámico le permite abandonar por completo los punteros y unsafe / fixed
contextos unsafe / fixed
, lo que puede ser una ventaja. La desventaja obvia es la necesidad de reflexión y generación de tipos.
Para aquellos que han leído hasta el final.
Resultados de referencia ingenuos¿Y qué tan rápido es todo?
Comparé la velocidad de las castas en un escenario tonto que no refleja el uso real / potencial de tales castas y vanos, pero al menos da una idea de la velocidad.
Cast_Explicit
utiliza la conversión a través de un tipo declarado explícitamente, como en el Método 2 . Cada casta requiere la asignación de dos estructuras pequeñas y accesos a los campos;Cast_IL
implementa el Método 3 , pero cada vez crea una instancia nuevamente Generator<TIn, TOut>
, lo que lleva a búsquedas constantes en los diccionarios, después de que el primer pase genera todos los tipos;Cast_IL_Cached
almacena en caché la instancia del convertidor directamente Generator<TIn, TOut>
, por lo que resulta ser más rápido en promedio, porque toda la casta se reduce a las llamadas de cuatro delegados;Buffer
, , . .
— int[N]
N/2
.
, , . , . , , . , unmanaged
.
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1
PS
3 , ref
, , . ( ) . ref
structs,
static Raw Generated_Int32.GetRaw(Span<int> span) { var inst = new Generated_Int32() { Span = span }; return inst.Raw; }
, Reflection.Emit
. , ILGenerator.DeclareLocal
.
static Span<int> Generated_Int32.GetSpan(Raw raw);
delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged; delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged;
, , ref
— . Porque ,
var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>;
—
Raw raw = getter(Span<TIn>.Empty); Raw newRaw = convert(raw); Span<TOut> = setter(newRaw);
UPD01: