Compartir a través de


Miembros de instancia readonly

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 y 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 especificaciones.

Problema planteado por el experto: <https://github.com/dotnet/csharplang/issues/1710>

Resumen

Proporcione una manera de especificar que los miembros de instancia individuales de una estructura no modifiquen el estado, de la misma manera que readonly struct especifica que ningún miembro de instancia modifique el estado.

Vale la pena tener en cuenta que el miembro de instancia de solo lectura != miembro de instancia puro. Un miembro de instancia puro garantiza que no se modificará ningún estado. Un miembro de instancia de readonly solo garantiza que no se modifique el estado de la instancia.

Todos los miembros de instancia de un readonly struct se podrían considerar implícitamente miembros de instancia de solo lectura. Los miembros de instancia de solo lectura explícitos declarados en structs no de solo lectura, se comportarían de la misma manera. Por ejemplo, seguirían creando copias ocultas si llamara a un miembro de la instancia (ya sea en la instancia actual o en un campo de la instancia) que no fuera de solo lectura.

Motivación

Antes de C# 8.0, los usuarios tenían la capacidad de crear tipos readonly struct que el compilador exige que todos los campos sean de solo lectura (y, por extensión, que ningún miembro de instancia modifique el estado). Sin embargo, hay algunos escenarios en los que usted tiene una API existente que expone campos accesibles o que tiene una mezcla de miembros mutantes y no mutantes. En estas circunstancias, no puede marcar el tipo como readonly (sería un cambio de ruptura).

Esto normalmente no tiene mucho impacto, excepto en el caso de parámetros in. Con parámetros in para structs no legibles, el compilador hará una copia del parámetro para cada invocación de miembro de instancia, ya que no puede garantizar que la invocación no modifique el estado interno. Esto puede provocar una gran cantidad de copias y un rendimiento general peor que si hubiera pasado el struct directamente por valor. Para ver un ejemplo, consulte este código en sharplab

Algunos otros escenarios en los que se pueden producir copias ocultas incluyen campos static readonly y literales. Si se admitieran en el futuro, las constantes que puede transferirse en bloque de bits también acabarían en la misma situación; es decir, actualmente todas requieren una copia completa (al invocar al miembro de instancia) si el struct no está marcado como readonly.

Diseño

Permitir que un usuario especifique que un miembro de instancia es, en sí mismo, readonly y no modifica el estado de la instancia (con toda la verificación apropiada realizada por el compilador, por supuesto). Por ejemplo:

public struct Vector2
{
    public float x;
    public float y;

    public readonly float GetLengthReadonly()
    {
        return MathF.Sqrt(LengthSquared);
    }

    public float GetLength()
    {
        return MathF.Sqrt(LengthSquared);
    }

    public readonly float GetLengthIllegal()
    {
        var tmp = MathF.Sqrt(LengthSquared);

        x = tmp;    // Compiler error, cannot write x
        y = tmp;    // Compiler error, cannot write y

        return tmp;
    }

    public readonly float LengthSquared
    {
        get
        {
            return (x * x) +
                   (y * y);
        }
    }
}

public static class MyClass
{
    public static float ExistingBehavior(in Vector2 vector)
    {
        // This code causes a hidden copy, the compiler effectively emits:
        //    var tmpVector = vector;
        //    return tmpVector.GetLength();
        //
        // This is done because the compiler doesn't know that `GetLength()`
        // won't mutate `vector`.

        return vector.GetLength();
    }

    public static float ReadonlyBehavior(in Vector2 vector)
    {
        // This code is emitted exactly as listed. There are no hidden
        // copies as the `readonly` modifier indicates that the method
        // won't mutate `vector`.

        return vector.GetLengthReadonly();
    }
}

La propiedad de solo lectura se puede aplicar a los descriptores de acceso de propiedad para indicar que this no se mutará en el descriptor de acceso. Los siguientes ejemplos tienen setters readonly porque esos accessors modifican el estado del campo miembro, pero no modifican el valor de ese campo miembro.

public readonly int Prop1
{
    get
    {
        return this._store["Prop1"];
    }
    set
    {
        this._store["Prop1"] = value;
    }
}

Cuando se aplica readonly a la sintaxis de propiedad, significa que todos los descriptores de acceso son readonly.

public readonly int Prop2
{
    get
    {
        return this._store["Prop2"];
    }
    set
    {
        this._store["Prop2"] = value;
    }
}

Solo se puede aplicar la propiedad de solo lectura a los descriptores de acceso que no mutan el tipo contenedor.

public int Prop3
{
    readonly get
    {
        return this._prop3;
    }
    set
    {
        this._prop3 = value;
    }
}

Readonly puede aplicarse a algunas propiedades auto-implementadas, pero no tendrá un efecto significativo. El compilador tratará todos los getters auto-implementados como readonly tanto si la palabra clave readonly está presente como si no.

// Allowed
public readonly int Prop4 { get; }
public int Prop5 { readonly get; set; }

// Not allowed
public int Prop6 { readonly get; }
public readonly int Prop7 { get; set; }
public int Prop8 { get; readonly set; }

La propiedad de solo lectura se puede aplicar a eventos implementados manualmente, pero no a eventos similares a campos. La propiedad de solo lectura no se puede aplicar a descriptores de acceso de eventos individuales (agregar/quitar).

// Allowed
public readonly event Action<EventArgs> Event1
{
    add { }
    remove { }
}

// Not allowed
public readonly event Action<EventArgs> Event2;
public event Action<EventArgs> Event3
{
    readonly add { }
    readonly remove { }
}
public static readonly event Event4
{
    add { }
    remove { }
}

Algunos otros ejemplos de sintaxis:

  • Miembros con forma de expresión: public readonly float ExpressionBodiedMember => (x * x) + (y * y);
  • Restricciones genéricas: public readonly void GenericMethod<T>(T value) where T : struct { }

El compilador emitiría el miembro de instancia, como de costumbre, y además emitiría un atributo reconocido por el compilador indicando que el miembro de instancia no modifica el estado. Esto hace que el parámetro oculto this se convierta en in T en lugar de ref T.

Esto permitiría al usuario llamar de forma segura a dicho método de instancia sin que el compilador necesite hacer una copia.

Las restricciones incluirían:

  • El modificador readonly no puede aplicarse a métodos estáticos, compiladores o destructores.
  • El modificador readonly no puede aplicarse a delegados.
  • El modificador readonly no puede aplicarse a miembros de clase o interfaz.

Inconvenientes

Los mismos inconvenientes que existen con los métodos readonly struct actuales. Cierto código puede seguir causando copias ocultas.

Notas

También es posible utilizar un atributo u otra palabra clave.

Esta propuesta está algo relacionada con (pero es más un subconjunto de) functional purity o constant expressions, ambos de los cuales han tenido algunas propuestas existentes.