Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Nota
Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.
Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.
Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .
Resumen
Esta propuesta proporciona construcciones de lenguaje que exponen códigos de operación il a los que actualmente no se puede acceder de forma eficaz, o en absoluto, en C#: ldftn y calli. Estos códigos de operación de IL pueden ser importantes en código de alto rendimiento y los desarrolladores necesitan una manera eficaz de acceder a ellos.
Motivación
Las motivaciones y antecedentes de esta característica se describen en el siguiente asunto, así como una posible implementación de la característica.
Esta es una propuesta de diseño alternativa a los intrínsecos del compilador
Diseño detallado
Punteros de función
El lenguaje permitirá la declaración de punteros de función mediante la sintaxis delegate*. La sintaxis completa se describe en detalle en la sección siguiente, pero está pensada para parecerse a la sintaxis usada por declaraciones de tipo Func y Action.
unsafe class Example
{
void M(Action<int> a, delegate*<int, void> f)
{
a(42);
f(42);
}
}
Estos tipos se representan mediante el tipo de puntero de función como se describe en ECMA-335. Esto significa que la invocación de un delegate* usará calli donde la invocación de un delegate usará callvirt en el método Invoke.
Syntácticamente, la invocación es idéntica para ambas construcciones.
La definición ECMA-335 de punteros de método incluye la convención de llamada como parte de la firma de tipo (sección 7.1).
La convención de llamada predeterminada será managed. Las convenciones de llamada no administradas se pueden especificar colocando una palabra clave unmanaged después de la sintaxis delegate*, que usará la predeterminada de la plataforma de tiempo de ejecución. A continuación, se pueden especificar convenciones no administradas específicas entre corchetes a la palabra clave unmanaged especificando cualquier tipo que empiece por CallConv en el espacio de nombres System.Runtime.CompilerServices, dejando el prefijo CallConv. Estos tipos deben proceder de la biblioteca principal del programa y el conjunto de combinaciones válidas depende de la plataforma.
//This method has a managed calling convention. This is the same as leaving the managed keyword off.
delegate* managed<int, int>;
// This method will be invoked using whatever the default unmanaged calling convention on the runtime
// platform is. This is platform and architecture dependent and is determined by the CLR at runtime.
delegate* unmanaged<int, int>;
// This method will be invoked using the cdecl calling convention
// Cdecl maps to System.Runtime.CompilerServices.CallConvCdecl
delegate* unmanaged[Cdecl] <int, int>;
// This method will be invoked using the stdcall calling convention, and suppresses GC transition
// Stdcall maps to System.Runtime.CompilerServices.CallConvStdcall
// SuppressGCTransition maps to System.Runtime.CompilerServices.CallConvSuppressGCTransition
delegate* unmanaged[Stdcall, SuppressGCTransition] <int, int>;
Las conversiones entre tipos delegate* se realiza en base a su firma incluyendo la convención de llamada.
unsafe class Example {
void Conversions() {
delegate*<int, int, int> p1 = ...;
delegate* managed<int, int, int> p2 = ...;
delegate* unmanaged<int, int, int> p3 = ...;
p1 = p2; // okay p1 and p2 have compatible signatures
Console.WriteLine(p2 == p1); // True
p2 = p3; // error: calling conventions are incompatible
}
}
Un tipo delegate* es un tipo de puntero, lo que significa que tiene todas las funcionalidades y restricciones de un tipo de puntero estándar:
- Solo válido en un contexto
unsafe. - Los métodos que contienen un parámetro
delegate*o un tipo de valor devuelto solo se pueden llamar desde un contexto deunsafe. - No se puede convertir en
object. - No se puede usar como argumento genérico.
- Puede convertir implícitamente
delegate*envoid*. - Puede convertir explícitamente de
void*adelegate*.
Restricciones:
- Los atributos personalizados no se pueden aplicar a una
delegate*ni a ninguno de sus elementos. - Un parámetro
delegate*no se puede marcar comoparams - Un tipo
delegate*tiene todas las restricciones de un tipo de puntero normal. - La aritmética de punteros no puede realizarse directamente sobre tipos de punteros de función.
Sintaxis de los punteros de función
La sintaxis del puntero de función completa se representa mediante la siguiente gramática:
pointer_type
: ...
| funcptr_type
;
funcptr_type
: 'delegate' '*' calling_convention_specifier? '<' funcptr_parameter_list funcptr_return_type '>'
;
calling_convention_specifier
: 'managed'
| 'unmanaged' ('[' unmanaged_calling_convention ']')?
;
unmanaged_calling_convention
: 'Cdecl'
| 'Stdcall'
| 'Thiscall'
| 'Fastcall'
| identifier (',' identifier)*
;
funptr_parameter_list
: (funcptr_parameter ',')*
;
funcptr_parameter
: funcptr_parameter_modifier? type
;
funcptr_return_type
: funcptr_return_modifier? return_type
;
funcptr_parameter_modifier
: 'ref'
| 'out'
| 'in'
;
funcptr_return_modifier
: 'ref'
| 'ref readonly'
;
Si no se proporciona ningún calling_convention_specifier, el valor predeterminado es managed. La codificación precisa de metadatos del calling_convention_specifier y qué identifier son válidos en el unmanaged_calling_convention se trata en Representación de metadatos de convenciones de llamada.
delegate int Func1(string s);
delegate Func1 Func2(Func1 f);
// Function pointer equivalent without calling convention
delegate*<string, int>;
delegate*<delegate*<string, int>, delegate*<string, int>>;
// Function pointer equivalent with calling convention
delegate* managed<string, int>;
delegate*<delegate* managed<string, int>, delegate*<string, int>>;
Conversiones de punteros de función
En un contexto no seguro, el conjunto de conversiones implícitas disponibles (conversiones implícitas) se extiende para incluir las siguientes conversiones de puntero implícitas:
- Conversiones existentes - (§23.5)
- De funcptr_type
F0a otra funcptr_typeF1, siempre que se cumplan todas las siguientes condiciones:-
F0yF1tienen el mismo número de parámetros, y cada parámetroD0ndeF0tiene los mismos modificadoresref,outoinque el parámetro correspondienteD1nenF1. - Para cada parámetro de valor (un parámetro sin
ref,outoinmodificador), existe una conversión de identidad, conversión de referencia implícita o conversión de puntero implícita del tipo de parámetro enF0al tipo de parámetro correspondiente enF1. - Para cada parámetro
ref,outoin, el tipo de parámetro deF0es el mismo que el tipo de parámetro correspondiente enF1. - Si el tipo de valor devuelto es por valor (no
refniref readonly), existe una conversión de identidad, de referencia implícita o de puntero implícito del tipo de valor devuelto deF1al tipo de valor devuelto deF0. - Si el tipo de valor devuelto es por referencia (
reforef readonly), el tipo de valor devuelto y los modificadoresrefdeF1son los mismos que el tipo de valor devuelto y los modificadoresrefdeF0. - La convención de llamada de
F0es la misma que la convención de llamada deF1.
-
Permitir direcciones de métodos de destino
Ahora se permitirán grupos de métodos como argumentos de una expresión address-of. El tipo de esta expresión será un delegate* que tiene la firma equivalente del método de destino y una convención de llamada administrada:
unsafe class Util {
public static void Log() { }
void Use() {
delegate*<void> ptr1 = &Util.Log;
// Error: type "delegate*<void>" not compatible with "delegate*<int>";
delegate*<int> ptr2 = &Util.Log;
}
}
En un contexto no seguro, un método M es compatible con un tipo de puntero de función F si se cumplen todas las siguientes opciones:
-
MyFtienen el mismo número de parámetros y cada parámetro deMtiene los mismos modificadoresref,outoincomo parámetro correspondiente enF. - Para cada parámetro de valor (un parámetro sin
ref,outoinmodificador), existe una conversión de identidad, conversión de referencia implícita o conversión de puntero implícita del tipo de parámetro enMal tipo de parámetro correspondiente enF. - Para cada parámetro
ref,outoin, el tipo de parámetro deMes el mismo que el tipo de parámetro correspondiente enF. - Si el tipo de valor devuelto es por valor (no
refniref readonly), existe una conversión de identidad, de referencia implícita o de puntero implícito del tipo de valor devuelto deFal tipo de valor devuelto deM. - Si el tipo de valor devuelto es por referencia (
reforef readonly), el tipo de valor devuelto y los modificadoresrefdeFson los mismos que el tipo de valor devuelto y los modificadoresrefdeM. - La convención de llamada de
Mes la misma que la convención de llamada deF. Esto incluye el bit de convención de llamada, así como los indicadores de convención de llamada especificados en el identificador no gestionado. -
Mes un método estático.
En un contexto no seguro, existe una conversión implícita desde una dirección de expresión cuyo destino es un grupo de métodos E a un tipo de puntero de función compatible F si E contiene al menos un método que se aplica en su forma normal a una lista de argumentos construida mediante el uso de los tipos de parámetros y modificadores de F, como se describe en lo siguiente.
- Se selecciona un único método
Mcorrespondiente a una invocación de método del formularioE(A)con las siguientes modificaciones:- La lista de argumentos
Aes una lista de expresiones, cada una clasificada como una variable, y tiene el tipo y el modificador (ref,out, oin) del funcptr_parameter_list correspondiente deF. - Los métodos candidatos son solo los métodos que son aplicables en su forma normal, no los aplicables en su forma expandida.
- Los métodos candidatos son solo los métodos estáticos.
- La lista de argumentos
- Si el algoritmo de resolución de sobrecarga genera un error, se produce un error en tiempo de compilación. De lo contrario, el algoritmo genera un único método mejor
Mque tiene el mismo número de parámetros queFy se considera que la conversión existe. - El método seleccionado
Mdebe ser compatible (tal como se definió anteriormente) con el tipo de puntero de funciónF. De lo contrario, se produce un error en tiempo de compilación. - El resultado de la conversión es un puntero de función de tipo
F.
Esto significa que los desarrolladores pueden depender de las reglas de resolución de sobrecargas para trabajar en conjunto con el operador address-of:
unsafe class Util {
public static void Log() { }
public static void Log(string p1) { }
public static void Log(int i) { }
void Use() {
delegate*<void> a1 = &Log; // Log()
delegate*<int, void> a2 = &Log; // Log(int i)
// Error: ambiguous conversion from method group Log to "void*"
void* v = &Log;
}
}
El operador address-of se implementará mediante la instrucción ldftn.
Restricciones de esta característica:
- Solo se aplica a los métodos marcados como
static. - Las funciones no locales de
staticno se pueden usar en&. El lenguaje no especifica deliberadamente los detalles de implementación de estos métodos. Esto incluye si son estáticos o de instancia o exactamente con qué firma se emiten.
Operadores sobre tipos de puntero de función
La sección del código no seguro en las expresiones se modifica como tal:
En un contexto inseguro, hay varias construcciones disponibles para operar sobre todos los _pointer_type_s que no son _funcptr_type_s:
- El operador
*se puede usar para realizar la direccionamiento indirecto del puntero (§23.6.2).- El operador
->se puede usar para acceder a un miembro de una estructura a través de un puntero (§23.6.3).- El operador
[]se puede usar para indexar un puntero (§23.6.4).- El operador
&se puede usar para obtener la dirección de una variable (§23.6.5).- Los operadores
++y--se pueden usar para incrementar y disminuir punteros (§23.6.6).- Los operadores
+y-se pueden usar para realizar la aritmética del puntero (§23.6.7).- Los operadores
==,!=,<,>,<=y=>se pueden usar para comparar punteros (§23.6.8).- El operador
stackallocse puede usar para asignar memoria desde la pila de llamadas (§23.8).- La instrucción
fixedse puede usar para corregir temporalmente una variable para que se pueda obtener su dirección (§23.7).En un contexto inseguro, hay varias construcciones disponibles para operar con todos los _funcptr_type_s:
- El operador
&puede usarse para obtener la dirección de métodos estáticos (Permitir address-of a métodos objetivo)- Los operadores
==,!=,<,>,<=y=>se pueden usar para comparar punteros (§23.6.8).
Además, modificamos todas las secciones de Pointers in expressions para prohibir los tipos de puntero de función, excepto Pointer comparison y The sizeof operator.
Mejor miembro de función
§12.6.4.3 Mejor miembro de función se cambiará para incluir la siguiente línea:
Un
delegate*es más específico quevoid*
Esto significa que es posible sobrecargar en void* y un delegate* y seguir utilizando sensatamente el operador address-of.
Inferencia de tipos
En el código no seguro, se realizan los siguientes cambios en los algoritmos de inferencia de tipos:
Tipos de entrada
Se agrega lo siguiente:
Si
Ees un grupo de métodos de referencia yTes un tipo de puntero de función, entonces todos los tipos de parámetros deTson tipos de entrada deEcon el tipoT.
Tipos de salida
Se agrega lo siguiente:
Si
Ees un grupo de métodos address-of yTes un tipo de puntero de función entonces el tipo de retorno deTes un tipo de salida deEcon tipoT.
Inferencias de tipo de salida
Se añade el siguiente punto entre los puntos 2 y 3:
- Si
Ees una dirección de un grupo de métodos yTes un tipo de puntero de función con tipos de parámetrosT1...Tky tipo de retornoTb, y la resolución de sobrecarga deEcon los tiposT1..Tkproduce un único método con tipo de retornoU, entonces se hace una inferencia de límite inferior deUaTb.
Mejor conversión a partir de expresión
Se añade el siguiente subpunto como caso al punto 2:
Ves un tipo puntero de funcióndelegate*<V2..Vk, V1>yUes un tipo de puntero de funcióndelegate*<U2..Uk, U1>, y la convención de llamada deVes idéntica aU, y la referencia deVies idéntica aUi.
Inferencias de límite inferior
El siguiente caso se agrega al punto 3:
Ves un tipo de puntero de funcióndelegate*<V2..Vk, V1>y existe un tipo de puntero de funcióndelegate*<U2..Uk, U1>tal queUes idéntico adelegate*<U2..Uk, U1>, la convención de llamada deVes idéntica aU, y el estado de referencia deVies idéntico aUi.
El primer punto de la inferencia de Ui a Vi se modifica a:
- Si
Uno es un tipo de puntero de función yUino se sabe que es un tipo de referencia, o siUes un tipo de puntero de función y no se sabe queUisea un tipo de puntero de función o un tipo de referencia, se realiza una inferencia exacta
Entonces, se añade después del tercer punto de la inferencia de Ui a Vi:
- De lo contrario, si
Vesdelegate*<V2..Vk, V1>, la inferencia depende del parámetro i-th dedelegate*<V2..Vk, V1>:
- Si V1:
- Si el retorno es por valor, entonces se hace una inferencia de límite inferior.
- Si el retorno es por referencia, entonces se hace una inferencia exacta.
- Si V2..Vk:
- Si el parámetro es un valor, se realiza una inferencia de límite superior.
- Si el parámetro es una referencia, se realiza una inferencia exacta.
Inferencias de límite superior
El siguiente caso se agrega al punto 2:
Ues un tipo de puntero de funcióndelegate*<U2..Uk, U1>, yVes un tipo de puntero de función que es idéntico adelegate*<V2..Vk, V1>, la convención de llamada deUes idéntica a la deV, y la referencialidad deUies idéntica a la deVi.
El primer punto de la inferencia de Ui a Vi se modifica a:
- Si
Uno es un tipo de puntero de función yUino se sabe que es un tipo de referencia, o siUes un tipo de puntero de función y no se sabe queUisea un tipo de puntero de función o un tipo de referencia, se realiza una inferencia exacta
Entonces, se añade después del tercer punto de la inferencia de Ui a Vi:
- De lo contrario, si
Uesdelegate*<U2..Uk, U1>, la inferencia depende del parámetro i-th dedelegate*<U2..Uk, U1>:
- Si U1:
- Si el retorno se realiza por valor, entonces se realiza una inferencia de límite superior .
- Si el retorno es por referencia, entonces se hace una inferencia exacta.
- Si U2..Uk:
- Si el parámetro es por valor, entonces se hace una inferencia de límite inferior.
- Si el parámetro es una referencia, se realiza una inferencia exacta.
Representación de metadatos de los parámetros in, outy ref readonly, y tipos de retorno.
Las firmas de puntero de función no tienen ubicación de indicadores de parámetro, por lo que debemos codificar si los parámetros y el tipo de retorno son in, out, o ref readonly utilizando modreqs.
in
Reutilizamos System.Runtime.InteropServices.InAttribute, aplicado como modreq al especificador ref en un parámetro o tipo de retorno, para significar lo siguiente:
- Si se aplica a un especificador de referencia de parámetros, este parámetro se trata como
in. - Si se aplica al especificador ref de tipo de retorno, el tipo de retorno se trata como
ref readonly.
out
Usamos System.Runtime.InteropServices.OutAttribute, aplicado como modreq al especificador ref en un tipo de parámetro, para indicar que el parámetro es un parámetro out.
Errores
- Es un error aplicar
OutAttributecomo modreq a un tipo de valor devuelto. - Es un error aplicar tanto
InAttributecomoOutAttributecomo modreq a un tipo de parámetro. - Si se especifica cualquiera de ellos a través de modopt, se omiten.
Representación de metadatos de convenciones de llamada
Las convenciones de llamada se codifican en una firma de método en metadatos mediante una combinación de la marca CallKind en la firma y cero o más modopt al principio de la firma. ECMA-335 declara actualmente los siguientes elementos en la marca CallKind:
CallKind
: default
| unmanaged cdecl
| unmanaged fastcall
| unmanaged thiscall
| unmanaged stdcall
| varargs
;
De estos, los punteros de función en C# soportarán todos menos varargs.
Además, el tiempo de ejecución (y eventualmente 335) se actualizará para incluir un nuevo CallKind en las nuevas plataformas. Esto no tiene un nombre formal actualmente, pero este documento usará unmanaged ext como marcador de posición para representar el nuevo formato de convención de llamadas extensible. Sin modopts, unmanaged ext es la convención de llamada predeterminada de la plataforma, unmanaged sin corchetes.
Asignación de calling_convention_specifier a CallKind
Un calling_convention_specifier que se omite, o se especifica como managed, se asigna a defaultCallKind. Este es el valor por defecto CallKind de cualquier método que no tiene el atributo UnmanagedCallersOnly.
C# reconoce 4 identificadores especiales que se corresponden con componentes CallKindespecíficos existentes no administrados de ECMA 335. Para que se produzca esta asignación, estos identificadores deben especificarse por sí solos, sin ningún otro identificador, y este requisito está codificado en la especificación de unmanaged_calling_convention. Estos identificadores son Cdecl, Thiscall, Stdcally Fastcall, que corresponden a unmanaged cdecl, unmanaged thiscall, unmanaged stdcally unmanaged fastcall, respectivamente. Si se especifica más de un identifer o el único identifier no es de los identificadores especialmente reconocidos, realizamos una búsqueda de nombres especial en el identificador con las siguientes reglas:
- Anteponemos el
identifiercon la cadenaCallConv - Solo se examinan los tipos definidos en el espacio de nombres
System.Runtime.CompilerServices. - Solo se examinan los tipos definidos en la biblioteca principal de la aplicación, que es la biblioteca que define
System.Objecty no tiene dependencias. - Solo se examinan los tipos públicos.
Si la búsqueda tiene éxito en todos los identifier especificados en un unmanaged_calling_convention, codificamos el CallKind como unmanaged ext, y codificamos cada uno de los tipos resueltos en el conjunto de modopt al principio de la firma del puntero de función. Como nota, estas reglas significan que los usuarios no pueden prefijar estos identifier con CallConv, ya que esto resultará en la búsqueda de CallConvCallConvVectorCall.
Al interpretar los metadatos, primero observamos el CallKind. Si es algo distinto de unmanaged ext, omitimos todos los modopten el tipo de retorno para determinar la convención de llamada y usamos solo el CallKind. Si el CallKind es unmanaged ext, miramos los modopts al principio del tipo del puntero de función, tomando la unión de todos los tipos que cumplan los siguientes requisitos:
- El está definido en la biblioteca principal, que es la biblioteca que no hace referencia a otras bibliotecas y define
System.Object. - El tipo se define en el espacio de nombres
System.Runtime.CompilerServices. - El tipo comienza con el prefijo
CallConv. - El tipo es público.
Estos representan los tipos que deben ser encontrados al realizar la búsqueda en el identifier en un cuando se define un tipo de puntero unmanaged_calling_convention de función en la fuente.
Es un error intentar utilizar un puntero de función con un CallKind de unmanaged ext si el tiempo de ejecución de destino no soporta la característica. Esto se determinará buscando la presencia de la constante System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind. Si esta constante está presente, el tiempo de ejecución se considera compatible con la característica.
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute es un atributo usado por CLR para indicar que se debe llamar a un método con una convención de llamada específica. Por este motivo, presentamos el siguiente soporte para trabajar con el atributo:
- Es un error llamar directamente a un método anotado con este atributo desde C#. Los usuarios deben obtener un puntero de función para el método y luego invocar ese puntero.
- Se trata de un error para aplicar el atributo a cualquier cosa que no sea un método estático normal o una función local estática normal. El compilador de C# marcará los métodos no estáticos o estáticos no normales importados de metadatos con este atributo como no admitido por el lenguaje.
- Es un error que un método marcado con el atributo tenga un parámetro o un tipo de retorno que no sea un
unmanaged_type. - Es un error que un método marcado con el atributo tenga parámetros de tipo, incluso si esos parámetros de tipo están restringidos a
unmanaged. - Es un error que un método de un tipo genérico esté marcado con el atributo.
- Es un error convertir un método marcado con el atributo en un tipo delegado.
- Es un error especificar cualquier tipo para
UnmanagedCallersOnly.CallConvsque no cumpla con los requisitos de la convención de invocaciónmodopten los metadatos.
Al determinar la convención de llamada de un método marcado con un atributo UnmanagedCallersOnly válido, el compilador realiza las siguientes comprobaciones sobre los tipos especificados en la propiedad CallConvs para determinar los CallKind efectivos y modopts que se deben usar para determinar la convención de llamada:
- Si no se especifican tipos, el
CallKindse trata comounmanaged ext, sin la convención de llamadamodoptal comienzo del tipo de puntero de función. - Si hay un tipo especificado y ese tipo se denomina
CallConvCdecl,CallConvThiscall,CallConvStdcalloCallConvFastcall, elCallKindse trata comounmanaged cdecl,unmanaged thiscall,unmanaged stdcallounmanaged fastcall, respectivamente, sin convención de llamadamodopts al principio del tipo de puntero de función. - Si se especifican múltiples tipos o el único tipo no se llama uno de los tipos especialmente llamados arriba, el
CallKindse trata comounmanaged ext, con la unión de los tipos especificados tratados comomodoptal comienzo del tipo de puntero de función.
A continuación, el compilador examina esta colección efectiva de CallKind y modopt y usa reglas de metadatos normales para determinar la convención de llamada final del puntero de función tipo.
Preguntas abiertas
La detección de soporte en tiempo de ejecución para unmanaged ext
https://github.com/dotnet/runtime/issues/38135 realiza un seguimiento de la adición de esta marca. Dependiendo de los resultados de la revisión, utilizaremos la propiedad especificada en la cuestión, o utilizaremos la presencia de UnmanagedCallersOnlyAttribute como la marca que determina si los tiempos de ejecución soportan unmanaged ext.
Consideraciones
Permitir métodos de instancia
La propuesta podría ampliarse para admitir métodos de instancia aprovechando la convención de llamada de la CLI de EXPLICITTHIS (denominada instance en código de C#). Esta forma de punteros de función de la CLI coloca el parámetro this como un primer parámetro explícito de la sintaxis del puntero de función.
unsafe class Instance {
void Use() {
delegate* instance<Instance, string> f = &ToString;
f(this);
}
}
Esto es sólido, pero añade cierta complicación a la propuesta. Especialmente porque los punteros de función que difieren por la convención de llamada instance y managed serían incompatibles a pesar de que ambos casos se utilizan para invocar métodos administrados con la misma firma C#. Además, en todos los casos considerados en los que sería valioso tener esto, había una solución sencilla: utilizar una función local static.
unsafe class Instance {
void Use() {
static string toString(Instance i) => i.ToString();
delegate*<Instance, string> f = &toString;
f(this);
}
}
No exigir unsafe en la declaración
En lugar de requerir unsafe en cada uso de un delegate*, solo es necesario en el punto en el que se convierte un grupo de métodos en un delegate*. Aquí es donde los problemas de seguridad entran en juego (sabiendo que el ensamblado que contiene no puede ser descargado mientras el valor está vivo). Requerir unsafe en las otras localizaciones puede ser visto como excesivo.
Así es como se diseñó originalmente el diseño. Pero las reglas de lenguaje resultantes parecían muy incómodas. Es imposible ocultar el hecho de que se trata de un valor puntero y que seguía asomándose incluso sin la palabra clave unsafe. Por ejemplo, no se puede permitir la conversión a object, no puede ser miembro de un class, etc. El diseño de C# es requerir unsafe para todos los usos del puntero y, por lo tanto, este diseño sigue esto.
Los desarrolladores seguirán siendo capaces de presentar una contenedor seguro sobre los valores delegate* de la misma forma que lo hacen actualmente para los tipos de puntero normales. Ten en cuenta:
unsafe struct Action {
delegate*<void> _ptr;
Action(delegate*<void> ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
Uso de delegados
En lugar de utilizar un nuevo elemento sintáctico, delegate*, simplemente se utilizarían los tipos existentes delegate con un * a continuación del tipo:
Func<object, object, bool>* ptr = &object.ReferenceEquals;
Para controlar la convención de llamada, se pueden anotar los tipos de delegate con un atributo que especifica un valor de CallingConvention. La falta de un atributo significaría la convención de llamada administrada.
La codificación de esto en IL es problemática. El valor subyacente debe representarse como puntero, pero también debe:
- Tener un tipo único para permitir sobrecargas con diferentes tipos de puntero de función.
- Ser equivalente para propósitos OHI a través de los límites de los ensamblados.
El último punto es especialmente problemático. Esto significa que cada ensamblado que utilice Func<int>* debe codificar un tipo equivalente en metadatos aunque Func<int>* esté definido en un ensamblado que no controle.
Además, cualquier otro tipo definido con el nombre System.Func<T> en un ensamblado que no sea mscorlib debe ser diferente de la versión definida en mscorlib.
Una opción que se exploró fue la de emitir dicho puntero como mod_req(Func<int>) void*. Esto no funciona como una mod_req no se puede enlazar a un TypeSpec y, por tanto, no puede tener como destino instancias genéricas.
Punteros de función con nombre
La sintaxis de los punteros de función puede ser engorrosa, especialmente en casos complejos como los punteros de función anidados. En lugar de que los desarrolladores tengan que escribir la firma cada vez, el lenguaje podría permitir declaraciones nombradas de punteros de función, tal como se hace con delegate.
func* void Action();
unsafe class NamedExample {
void M(Action a) {
a();
}
}
Parte del problema aquí es que el primitivo de la CLI subyacente no tiene nombres, por lo que esto sería puramente una invención de C# y requiere un poco de trabajo de metadatos para habilitar. Eso es factible, pero supone una cantidad significativa de trabajo. Básicamente, requiere que C# tenga un complemento para la tabla def de tipo exclusivamente para estos nombres.
Además, cuando se examinaron los argumentos de los punteros de función con nombre, descubrimos que podían aplicarse igualmente bien a una serie de otros escenarios. Por ejemplo, sería igual de conveniente declarar tuplas con nombre para reducir la necesidad de escribir la firma completa en todos los casos.
(int x, int y) Point;
class NamedTupleExample {
void M(Point p) {
Console.WriteLine(p.x);
}
}
Tras debatirlo, decidimos no permitir la declaración de tipos delegate* con nombre. Si descubrimos que existe una necesidad significativa de ello, basándonos en los comentarios de los clientes, investigaremos una solución de nomenclatura que funcione para punteros de función, tuplas, genéricos, etc. Es probable que esto sea similar a otras sugerencias, como el soporte completo en el lenguaje typedef.
Consideraciones futuras
delegados estáticos
Se refiere a la propuesta de permitir la declaración de tipos delegate que solo pueden referirse a miembros static. La ventaja es que instancias como delegate pueden no requerir asignación, lo que mejora el rendimiento en escenarios sensibles.
Si se implementa la característica de apuntador de función, probablemente se cerrará la propuesta static delegate. La ventaja propuesta de esa característica es su naturaleza que no requiere asignación. Sin embargo, investigaciones recientes han descubierto que no es posible conseguirlo debido a la descarga del conjunto. Tiene que haber un manejador fuerte desde el método static delegate al que se refiere para evitar que el ensamblaje se descargue por debajo de él.
Para mantener cada instancia static delegate sería necesario asignar un nuevo manejador que va en contra de los objetivos de la propuesta. Hubo algunos diseños en los que la asignación podría ser amortizada a una sola asignación por sitio de llamada, pero eso era un poco complejo y no parecía valer la pena.
Esto significa que los desarrolladores tienen que decidir esencialmente entre las siguientes compensaciones:
- Seguridad frente a la descarga del ensamblaje: requiere asignaciones y, por tanto,
delegateya es una opción suficiente. - Sin seguridad frente a la descarga de ensamblajes: utilizar un
delegate*. Esto se puede encapsular en unstructpara permitir el uso fuera de un contexto deunsafeen el resto del código.
C# feature specifications