Compartir vía


palabra clave field en propiedades

Resumen

Extienda todas las propiedades para permitirles hacer referencia a un campo de respaldo generado automáticamente mediante la nueva palabra clave contextual field. Ahora las propiedades también pueden contener un descriptor de acceso sin cuerpo junto a un descriptor de acceso con cuerpo.

Motivación

Las propiedades automáticas solo permiten establecer o obtener directamente el campo de respaldo, lo que proporciona cierto control únicamente al colocar modificadores de acceso en los accesores. A veces es necesario tener un control adicional sobre lo que sucede en uno o ambos accesores, pero esto enfrenta a los usuarios con el esfuerzo adicional de declarar un campo de respaldo. El nombre del campo de respaldo debe entonces mantenerse sincronizado con la propiedad, y el campo de respaldo se extiende a toda la clase, lo que puede dar lugar a la omisión accidental de los descriptores de acceso desde dentro de la clase.

Hay varios escenarios comunes. Dentro del getter, hay inicialización perezosa, o valores por defecto cuando la propiedad nunca ha sido dada. Dentro del establecedor, se aplica una restricción para garantizar la validez de un valor, o detectar y propagar actualizaciones, como mediante la generación del evento INotifyPropertyChanged.PropertyChanged.

En estos casos por ahora siempre tiene que crear un campo de instancia y escribir toda la propiedad su mismo. Esto no solo añade una buena cantidad de código, sino que también filtra el campo de respaldo al resto del ámbito de la clase, cuando a menudo es deseable que solo esté disponible para los cuerpos de los descriptores de acceso.

Glosario

  • Propiedad automática: Abreviatura de "propiedad implementada automáticamente" (§15.7.4). Los descriptores de acceso a una propiedad auto no tienen cuerpo. El compilador proporciona la implementación y el almacenamiento de respaldo. Las propiedades auto tienen { get; }, { get; set; }, o { get; init; }.

  • Accesor automático: Abreviatura de "accesor implementado automáticamente". Este es un accesor que no tiene cuerpo. El compilador proporciona la implementación y el almacenamiento de respaldo. get;, set; y init; son accesores automáticos.

  • Descriptor de acceso completo: Es un descriptor de acceso que tiene cuerpo. El compilador no proporciona la implementación, aunque el almacenamiento de respaldo todavía puede estar (como en el ejemplo set => field = value;).

  • Propiedad respaldada por campo: se trata de una propiedad que usa la palabra clave field dentro de un cuerpo del descriptor de acceso o una propiedad automática.

  • Campo respaldo: esta es la variable indicada por la palabra clave field en los descriptores de acceso de una propiedad, que también se lee o escribe implícitamente en descriptores de acceso implementados automáticamente (get;, set;o init;).

Diseño detallado

Para las propiedades con un descriptor de acceso init, todo lo que se aplica a continuación a set se aplicaría en su lugar al descriptor de acceso init.

Hay dos cambios de sintaxis:

  1. Existe una nueva palabra clave contextual, field, que puede utilizarse dentro de los cuerpos de descriptores de acceso de propiedades para acceder a un campo de respaldo para la declaración de la propiedad (decisión LDM).

  2. Las propiedades ahora pueden mezclar y combinar autodescriptores de acceso con descriptores de acceso completos (decisión LDM). Por "propiedad automática" se seguirá entendiendo una propiedad cuyos descriptores de acceso no tienen cuerpo. Ninguno de los ejemplos siguientes se considerarán propiedades automáticas.

Ejemplos:

{ get; set => Set(ref field, value); }
{ get => field ?? parent.AmbientValue; set; }

Ambos descriptores de acceso pueden ser descriptores de acceso completos con uno o ambos haciendo uso de field:

{ get => field; set => field = value; }
{ get => field; set => throw new InvalidOperationException(); }
{ get => overriddenValue; set => field = value; }
{
    get;
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged();
    }
}

Las propiedades con cuerpo de expresión y las propiedades con solo un descriptor de acceso get también pueden utilizar field:

public string LazilyComputed => field ??= Compute();
public string LazilyComputed { get => field ??= Compute(); }

Las propiedades que solo tienen un set también pueden utilizar field:

{
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged(new XyzEventArgs(value));
    }
}

Cambios importantes

La existencia de la palabra clave contextual field dentro de los cuerpos de descriptor de acceso de propiedad es un cambio potencialmente importante.

Dado que field es una palabra clave y no un identificador, solo puede ser "sombreado" por un identificador mediante la ruta normal de escape de palabras clave: @field. Todos los identificadores con nombre declarados field dentro de cuerpos de descriptores de acceso de propiedades pueden protegerse contra rupturas al actualizar desde versiones de C# anteriores a 14 añadiendo la palabra clave inicial @.

Si una variable denominada field se declara en un descriptor de acceso de propiedad, se notifica un error.

En la versión del lenguaje 14 o posterior, se notifica una advertencia si una expresión principal field hace referencia al campo de respaldo, pero habría hecho referencia a un símbolo diferente en una versión de idioma anterior.

Atributos orientados al campo

Al igual que con las propiedades automáticas, cualquier propiedad que utilice un campo de respaldo en uno de sus descriptores de acceso podrá utilizar atributos orientados a campos:

[field: Xyz]
public string Name => field ??= Compute();

[field: Xyz]
public string Name { get => field; set => field = value; }

Un atributo destinado a un campo permanecerá inválido a menos que un método accesor utilice un campo de respaldo.

// ❌ Error, will not compile
[field: Xyz]
public string Name => Compute();

Inicializadores de propiedades

Las propiedades con inicializadores pueden usar field. El campo de respaldo se inicializa directamente en lugar de llamar al setter (decisión LDM).

Llamar a un establecedor para un inicializador no es una opción; Los inicializadores se procesan antes de llamar a constructores base y no es válido llamar a cualquier método de instancia antes de llamar al constructor base. Esto también es importante para la inicialización predeterminada o la asignación definitiva de estructuras.

Esto produce un control flexible sobre la inicialización. Si desea inicializar sin llamar al establecedor, use un inicializador de propiedad. Si desea inicializar llamando al setter, debe asignar a la propiedad un valor inicial en el constructor.

Este es un ejemplo de dónde resulta útil. Creemos que la palabra clave field será ampliamente utilizada con modelos de vistas debido a la solución elegante que aporta para el patrón INotifyPropertyChanged. Es probable que los establecedores de propiedades del modelo de vista estén enlazados a la interfaz de usuario y causen el seguimiento de cambios o desencadenen otros comportamientos. El código siguiente debe inicializar el valor predeterminado de IsActive sin establecer HasPendingChanges en true:

class SomeViewModel
{
    public bool HasPendingChanges { get; private set; }

    public bool IsActive { get; set => Set(ref field, value); } = true;

    private bool Set<T>(ref T location, T value)
    {
        if (RuntimeHelpers.Equals(location, value))
            return false;

        location = value;
        HasPendingChanges = true;
        return true;
    }
}

Esta diferencia en el comportamiento entre un inicializador de propiedad y la asignación desde el constructor también se puede ver con propiedades automáticas virtuales en versiones anteriores del lenguaje:

using System;

// Nothing is printed; the property initializer is not
// equivalent to `this.IsActive = true`.
_ = new Derived();

class Base
{
    public virtual bool IsActive { get; set; } = true;
}

class Derived : Base
{
    public override bool IsActive
    {
        get => base.IsActive;
        set
        {
            base.IsActive = value;
            Console.WriteLine("This will not be reached");
        }
    }
}

Asignación de constructores

Al igual que con las propiedades automáticas, la asignación en el constructor llama al definidor (potencialmente virtual) si existe, y si no hay definidor vuelve a la asignación directa al campo de respaldo.

class C
{
    public C()
    {
        P1 = 1; // Assigns P1's backing field directly
        P2 = 2; // Assigns P2's backing field directly
        P3 = 3; // Calls P3's setter
        P4 = 4; // Calls P4's setter
    }

    public int P1 => field;
    public int P2 { get => field; }
    public int P4 { get => field; set => field = value; }
    public int P3 { get => field; set; }
}

Asignación definitiva en structs

Aunque no se puede hacer referencia a ellos en el constructor, los campos de reserva indicados por la palabra clave field están sujetos a advertencias predeterminadas de inicialización y deshabilitadas de forma predeterminada en las mismas condiciones que cualquier otro campo struct (decisión LDM 1, decisión LDM 2).

Por ejemplo (estos diagnósticos son silenciosos de forma predeterminada):

public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        _ = P1;
    }

    public int P1 { get => field; }
}
public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        P2 = 5;
    }

    public int P2 { get => field; set => field = value; }
}

Propiedades devueltas

Al igual que con las propiedades automáticas, la palabra clave field no estará disponible para su uso en las propiedades que devuelven referencias. Las propiedades de devolución de referencia no pueden tener descriptores de acceso establecidos y, sin un descriptor de acceso set, el descriptor de acceso get y el inicializador de propiedad serían los únicos elementos que pueden tener acceso al campo de respaldo. En ausencia de casos de uso para esto, ahora no es el momento para que las propiedades que devuelven referencias puedan ser escritas como propiedades automáticas.

Nulabilidad

Un principio de la característica Tipos de referencia que aceptan valores NULL era comprender los patrones de codificación idiomática existentes en C# y requerir la menor ceremonia posible en torno a esos patrones. La propuesta de palabra clave field permite que los patrones idiomáticos simples aborden escenarios ampliamente solicitados, como propiedades inicializadas de forma diferida. Es importante que los Tipos de Referencia Anulables encajen bien con estos nuevos patrones de codificación.

Metas:

  • Se debe garantizar un nivel razonable de seguridad nula para varios patrones de uso de la característica de palabra clave field.

  • Los patrones que usan la palabra clave field deben sentirse como si siempre hubieran sido parte del lenguaje. Evite hacer que el usuario pase por el aro para habilitar los Tipos de referencia que aceptan valores NULL en código que es perfectamente idiomático para la característica de la palabra clave field.

Uno de los escenarios clave son las propiedades inicializadas perezosamente:

public class C
{
    public C() { } // It would be undesirable to warn about 'Prop' being uninitialized here

    string Prop => field ??= GetPropValue();
}

Las siguientes reglas de nulabilidad se aplicarán no solo a las propiedades que usan la palabra clave field, sino también a las propiedades automáticas existentes.

Nulabilidad del campo de respaldo

Consulte glosario para obtener definiciones de nuevos términos.

El campo de respaldo tiene el mismo tipo que la propiedad. Sin embargo, su anotación de anulabilidad puede diferir de la propiedad. Para determinar esta anotación que admite valores NULL, introducimos el concepto de resiliencia a valores nulos. La resiliencia ante valores nulos significa intuitivamente que el descriptor de acceso a la propiedad get conserva la seguridad frente a valores nulos incluso cuando el campo contiene el valor default correspondiente a su tipo.

Para determinar si una propiedad respaldada por un campo es resistente a los valores nulos o no, se realiza un análisis especial de su descriptor de acceso get.

  • Para los fines de este análisis, se supone que field temporalmente tienen anotación nulabilidad, por ejemplo, string?. Esto hace que field tenga maybe-null o maybe-default estado inicial en el descriptor de acceso get, dependiendo de su tipo.
  • Entonces, si el análisis de anulabilidad del getter no produce advertencias de anulabilidad, la propiedad es resistente a valores NULL. En caso contrario, no es resistente a los valores nulos.
  • Si la propiedad no tiene un descriptor de acceso get, es (vacuamente) resistente a los valores nulos.
  • Si el descriptor de acceso get se implementa automáticamente, la propiedad no es resistente a valores NULL.

La nulabilidad del campo de respaldo se determina de la siguiente manera:

  • Si el campo tiene atributos de nulabilidad, como [field: MaybeNull], AllowNull, NotNullo DisallowNull, la anotación que acepta valores NULL del campo es la misma que la anotación que acepta valores NULL de la propiedad.
    • Esto se debe a que cuando el usuario empieza a aplicar atributos de nulabilidad al campo, ya no queremos deducir nada, solo queremos que la nulabilidad sea lo que el usuario dijo.
  • Si la propiedad contenedora tiene anulabilidad olvidada o anotada, el campo de respaldo tiene la misma anulabilidad que la propiedad.
  • Si la propiedad contenedora tiene nulabilidad no anotada (por ejemplo, string o T) o tiene el atributo [NotNull] y la propiedad es resistente a valores NULL, el campo de respaldo tiene nulabilidad anotada.
  • Si la propiedad contenedora tiene nulabilidad no anotada (por ejemplo, string o T) o tiene el atributo [NotNull] y la propiedad es no resistente a valores NULL, el campo de respaldo tiene nulabilidad no anotada.

Análisis de constructores

Actualmente, una propiedad auto se trata de forma muy similar a un campo ordinario en el análisis de constructores que aceptan valores NULL. Ampliamos este tratamiento para las propiedades respaldadas por campos , tratando cada una de estas como un intermediario para su campo de respaldo.

Actualizamos el siguiente lenguaje de especificación del enfoque propuesto anterior para lograr esto:

En cada "devolución" explícita o implícita de un constructor, se proporciona una advertencia para cada miembro cuyo estado de flujo es incompatible con sus anotaciones y atributos de nulabilidad. Si el miembro es una propiedad respaldada por campo, la anotación anulable del campo de respaldo se utiliza para esta comprobación. En caso contrario, se utilizará la anotación anulable del propio miembro. Una aproximación razonable para esto es: si asignar el miembro a sí mismo en el punto de retorno produjera una advertencia de anulabilidad, entonces se producirá una advertencia de anulabilidad en el punto de retorno.

Tenga en cuenta que esto es básicamente un análisis interprocedural restringido. Se anticipa que para analizar un constructor, será necesario realizar el análisis de vinculación y la resiliencia frente a valores nulos en todos los descriptores de acceso get aplicables del mismo tipo, que utilizan la palabra clave contextual field y tienen nulabilidad no anotada. Especulamos que esto no es prohibitivamente caro porque los cuerpos de los getters no suelen ser muy complejos, y que el análisis de «resistencia a los nulos» solo necesita realizarse una vez independientemente de cuántos constructores haya en el tipo.

Análisis de los setters

Para simplificar, utilizamos los términos "setter" y "set accessor" para referirnos a un set o descriptor de acceso de init.

Es necesario comprobar que los definidores de propiedades respaldadas por campos inicializan realmente el campo de respaldo.

class C
{
    string Prop
    {
        get => field;

        // getter is not null-resilient, so `field` is not-annotated.
        // We should warn here that `field` may be null when exiting.
        set { }
    }

    public C()
    {
        Prop = "a"; // ok
    }

    public static void Main()
    {
        new C().Prop.ToString(); // NRE at runtime
    }
}

El estado de flujo inicial del campo de respaldo en el setter de una propiedad respaldada por campo se determina como sigue:

  • Si la propiedad tiene un inicializador, el estado de flujo inicial es el mismo que el estado de flujo de la propiedad después de visitar el inicializador.
  • De lo contrario, el estado de flujo inicial es el mismo que el estado de flujo proporcionado por field = default;.

En cada "devolución" explícita o implícita del setter, se notifica una advertencia si el estado de flujo del campo de respaldo es incompatible con sus anotaciones y atributos de anulabilidad.

Observaciones

Esta formulación es intencionadamente muy similar a la de los campos ordinarios en los constructores. Esencialmente, debido a que solo los descriptores de acceso de propiedad pueden referirse realmente al campo de respaldo, el establecedor es tratado como un "mini-constructor" para el campo de respaldo.

Al igual que con los campos ordinarios, normalmente sabemos que la propiedad se inicializó en el constructor porque se estableció, pero no necesariamente. Simplemente devolver dentro de una rama donde Prop != null era verdadero también es suficiente para nuestro análisis de constructores, ya que entendemos que los mecanismos no rastreados pueden haberse usado para establecer la propiedad.

Se consideraron alternativas; véase la sección Alternativas de anulabilidad.

nameof

En lugares donde field es una palabra clave, nameof(field) fallará al compilar (decisión LDM), como nameof(nint). No es como nameof(value), que es lo que se debe usar cuando los establecedores de propiedades inician ArgumentException como algunos en las bibliotecas de .NET Core. En cambio, nameof(field) no tiene casos de uso esperados.

Anulaciones

Las propiedades sustitutivas pueden utilizar field. Tales usos de field se refieren al campo de respaldo de la propiedad que anula, separado del campo de respaldo de la propiedad base si tiene uno. No existe una ABI para exponer el campo de respaldo de una propiedad base a las clases que la sobrescriben, ya que esto rompería la encapsulación.

Al igual que con las propiedades automáticas, las propiedades que usan la palabra clave field y sobrescriben una propiedad base deben sobrescribir todos los descriptores de acceso (decisión LDM).

Capturas

field debería poder ser capturado en funciones locales y expresiones lambda, y se permiten referencias a field desde dentro de funciones locales y expresiones lambda, incluso cuando no haya otras referencias (decisión 1 de LDM, decisión 2 de LDM):

public class C
{
    public static int P
    {
        get
        {
            Func<int> f = static () => field;
            return f();
        }
    }
}

Advertencias sobre el uso de campos

Cuando se usa la palabra clave field en un descriptor de acceso, el análisis existente del compilador de campos sin asignar o sin leer incluirá ese campo.

  • CS0414: se asigna el campo de respaldo para la propiedad "Xyz", pero nunca se usa su valor.
  • CS0649: El campo de respaldo de la propiedad "Xyz" nunca se le asigna un valor, y siempre tendrá su valor predeterminado.

Cambios de especificación

Sintaxis

Al compilar con la versión 14 o superior del lenguaje, field se considera una palabra clave cuando se utiliza como expresión primaria (decisión LDM) en las siguientes ubicaciones (decisión LDM):

  • En los cuerpos de los métodos de get, set, y en los descriptores de acceso init de las propiedades , pero no en los indexadores
  • En los atributos aplicados a dichos descriptores de acceso
  • En expresiones lambda anidadas y funciones locales, y en expresiones LINQ en esos descriptores de acceso

En todos los demás casos, incluida la compilación con la versión de idioma 12 o inferior, field se considera un identificador.

primary_no_array_creation_expression
    : literal
+   | 'field'
    | interpolated_string_expression
    | ...
    ;

Propiedades

§15.7.1Propiedades - General

Un property_initializer solo puede darse para una propiedad implementada automáticamente, yuna propiedad que tiene un campo de respaldo que será emitido. El property_initializer provoca la inicialización del campo subyacente de tales propiedades con el valor dado por la expresión.

§15.7.4propiedades implementadas automáticamente

Una propiedad implementada automáticamente (o auto-propiedad para abreviar), es una propiedad no abstracta, no-externa, no-ref-valued con descriptores de acceso de solo punto y coma. Las propiedad automáticas deben tener un descriptor de acceso get y opcionalmente pueden tener un descriptor de acceso set.o ambos de:

  1. un descriptor de acceso con un cuerpo de solo punto y coma
  2. el uso de la palabra clave contextual field dentro delcuerpo de descriptores de acceso o expresiones de la propiedad

Cuando una propiedad se especifica como una propiedad implementada automáticamente, un campo de respaldo oculto sin nombre está automáticamente disponible para la propiedad , y los descriptores de acceso se implementan para leer y escribir en ese campo de respaldo. En el caso de las propiedad automáticas, se implementa cualquier descriptor de acceso get de solo punto y coma para leer desde, y cualquier descriptor de acceso de solo punto y comaset para escribir en su campo de respaldo.

El campo de respaldo oculto no es accesible, solo se puede leer y escribir a través de los descriptores de acceso de propiedad implementados automáticamente, incluso dentro del tipo contenedor.Se puede hacer referencia directamente al campo de respaldo mediante la palabra clave fielddentro de todos los descriptores de acceso y dentro del cuerpo de la expresión de propiedad. Dado que el campo no tiene nombre, no se puede usar en una expresiónnameof.

Si la propiedad automática no tiene un descriptor de acceso set, solo un descriptor de acceso get de solo punto y coma, se considera el campo de respaldo readonly (§15.5.3). Al igual que un campo readonly, una propiedad automática de solo lectura (sin un descriptor de acceso set o un descriptor de acceso init) también puede ser asignada en el cuerpo de un constructor de la clase que la contiene. Una asignación de este tipo asigna directamente al campo de respaldo de solo lectura de la propiedad.

No se permite que una propiedad automática tenga un único descriptor de acceso de solo punto y coma set sin un descriptor de acceso get.

Una propiedad automática puede tener opcionalmente un property_initializer, que se aplica directamente al campo de respaldo como variable_initializer (§17.7).

En el ejemplo siguiente:

// No 'field' symbol in scope.
public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

es equivalente a la siguiente declaración:

// No 'field' symbol in scope.
public class Point
{
    public int X { get { return field; } set { field = value; } }
    public int Y { get { return field; } set { field = value; } }
}

que es equivalente a:

// No 'field' symbol in scope.
public class Point
{
    private int __x;
    private int __y;
    public int X { get { return __x; } set { __x = value; } }
    public int Y { get { return __y; } set { __y = value; } }
}

En el ejemplo siguiente:

// No 'field' symbol in scope.
public class LazyInit
{
    public string Value => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

es equivalente a la siguiente declaración:

// No 'field' symbol in scope.
public class Point
{
    private string __value;
    public string Value { get { return __value ??= ComputeValue(); } }
    private static string ComputeValue() { /*...*/ }
}

Alternativas

Alternativas de nulabilidad

Además del enfoque de null-resilience descrito en la sección Nulabilidad, el grupo de trabajo sugirió las siguientes alternativas para su consideración por parte del LDM:

No hacer nada

No podríamos introducir ningún comportamiento especial aquí. Vigente:

  • Tratar una propiedad respaldada por campo de la misma forma que se tratan las propiedad automáticas hoy en día: deben inicializarse en el constructor excepto cuando se marcan como requeridas, etc.
  • No dar un tratamiento especial a la variable de campo al analizar los descriptores de acceso de propiedades. Es simplemente una variable con el mismo tipo y nulabilidad que la propiedad .

Tenga en cuenta que esto daría lugar a advertencias molestas para los escenarios de "propiedad perezosa", en cuyo caso los usuarios probablemente necesitarían asignar null! o similar para silenciar las advertencias del constructor.
Una "sub-alternativa" que podemos considerar es omitir completamente las propiedades utilizando la palabra clave field para el análisis de constructores nulos. En ese caso, no habría advertencias en ningún lugar sobre la necesidad de que el usuario inicialice algo, pero tampoco habría molestias para el usuario, independientemente del patrón de inicialización que pueda usar.

Dado que solo tenemos previsto distribuir la función de palabras clave field con la versión preliminar LangVersion en .NET 9, esperamos poder cambiar el comportamiento de anulabilidad de la función en .NET 10. Por lo tanto, podríamos considerar la adopción de una solución de "menor costo" como esta en el corto plazo y crecer hasta una de las soluciones más complejas a largo plazo.

atributos de anulabilidad field

Podríamos introducir los siguientes valores predeterminados, logrando un nivel razonable de seguridad nula, sin implicar ningún análisis interprocedural en absoluto:

  1. La variable field siempre tiene la misma anotación de anulabilidad que la propiedad.
  2. Los atributos de nulabilidad [field: MaybeNull, AllowNull] etc. se pueden usar para personalizar la nulabilidad del campo de respaldo.
  3. se comprueba la inicialización de las propiedades respaldadas por campo en los constructores basándose en la anotación y los atributos de anulabilidad del campo.
  4. los setters en propiedades respaldadas por campos comprueban la inicialización de forma similar field a los constructores.

Esto significaría que el "little-l lazy scenario" se vería así en su lugar:

class C
{
    public C() { } // no need to warn about initializing C.Prop, as the backing field is marked nullable using attributes.

    [field: AllowNull, MaybeNull]
    public string Prop => field ??= GetPropValue();
}

Una de las razones por las que evitamos usar atributos de anulabilidad es que los que tenemos están orientados a describir entradas y salidas de firmas. Son complicados de usar para describir la nulabilidad de las variables de larga duración.

  • En la práctica, se requiere [field: MaybeNull, AllowNull] para que el campo se comporte "razonablemente" como una variable anulable, lo que proporciona un estado de ejecución posiblemente nulo y permite escribir posibles valores nulos en él. Nos parece engorroso pedir a los usuarios que lo hagan para escenarios "little-l lazy" relativamente comunes.
  • Si perseguimos este enfoque, consideraríamos agregar una advertencia cuando se usa [field: AllowNull], lo que sugiere agregar también MaybeNull. Esto se debe a que AllowNull por sí mismo no hace lo que los usuarios necesitan de una variable que acepta valores NULL: supone que el campo no es null inicialmente cuando nunca vimos nada escrito en ella todavía.
  • También podríamos considerar la posibilidad de ajustar el comportamiento de [field: MaybeNull] en la palabra clave field, o incluso campos en general, para permitir que también se escriban valores NULL en la variable, como si AllowNull estuvieran presentes implícitamente.

Preguntas respondidas sobre LDM

Ubicaciones de sintaxis para palabras clave

En los descriptores de acceso donde field y value podrían enlazarse a un campo de respaldo sintetizado o a un parámetro implícito de setter, ¿en qué ubicaciones de la sintaxis deben los identificadores considerarse como palabras clave?

  1. siempre
  2. expresiones primarias solo
  3. nunca

Los dos primeros casos son cambios de ruptura.

Si los identificadores se consideran siempre palabras clave, se trata de un cambio rupturista para lo que sigue, por ejemplo:

class MyClass
{
    private int field;
    public int P => this.field; // error: expected identifier

    private int value;
    public int Q
    {
        set { this.value = value; } // error: expected identifier
    }
}

Si los identificadores son palabras clave solo cuando se utilizan como expresiones primarias, el cambio de ruptura es menor. La ruptura más común puede ser el uso no cualificado de un miembro existente llamado field.

class MyClass
{
    private int field;
    public int P => field; // binds to synthesized backing field rather than 'this.field'
}

También hay una interrupción cuando se redeclaran field o value en una función anidada. Esta puede ser la única interrupción para value las expresiones primarias.

class MyClass
{
    private IEnumerable<string> _fields;
    public bool HasNotNullField
    {
        get => _fields.Any(field => field is { }); // 'field' binds to synthesized backing field
    }
    public IEnumerable<string> Fields
    {
        get { return _fields; }
        set { _fields = value.Where(value => Filter(value)); } // 'value' binds to setter parameter
    }
}

Si los identificadores nunca se consideran palabras clave, los identificadores solo se vincularán a un campo de respaldo sintetizado o al parámetro implícito cuando los identificadores no se vinculen a otros miembros. No hay ningún cambio importante en este caso.

Respuesta

field es una palabra clave en descriptores de acceso adecuados cuando se usa como expresión primaria solamente; value nunca se considera una palabra clave.

Escenarios similares a { set; }

{ set; } no se permite actualmente y esto tiene sentido: el campo que crea nunca se puede leer. Ahora hay nuevas formas de acabar en una situación en la que el establecedor introduce un campo de respaldo que nunca se lee, como la expansión de { set; } en { set => field = value; }.

¿Cuál de estos escenarios debe poder compilarse? Supongamos que la advertencia "field is never read" se aplicaría igual que con un campo declarado manualmente.

  1. { set; } - Prohibido hoy, seguir prohibiendo
  2. { set => field = value; }
  3. { get => unrelated; set => field = value; }
  4. { get => unrelated; set; }
  5. {
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    
  6. {
        get => unrelated;
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    

Respuesta

Solo se desautoriza lo que ya está desautorizado hoy en las propiedades automáticas, el descriptor de acceso de eventos set;.

field en descriptor de acceso de evento

¿Debería field ser una palabra clave en un descriptor de acceso de evento, y debería el compilador generar un campo de respaldo?

class MyClass
{
    public event EventHandler E
    {
        add { field += value; }
        remove { field -= value; }
    }
}

Recomendación: field no es una palabra clave dentro de un descriptor de acceso de evento, y no se genera un campo de respaldo.

Respuesta

Recomendación tomada. field no es una palabra clave dentro de un descriptor de acceso de evento, y no se genera un campo de respaldo.

Nulabilidad de field

¿Se debe aceptar la nulabilidad propuesta de field? Ver la sección Anulabilidad, y la pregunta abierta dentro.

Respuesta

Se aprueba la propuesta general. El comportamiento específico todavía necesita más revisión.

field en el inicializador de propiedades

¿Debe field ser un término clave en un inicializador de propiedad y vincularse con el campo de respaldo?

class A
{
    const int field = -1;

    object P1 { get; } = field; // bind to const (ok) or backing field (error)?
}

¿Hay escenarios útiles para hacer referencia al campo de respaldo en el inicializador?

class B
{
    object P2 { get; } = (field = 2);        // error: initializer cannot reference instance member
    static object P3 { get; } = (field = 3); // ok, but useful?
}

En el ejemplo anterior, el enlace al campo de respaldo debería producir un error: "El inicializador no puede hacer referencia a un campo no estático".

Respuesta

Enlazaremos el inicializador como en versiones anteriores de C#. No pondremos el campo de respaldo en el ámbito, ni evitaremos hacer referencia a otros miembros llamados field.

Interacción con propiedades parciales

Inicializadores

Cuando una propiedad parcial usa field, ¿qué partes deben tener un inicializador?

partial class C
{
    public partial int Prop { get; set; } = 1;
    public partial int Prop { get => field; set => field = value; } = 2;
}
  • Parece evidente que se debe producir un error cuando ambas partes tienen un inicializador.
  • Podemos pensar en casos de uso en los que la definición o la parte de implementación podrían querer establecer el valor inicial de la field.
  • Parece que si permitimos el inicializador en la parte de definición, obliga al implementador a usar field para que el programa sea válido. ¿Está bien?
  • Creemos que será habitual que los generadores usen field siempre que se necesite un campo de respaldo del mismo tipo en la implementación. Esto se debe en parte a que los generadores suelen querer permitir que los usuarios usen atributos de destino [field: ...] en la parte de definición de propiedad. El uso de la palabra clave field ahorra al implementador del generador el problema de "reenviar" dichos atributos a algún campo generado y suprimir las advertencias de la propiedad. Es probable que esos mismos generadores también quieran permitir al usuario especificar un valor inicial para el campo.

Recomendación: Permitir un inicializador en cualquiera de las partes de una propiedad parcial cuando la parte de implementación utiliza field. Informe de un error si ambas partes tienen un inicializador.

Respuesta

Recomendación aceptada. Tanto al declarar como al implementar las ubicaciones de las propiedades se puede utilizar un inicializador, pero no ambos a la vez.

Descriptores de acceso automáticos

Tal y como se diseñó originalmente, la implementación de propiedades parciales debe tener cuerpos para todos los descriptores de acceso. Sin embargo, las iteraciones recientes de la función de palabras clave field han incluido la noción de "descriptores de acceso automáticos". ¿Deben las implementaciones parciales de propiedades poder usar estos descriptores de acceso? Si se usan exclusivamente, será indistinguible de una declaración de definición.

partial class C
{
    public partial int Prop0 { get; set; }
    public partial int Prop0 { get => field; set => field = value; } // this is equivalent to the two "semi-auto" forms below.

    public partial int Prop1 { get; set; }
    public partial int Prop1 { get => field; set; } // is this a valid implementation part?

    public partial int Prop2 { get; set; }
    public partial int Prop2 { get; set => field = value; } // what about this? will there be disagreement about which is the "best" style?

    public partial int Prop3 { get; }
    public partial int Prop3 { get => field; } // it will only be valid to use at most 1 auto-accessor, when a second accessor is manually implemented.

Recomendación: no permitir accesores automáticos en implementaciones de propiedades parciales, ya que las limitaciones en torno a cuándo se pueden usar son más confusas que las ventajas de permitirlos.

Respuesta

Al menos un accesor de implementación debe implementarse manualmente, pero el otro accesor se puede implementar automáticamente.

Campo de solo lectura

¿Cuándo debe considerarse de solo lectura el campo de respaldo sintetizado?

struct S
{
    readonly object P0 { get => field; } = "";         // ok
    object P1          { get => field ??= ""; }        // ok
    readonly object P2 { get => field ??= ""; }        // error: 'field' is readonly
    readonly object P3 { get; set { _ = field; } }     // ok
    readonly object P4 { get; set { field = value; } } // error: 'field' is readonly
}

Cuando el campo de respaldo se considera de solo lectura, el campo emitido a metadatos se marca initonly, y se notifica un error si field se modifica de otra forma que no sea en un inicializador o constructor.

Recomendación: El campo de respaldo sintetizado es de solo lectura cuando el tipo contenedor es un struct y la propiedad o tipo contenedor se declara readonly.

Respuesta

Se acepta la recomendación.

Contexto de solo lectura y set

¿Debería permitirse un accesor set en un contexto readonly para una propiedad que utiliza field?

readonly struct S1
{
    readonly object _p1;
    object P1 { get => _p1; set { } }   // ok
    object P2 { get; set; }             // error: auto-prop in readonly struct must be readonly
    object P3 { get => field; set { } } // ok?
}

struct S2
{
    readonly object _p1;
    readonly object P1 { get => _p1; set { } }   // ok
    readonly object P2 { get; set; }             // error: auto-prop with set marked readonly
    readonly object P3 { get => field; set { } } // ok?
}

Respuesta

Podría haber escenarios para esto donde estás implementando un descriptor de acceso set en un struct readonly y pasándolo a través, o lanzándolo. Vamos a permitir esto.

Código [Conditional]

¿Debe generarse el campo sintetizado cuando field se usa solo en llamadas omitidas a métodos condicionales ?

Por ejemplo, ¿se debe generar un campo de respaldo para lo siguiente en una compilación que no sea DEBUG?

class C
{
    object P
    {
        get
        {
            Debug.Assert(field is null);
            return null;
        }
    }
}

Como referencia, los campos para parámetros de constructores primarios se generan en casos similares - consulte sharplab.io.

Recomendación: El campo de respaldo se genera cuando se utiliza field solo en llamadas omitidas a métodos condicionales.

Respuesta

Conditional pueden tener efectos en código no condicional, como cambiar la anulabilidad Debug.Assert. Sería extraño si field no tuviera impactos similares. También es poco probable que aparezca en la mayoría del código, por lo que haremos lo sencillo y aceptaremos la recomendación.

Propiedades de interfaz y auto-accesores

¿Se reconoce una combinación de descriptores de acceso implementados manualmente y automático para una propiedad interface en la que el descriptor de acceso implementado automáticamente hace referencia a un campo de respaldo sintetizado?

En el caso de una propiedad de instancia, se notificará un error que indica que no se admiten campos de instancia.

interface I
{
           object P1 { get; set; }                           // ok: not an implementation
           object P2 { get => field; set { field = value; }} // error: instance field

           object P3 { get; set { } } // error: instance field
    static object P4 { get; set { } } // ok: equivalent to { get => field; set { } }
}

Recomendación: los descriptores de acceso automáticos se reconocen en propiedades interface y los descriptores de acceso automáticos hacen referencia a un campo de respaldo sintetizado. En el caso de una propiedad de instancia, se notifica un error que indica que no se admiten campos de instancia.

Respuesta

Estandarizar en torno al propio campo de instancia como causa del error es coherente con las propiedades parciales en las clases, y nos gusta ese resultado. Se acepta la recomendación.