2010-04-27 11 views
13

Tengo una clase con varias propiedades públicas que les permito a los usuarios editar a través de una grilla de propiedades. Para la persistencia, esta clase también se serializa/deserializa a/desde un archivo XML a través de DataContractSerializer.¿Cómo puedo hacer una versión de solo lectura de una clase?

A veces deseo que el usuario pueda guardar (serializar) los cambios que ha realizado en una instancia de la clase. Sin embargo, en otras ocasiones no quiero permitir que el usuario guarde sus cambios, y en su lugar debería ver todas las propiedades en la cuadrícula de propiedades como de solo lectura. No quiero permitir que los usuarios realicen cambios que nunca podrán guardar más adelante. Similar a cómo MS Word permitirá a los usuarios abrir documentos que actualmente otros abren pero solo como de solo lectura.

Mi clase tiene una propiedad booleana que determina si la clase debe ser de solo lectura, pero ¿es posible usar esta propiedad para agregar dinámicamente atributos de solo lectura a las propiedades de la clase en tiempo de ejecución? Si no, ¿qué es una solución alternativa? ¿Debo incluir mi clase en una clase contenedora de solo lectura?

Respuesta

16

La inmutabilidad es un área donde C# todavía tiene espacio para mejorar. Si bien la creación de tipos inmutables simples con readonly propiedades es posible, una vez que necesita un control más sofisticado sobre cuándo el tipo es mutable, comienza a toparse con obstáculos.

Hay tres opciones que usted tiene, dependiendo de la fuerza con la que hay que "cumplir" sólo lectura comportamiento:

  1. Utilice un indicador de sólo lectura en su tipo (como si estuvieras haciendo) y deje que la persona que llama sea responsable de no intentar cambiar las propiedades del tipo; si se realiza un intento de escritura, genere una excepción.

  2. Crea una interfaz de solo lectura y haz que tu tipo la implemente. De esta manera puede pasar el tipo a través de esa interfaz para codificar que solo debe realizar lecturas.

  3. Cree una clase contenedora que agregue su tipo y solo exhiba las operaciones de lectura.

La primera opción es a menudo la más fácil, ya que puede requerir menos refactorización de código existente, pero ofrece la menor oportunidad para que el autor de un tipo a informar a los consumidores cuando una instancia es inmutable vs cuando está no. Esta opción también ofrece el menor soporte del compilador para detectar el uso inapropiado, y relega la detección de errores al tiempo de ejecución.

La segunda opción es conveniente, ya que la implementación de una interfaz es posible sin mucho esfuerzo de refactorización. Desafortunadamente, las personas que llaman aún pueden emitir al tipo subyacente e intentar escribir en contra de él. A menudo, esta opción se combina con un indicador de solo lectura para garantizar que no se viole la inmutabilidad.

La tercera opción es la más fuerte, en lo que respecta a la aplicación, pero puede dar lugar a la duplicación de código y es más un esfuerzo de refactorización. A menudo, es útil combinar las opciones 2 y 3 para hacer que la relación entre el contenedor de solo lectura y el tipo mutable sea polimórfico.

Personalmente, tiendo a otorgarme la tercera opción cuando escribo un código nuevo donde espero necesitar forzar la inmutabilidad. Me gusta el hecho de que es imposible "descartar" el envoltorio inmutable, y a menudo le permite evitar la creación de verificaciones sucias de solo lectura en todos los instaladores.

+0

Gracias! Lamentablemente, la opción 2 no funciona porque la cuadrícula de propiedades usará la reflexión para descubrir que las propiedades reales detrás de una interfaz no son solo de lectura. Entonces parece que la opción 3 es la mejor respuesta para mí. Aunque me gusta su sugerencia de combinar las opciones 2 y 3. –

+0

No puedo expresar cómo esta respuesta es una de las más concisas y completas que encontré en la web. –

1

Usaría una clase contenedora que lo mantiene todo como de solo lectura. Esto es para escalabilidad, confiabilidad y legibilidad general.

No preveo ningún otro método para hacer esto que proporcionará los tres beneficios mencionados anteriormente así como algo más. Definitivamente uso una clase contenedora aquí en mi opinión.

2

Por qué no algo como:

private int someValue; 
public int SomeValue 
{ 
    get 
    { 
     return someValue; 
    } 
    set 
    { 
     if(ReadOnly) 
       throw new InvalidOperationException("Object is readonly"); 
     someValue= value; 
    } 
+1

Porque no funcionará (como se esperaba) para tipos de referencia. Su código es equivalente a la palabra clave readonly, solo que es más oscuro. – greenoldman

+0

¿Por qué sería equivalente a la palabra clave readonly? Esto es dinámico, la palabra clave readonly no lo es. –

0

¿Podría algo como esto ayuda:

class Class1 
{ 
    private bool _isReadOnly; 

    private int _property1; 
    public int Property1 
    { 
     get 
     { 
      return _property1; 
     } 
     set 
     { 
      if (_isReadOnly) 
       throw new Exception("At the moment this is ready only property."); 
      _property1 = value; 
     } 
    } 
} 

Es necesario capturar las excepciones al establecer propiedades.

Espero que esto es algo que está buscando.

+0

Eso siempre arrojaría excepciones, nunca debes arrojar una instancia de Excepción por cierto. –

+0

@Oskar Cierto, lo cambié. – user218447

0

No se pueden obtener comprobaciones en tiempo de compilación (como las dadas con la palabra clave readonly) cambiando una propiedad a readonly en tiempo de ejecución. Entonces no hay otra manera, como verificar manualmente y lanzar una excepción.

Pero es probable que sea mejor rediseñar el acceso a la clase. Por ejemplo, cree una "clase de escritor", que verifica si la "clase de datos" subyacente puede escribirse actualmente o no.

0

Puede usar PostSharp para crear OnFieldAccessAspect que no pasará un nuevo valor a ningún campo cuando _readOnly se establecerá en verdadero. Con el código de aspecto la repetición ha desaparecido y no habrá ningún campo olvidado.

1

Si está creando una biblioteca, es posible definir una interfaz pública con una clase privada/interna. Cualquier método que necesite devolver una instancia de su clase de solo lectura a un consumidor externo debería devolver una instancia de la interfaz de solo lectura en su lugar. Ahora, el down-casting a un tipo concreto es imposible ya que el tipo no está expuesto públicamente.

biblioteca de utilidades

public interface IReadOnlyClass 
{ 
    string SomeProperty { get; } 
    int Foo(); 
} 
public interface IMutableClass 
{ 
    string SomeProperty { set; } 
    void Foo(int arg); 
} 

su biblioteca

internal MyReadOnlyClass : IReadOnlyClass, IMutableClass 
{ 
    public string SomeProperty { get; set; } 
    public int Foo() 
    { 
     return 4; // chosen by fair dice roll 
        // guaranteed to be random 
    } 
    public void Foo(int arg) 
    { 
     this.SomeProperty = arg.ToString(); 
    } 
} 
public SomeClass 
{ 
    private MyThing = new MyReadOnlyClass(); 

    public IReadOnlyClass GetThing 
    { 
     get 
     { 
      return MyThing as IReadOnlyClass; 
     } 
    } 
    public IMutableClass GetATotallyDifferentThing 
    { 
     get 
     { 
      return MyThing as IMutableClass 
     } 
    } 
} 

Ahora, cualquier persona que utilice SomeClass se pondrá en contacto lo que parece ser dos objetos diferentes. Por supuesto, ellos podrían usar la reflexión para ver los tipos subyacentes, lo que les diría que este es realmente el mismo objeto con el mismo tipo. Pero la definición de ese tipo es privada en una biblioteca externa. En este punto, aún es técnicamente posible llegar a la definición, pero se requiere Heavy Wizardry para realizarla.

Dependiendo de su proyecto, puede combinar las bibliotecas anteriores en una sola. No hay nada que lo impida; simplemente no incluya el código anterior en la DLL que desee restringir los permisos de.

Crédito para XKCD para los comentarios.

Cuestiones relacionadas