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 .
Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/1331
Resumen
Unifique el comportamiento entre iteradores y métodos asincrónicos. Específicamente:
- Permitan usar elementos locales
ref
,/,ref struct
y bloquesunsafe
en iteradores y métodos asincrónicos, siempre que se utilicen en segmentos de código sinyield
niawait
. - Advertir sobre
yield
dentro delock
.
Motivación
No es necesario prohibir las variables locales ref
/ref struct
y los bloques de unsafe
en métodos asincrónicos o de iteradores si no se usan en yield
o await
, porque no es necesario elevarlos.
async void M()
{
await ...;
ref int x = ...; // error previously, proposed to be allowed
x.ToString();
await ...;
// x.ToString(); // still error
}
Cambios importantes
No hay cambios importantes en la especificación del lenguaje, pero hay un cambio importante en la implementación de Roslyn (debido a una infracción de especificación).
Roslyn infringe la parte de la especificación que indica que los iteradores introducen un contexto seguro (§13.3.1).
Por ejemplo, si hay un unsafe class
con un método de iterador que contiene una función local, esa función local hereda el contexto no seguro de la clase , aunque debería haber estado en un contexto seguro por la especificación debido al método iterador.
De hecho, todo el método del iterador heredó el contexto no seguro en Roslyn, simplemente no se permitía usar ninguna construcción no segura en iteradores.
En LangVersion >= 13
, los iteradores introducirán correctamente un contexto seguro porque queremos permitir construcciones no seguras en iteradores.
unsafe class C // unsafe context
{
System.Collections.Generic.IEnumerable<int> M() // an iterator
{
yield return 1;
local();
async void local()
{
int* p = null; // allowed in C# 12; error in C# 13 (breaking change)
await Task.Yield(); // error in C# 12, allowed in C# 13
}
}
}
Nota:
- El problema se puede solucionar simplemente agregando el modificador
unsafe
a la función local. - Esto no afecta a las expresiones lambda porque "heredan" el "contexto de iterador" y, por lo tanto, era imposible usar construcciones no seguras dentro de ellas.
Diseño detallado
Los siguientes cambios están vinculados a LangVersion, es decir, C# 12 y versiones inferiores seguirán prohibiendo los locales tipo ref y los bloques unsafe
en métodos asincrónicos e iteradores, y C# 13 levantará estas restricciones, como se describe a continuación.
Sin embargo, las aclaraciones de las especificaciones que coinciden con la implementación actual de Roslyn deberían mantenerse en todas las LangVersions.
Un bloque de que contiene una o varias instrucciones
yield
(§13.15) se denomina bloque iterador, incluso si esas instruccionesyield
solo se incluyen indirectamente en bloques anidados (excepto las lambdas anidadas y las funciones locales).[...]
Es un error en tiempo de compilación que un bloque de iterador contenga un contexto inseguro (§23.2). Un bloque de iterador siempre define un contexto seguro, incluso cuando su declaración está anidada en un contexto no seguro.El bloque de iterador usado para implementar un iterador (§15.14) siempre define un contexto seguro, incluso cuando la declaración del iterador está anidada en un contexto no seguro.
A partir de esta especificación también se muestra lo siguiente:
- Si una declaración de iterador está marcada con el modificador
unsafe
, la firma se encuentra en un ámbito no seguro, pero el bloque de iterador usado para implementar ese iterador sigue definiendo un ámbito seguro. - El descriptor de acceso
set
de una propiedad o indexador de iterador (es decir, su descriptor de accesoget
se implementa a través de un bloque de iterador) "hereda" su ámbito de seguro/no seguro de la declaración. - Esto no afecta a las declaraciones parciales sin implementación, ya que solo son firmas y no pueden tener un cuerpo de iterador.
Tenga en cuenta que en C# 12 es un error tener un método de iterador marcado con el modificador unsafe
, pero eso se permite en C# 13 debido al cambio de especificación.
Por ejemplo:
using System.Collections.Generic;
using System.Threading.Tasks;
class A : System.Attribute { }
unsafe partial class C1
{ // unsafe context
[/* unsafe context */ A]
IEnumerable<int> M1(
/* unsafe context */ int*[] x)
{ // safe context (this is the iterator block implementing the iterator)
yield return 1;
}
IEnumerable<int> M2()
{ // safe context (this is the iterator block implementing the iterator)
unsafe
{ // unsafe context
{ // unsafe context (this is *not* the block implementing the iterator)
yield return 1; // error: `yield return` in unsafe context
}
}
}
[/* unsafe context */ A]
unsafe IEnumerable<int> M3(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
[/* unsafe context */ A]
IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
[/* unsafe context */ A]
unsafe IEnumerable<int> this[
/* unsafe context */ long*[] x]
{ // unsafe context (the iterator declaration is unsafe)
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
IEnumerable<int> M4()
{
yield return 1;
var lam1 = async () =>
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield(); // error in C# 12, allowed in C# 13
int* p = null; // error in both C# 12 and C# 13 (unsafe in iterator)
};
unsafe
{
var lam2 = () =>
{ // unsafe context, lambda cannot be an iterator
yield return 1; // error: yield cannot be used in lambda
};
}
async void local()
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield(); // error in C# 12, allowed in C# 13
int* p = null; // allowed in C# 12, error in C# 13 (breaking change in Roslyn)
}
local();
}
public partial IEnumerable<int> M5() // unsafe context (inherits from parent)
{ // safe context
yield return 1;
}
}
partial class C1
{
public partial IEnumerable<int> M5(); // safe context (inherits from parent)
}
class C2
{ // safe context
[/* unsafe context */ A]
unsafe IEnumerable<int> M(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
unsafe IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
}
§13.6.2.4 Declaraciones de variables locales ref:
Es un error en tiempo de compilación declarar una variable local ref, o una variable de tipoEs un error en tiempo de compilación declarar y usar (incluso implícitamente en código sintetizado por el compilador) una variable local ref, o una variable de tiporef struct
, dentro de un método declarado con el method_modifierasync
o dentro de un iterador (§15.14).ref struct
entre expresionesawait
o instruccionesyield return
. Más concretamente, el error se controla mediante el siguiente mecanismo: después de una expresiónawait
(§12.9.8) o una instrucciónyield return
(§13.15), todas las variables locales ref y variables de tiporef struct
en el ámbito se consideran definitivamente sin asignar (§9.4).
Tenga en cuenta que este error no se reduce a una advertencia en contextos unsafe
, como algunos otros errores de seguridad de referencia.
Esto se debe a que estas variables locales similares a ref no se pueden manipular en contextos de unsafe
sin depender de los detalles de implementación de cómo funciona la reescritura de la máquina de estado, por lo que este error queda fuera de los límites de lo que queremos degradar a advertencias en contextos de unsafe
.
§15.14.1 Iteradores > General:
Cuando un miembro de función se implementa mediante un bloque de iterador, es un error de tiempo de compilación que la lista de parámetros formales del miembro de función especifique cualquier parámetro
in
,ref readonly
,out
oref
, o un parámetro de tiporef struct
o de tipo puntero.
No se necesita ningún cambio en la especificación para permitir los bloques unsafe
que no contienen await
en métodos asincrónicos, ya que la especificación nunca ha prohibido los bloques unsafe
en métodos asincrónicos.
Sin embargo, la especificación nunca debió haber permitido await
dentro de bloques unsafe
(ya había prohibido yield
en unsafe
en §13.3.1 como se mencionó anteriormente), por lo que proponemos el siguiente cambio en la especificación:
§15.15.1 Funciones asincrónicas > General:
Es un error en tiempo de compilación que la lista de parámetros formales de una función asincrónica especifique cualquier parámetro
in
,out
, oref
, o cualquier parámetro de tiporef struct
.Es un error en tiempo de compilación que un contexto no seguro (§23.2) contenga una expresión
await
(§12.9.8) o una instrucciónyield return
(§13.15).
§23.6.5 El operador address-of:
Se notificará un error en tiempo de compilación si se toma una dirección de un parámetro o variable local en un iterador.
Actualmente, tomar la dirección de una variable local o de un parámetro en un método asincrónico es una advertencia de la ola de advertencias de C# 12.
Tenga en cuenta que más construcciones pueden funcionar gracias al ref
permitido dentro de segmentos sin await
y yield
en métodos asincrónicos o de iteradores, aunque no se necesite un cambio de especificación específico para ellas, ya que todo procede de los cambios de especificación mencionados.
using System.Threading.Tasks;
ref struct R
{
public ref int Current { get { ... }};
public bool MoveNext() => false;
public void Dispose() { }
}
class C
{
public R GetEnumerator() => new R();
async void M()
{
await Task.Yield();
using (new R()) { } // allowed under this proposal
foreach (var x in new C()) { } // allowed under this proposal
foreach (ref int x in new C()) { } // allowed under this proposal
lock (new System.Threading.Lock()) { } // allowed under this proposal
await Task.Yield();
}
}
Alternativas
ref
/ Las variables localesref struct
solo se pueden permitir en bloques (§13.3.1) que no contienenawait
/yield
:// error always since `x` is declared/used both before and after `await` { ref int x = ...; await Task.Yield(); x.ToString(); } // allowed as proposed (`x` does not need to be hoisted as it is not used after `await`) // but alternatively could be an error (`await` in the same block) { ref int x = ...; x.ToString(); await Task.Yield(); }
yield return
dentro delock
podría ser un error, similar aawait
dentro delock
) o una advertencia de tipo oleada, pero eso sería un cambio importante: https://github.com/dotnet/roslyn/issues/72443. Tenga en cuenta que el nuevoLock
basado en objetoslock
notifica errores en tiempo de compilación parayield return
s en su cuerpo, ya que dicha instrucciónlock
es equivalente ausing
en unref struct
que no permiteyield return
s en su cuerpo.Las variables dentro de los métodos asincrónicos o iteradores no deben ser "fijas", sino "movibles" si deben moverse a campos de la máquina de estado (de forma similar a las variables capturadas). Tenga en cuenta que se trata de un error preexistente en la especificación independiente del resto de la propuesta porque siempre se permitieron los bloques
unsafe
dentro de los métodosasync
. Actualmente, hay una advertencia para esto en la oleada de advertencia de C# 12 y considerarlo en un error sería un cambio importante.§23.4 Variables fijas y movibles:
En términos precisos, una variable fija es una de las siguientes:
- una variable resultante de un simple_name (§12.8.4) que hace referencia a una variable local, parámetro de valor o matriz de parámetros, a menos que una función anónima capture la variable (§12.19.6.2) o una función local (§13.6.4) o la variable debe elevarse como parte de un método asincrónico (§15.15) o de iterador (§15.14).
- [...]
Actualmente, tenemos una advertencia existente en la oleada de advertencias de C# 12 para el operador address-of en métodos asíncronos y un error propuesto para el operador address-of en iteradores notificados para LangVersion 13+ (no es necesario notificarlo en versiones anteriores porque era imposible usar código no seguro en iteradores). Podríamos relajar ambas para que se apliquen únicamente a las variables que realmente se elevan, y no a todas las variables locales y parámetros.
Podría ser posible usar
fixed
para obtener la dirección de una variable elevada o capturada, aunque el hecho de que estas sean campos es un detalle de implementación; por lo tanto, en otras implementaciones, podría no ser posible usarfixed
con ellas. Tenga en cuenta que solo proponemos considerar también variables elevadas como "movibles", pero las variables capturadas ya eran "movibles" yfixed
no estaba permitido para ellas.
Podríamos permitir
await
/yield
dentro deunsafe
excepto dentro de instruccionesfixed
(el compilador no puede anclar variables a través de los límites del método). Esto podría dar lugar a un comportamiento inesperado, por ejemplo, alrededor destackalloc
como se describe en el siguiente apartado anidado. De lo contrario, la elevación de punteros se admite incluso hoy en día en algunos escenarios (hay un ejemplo a continuación relacionado con punteros como argumentos), por lo que no debe haber otras restricciones para permitirlo.- Podríamos no permitir la variante no segura de
stackalloc
en métodos asincrónicos o de iteradores, ya que el búfer asignado a la pila no se encuentra en instruccionesawait
/yield
. No parece necesario porque, por diseño, el código inseguro no impide el "uso después de liberar". Tenga en cuenta que también podríamos permitirstackalloc
no seguro siempre que no se use enawait
/yield
, pero analizarlo podría ser difícil (el puntero resultante se puede pasar en cualquier variable de puntero). O podríamos requerir que seafixed
en métodos asincrónicos/iteradores. Esto desaconseja usarlo enawait
/yield
pero no coincidiría con la semántica defixed
porque la expresiónstackalloc
no es un valor desplazable. (Tenga en cuenta que no sería imposible usar el resultado destackalloc
enawait
/yield
de forma similar, ya que actualmente puede guardar cualquier punterofixed
en otra variable de puntero y usarlo fuera del bloquefixed
).
- Podríamos no permitir la variante no segura de
Se podría permitir que iteradores y métodos asincrónicos tengan parámetros de puntero. Deberían elevarse, pero eso no debería ser un problema, ya que la elevación de punteros se admite incluso hoy, por ejemplo:
unsafe public void* M(void* p) { var d = () => p; return d(); }
La propuesta mantiene actualmente (y amplía o aclara) la especificación preexistente de que los métodos iteradores comienzan un contexto seguro incluso si se encuentran en un contexto no seguro. Por ejemplo, un método de iterador no es un contexto no seguro aunque se defina en una clase que tenga el modificador
unsafe
. Como alternativa, podríamos hacer que los iteradores "hereden" el modificadorunsafe
de la misma manera que otros métodos hacen.- Ventaja: elimina la complejidad de la especificación y la implementación.
- Ventaja: alinea iteradores con métodos asincrónicos (una de las motivaciones de la característica).
- Desventaja: los iteradores dentro de clases no seguras no podían contener instrucciones
yield return
, estos iteradores tendrían que definirse en una declaración de clase parcial independiente sin el modificadorunsafe
. - Desventaja: esto sería un cambio importante en LangVersion=13 (se permiten iteradores en clases no seguras en C# 12).
En vez de que un iterador defina un contexto seguro solo para el cuerpo, toda la firma podría ser un contexto seguro. Esto es incoherente con el resto del lenguaje, ya que normalmente los cuerpos no afectan a las declaraciones, pero aquí una declaración sería segura o insegura dependiendo de si el cuerpo es un iterador o no. También sería un cambio importante en LangVersion=13, ya que, en C# 12, las firmas de los iteradores no son seguras (pueden incluir parámetros de matrices de punteros, por ejemplo).
Aplicar el modificador
unsafe
a un iterador:- Podría afectar tanto al cuerpo como a la firma. Estos iteradores no serían muy útiles, ya que sus cuerpos no seguros no podían contener
yield return
s; solo podían teneryield break
s. - Podría ser un error en
LangVersion >= 13
, al igual que enLangVersion <= 12
, porque no es muy útil tener un miembro iterador no seguro, ya que solo permite tener parámetros de matriz de punteros o establecedores no seguros sin un bloque adicional de código no seguro. Pero los argumentos de puntero normales se podrían permitir en el futuro.
- Podría afectar tanto al cuerpo como a la firma. Estos iteradores no serían muy útiles, ya que sus cuerpos no seguros no podían contener
Cambio importante de Roslyn:
- Podríamos conservar el comportamiento actual (e incluso modificar la especificación para que coincida con ella), por ejemplo, introduciendo el contexto seguro en el método iterador, pero volviendo al contexto no seguro en la función local.
- O podríamos romper todas las versiones de Lang, no solo la 13 y las más recientes.
- También es posible simplificar más drásticamente las reglas al hacer que los iteradores hereden contexto no seguro como lo hacen todos los demás métodos. Se ha explicado anteriormente.
Puede realizarse en todas las LangVersions o solo para
LangVersion >= 13
.
Reuniones de diseño
- 03-06-2024: revisión posterior a la implementación del speclet
C# feature specifications