2010-08-03 28 views
51

Introduccióndefinida por el usuario operador de conversión de clase base

Soy consciente de que "conversiones definidas por el usuario hacia o desde una clase base no se les permite". MSDN da, como explicación a esta regla, "No necesita este operador".

Entiendo que una conversión definida por el usuario a no es necesaria una clase base, ya que esto obviamente se hace implícitamente. Sin embargo, sí necesito una conversión de una clase base.

En mi diseño actual, un contenedor de código no administrado, uso un puntero, almacenado en una clase Entity. Todas las clases que usan un puntero derivan de esa clase Entity, por ejemplo, una clase Body.

por lo tanto tengo:

Método A

class Entity 
{ 
    IntPtr Pointer; 

    Entity(IntPtr pointer) 
    { 
     this.Pointer = pointer; 
    } 
} 

class Body : Entity 
{ 
    Body(IntPtr pointer) : base(pointer) { } 

    explicit operator Body(Entity e) 
    { 
     return new Body(e.Pointer); 
    } 
} 

Este reparto es la ilegal. (Tenga en cuenta que no me molesté en escribir los accesos). Sin él, el compilador se me permite hacer:

Método B

(Body)myEntity 
... 

Sin embargo, en tiempo de ejecución, voy a tener una excepción diciendo que este reparto es imposible.

Conclusión

Por lo tanto, aquí estoy, que necesitan una conversión definida por el usuario de una clase base, y C# se niega a mí. Usando el método A, el compilador se quejará pero el código lógicamente funcionará en tiempo de ejecución. Usando el método B, el compilador no se quejará pero el código obviamente fallará en el tiempo de ejecución.

Lo que me parece extraño en esta situación es que MSDN me dice que no necesito este operador, y el compilador actúa como si fuera posible implícitamente (método B). ¿Que se supone que haga?

Soy consciente de que puedo usar:

Solución A

class Body : Entity 
{ 
    Body(IntPtr pointer) : base(pointer) { } 

    static Body FromEntity(Entity e) 
    { 
     return new Body(e.Pointer); 
    } 
} 

Solución B

class Body : Entity 
{ 
    Body(IntPtr pointer) : base(pointer) { } 

    Body(Entity e) : base(e.Pointer) { } 
} 

Solución C

class Entity 
{ 
    IntPtr Pointer; 

    Entity(IntPtr pointer) 
    { 
     this.Pointer = pointer; 
    } 

    Body ToBody() 
    { 
     return new Body(this.Pointer); 
    } 
} 

Pero, sinceramente, todas las sintaxis para estos son horribles y de hecho deberían ser versiones. Entonces, ¿hay alguna manera de hacer que los moldes funcionen? ¿Es un defecto de diseño de C# o perdí una posibilidad? Es como si C# no confiara lo suficiente en mí como para escribir mi propia conversión de base a hijo usando su sistema de conversión.

+2

¿Alguien más piensa que el operador de reparto hace demasiadas cosas? Upcasting, downcasting, boxing/unboxing, conversiones definidas por el usuario ... C++ dividió su operador de reparto hace años; tal vez es hora de que C# haga lo mismo? –

Respuesta

33

No es un defecto de diseño. He aquí por qué:

Entity entity = new Body(); 
Body body = (Body) entity; 

si se dejen de escribir su propia conversión definida por el usuario aquí, no habría dos conversiones válidas: un intento de simplemente hacer un reparto normal (que es una conversión de referencia, la preservación de la identidad) y su conversión definida por el usuario.

¿Cuál debe usarse? ¿Os gustaría realmente querer para que estos hagan cosas diferentes?

// Reference conversion: preserves identity 
Object entity = new Body(); 
Body body = (Body) entity; 

// User-defined conversion: creates new instance 
Entity entity = new Body(); 
Body body = (Body) entity; 

Yuk! De esa manera miente la locura, IMO. No olvide que el compilador decide esto al en tiempo de compilación, basado solo en en tiempo de compilación tipos de las expresiones implicadas.

Personalmente, me gustaría ir con la solución C, y posiblemente incluso convertirla en un método virtual. De esa manera, Bodypodría anularlo para que simplemente devuelva this, si desea que sea identidad preservando donde sea posible pero creando un nuevo objeto donde sea necesario.

+1

No entendiste mi intención - No quiero lanzar Body to Entity, eso sería horrible. Quiero incluir Entidad al cuerpo. La cuestión es que no puedo saber qué clase es porque solo estoy envolviendo un puntero no administrado. – Lazlo

+0

@Lazlo: Lo siento, * entendí * tu intención, me equivoqué con los ejemplos del código. (Los tipos de variables eran correctos, eran solo los moldes los que estaban equivocados. Los he solucionado ahora.) Sin embargo, el razonamiento detrás de mi publicación sigue siendo el mismo. Desea una conversión personalizada donde ya hay una conversión explícita incorporada. La forma * más clara * de expresar ese deseo es con un método. –

+0

(La llamada al constructor también sería aceptable, pero en algunos casos eso puede conducir a un código menos fluido.) –

9

Debería usar su Solución B (el argumento constructor); En primer lugar, he aquí por qué no de usar las otras soluciones propuestas:

  • solución A es simplemente un contenedor para la Solución B;
  • solución C es simplemente incorrecto (¿por qué una clase base sabe cómo convertir en sí a cualquier subclase?)

Además, si la clase Body eran que contienen propiedades adicionales, lo que debería éstos se inicializa en cuando realiza tu lanzamiento? Es mucho mejor usar el constructor e inicializar las propiedades de la subclase, como es convencional en los lenguajes OO.

+0

+1 para el último argumento. Lo consideraré. Pero no hay forma de hacerlo como un reparto apropiado? – Lazlo

+4

No tengo ningún problema con una clase base que sepa cómo convertirse en una subclase particular ... especialmente de forma virtual. Witness Object.ToString y los métodos de extensión IEnumerable.ToList, IEnumerable.ToArray. –

15

Bueno, cuando se está lanzando Entity a Body, usted no está realmente fundición de uno a otro, sino que más bien la fundición IntPtr a una nueva entidad.

¿Por qué no crear un operador de conversión explícito de IntPtr?

public class Entity { 
    public IntPtr Pointer; 

    public Entity(IntPtr pointer) { 
     this.Pointer = pointer; 
    } 
} 

public class Body : Entity { 
    Body(IntPtr pointer) : base(pointer) { } 

    public static explicit operator Body(IntPtr ptr) { 
     return new Body(ptr); 
    } 

    public static void Test() { 
     Entity e = new Entity(new IntPtr()); 
     Body body = (Body)e.Pointer; 
    } 
} 
+0

La mejor alternativa hasta el momento, la elegirá si la recompensa no arroja ningún otro resultado valioso. – Lazlo

2

La razón por la que no puede hacerlo es porque no es seguro en el caso general. Considera las posibilidades. Si desea poder hacer esto porque la clase base y derivada son intercambiables, entonces realmente solo tiene una clase y debería fusionar ambas. Si desea tener su operador de elenco para la conveniencia de poder bajar la base a derivado, entonces debe considerar que no todas las variables tipadas como clase base apuntarán a una instancia de la clase derivada específica que está tratando de convertir a. Es podría ser así, pero tendría que comprobar primero, o arriesgarse a una excepción de conversión no válida. Es por eso que el downcasting generalmente es mal visto y esto no es más que downcasting in drag. Te sugiero que reconsideres tu diseño.

+0

En mi contexto de ajuste actual, el molde debería ser posible. Entiendo que en cualquier otro caso, donde no se ajusta un objeto definido como un puntero, no se recomienda el downcasting. – Lazlo

+0

Todavía no puedo pensar en una razón por la que no solo combinarías las clases si realmente son iguales. Pero si insistes, diría que la mejor opción es tener una función explícita GetEntityFromBody() o similar que devuelva un objeto Entity dado un objeto Body. Eso deja en claro que estás haciendo algo fuera de lo común, al mismo tiempo que te permite hacerlo. Casting no debe ser abusado. – siride

1

¿Qué tal:

public class Entity {...} 

public class Body : Entity 
{ 
    public Body(Entity sourceEntity) { this.Pointer = sourceEntity.Pointer; } 
} 

por lo que en código que no tiene que escribir:

Body someBody = new Body(previouslyUnknownEntity.Pointer); 

pero se puede utilizar

Body someBody = new Body(previouslyUnknownEntity); 

lugar.

Es solo un cambio cosmético , lo sé, pero es bastante claro y puede cambiar las partes internas fácilmente. También se usa en un patrón de envoltura del que no recuerdo un nombre (con fines ligeramente diferentes).
También está claro que está creando una entidad nueva a partir de la proporcionada, por lo que no debe ser confusa como lo sería un operador/conversión.

Nota: no se ha utilizado un compilador por lo que la posibilidad de un error tipográfico está ahí.

1

(Invocando protocolos nigromancia ...)

Aquí es mi caso de uso:

class ParseResult 
{ 
    public static ParseResult Error(string message); 
    public static ParseResult<T> Parsed<T>(T value); 

    public bool IsError { get; } 
    public string ErrorMessage { get; } 
    public IEnumerable<string> WarningMessages { get; } 

    public void AddWarning(string message); 
} 

class ParseResult<T> : ParseResult 
{ 
    public static implicit operator ParseResult<T>(ParseResult result); // Fails 
    public T Value { get; } 
} 

... 

ParseResult<SomeBigLongTypeName> ParseSomeBigLongTypeName() 
{ 
    if (SomethingIsBad) 
     return ParseResult.Error("something is bad"); 
    return ParseResult.Parsed(new SomeBigLongTypeName()); 
} 

Aquí Parsed() puede inferir T desde su parámetro, pero Error no puede, pero puede devolver un typeless ParseResult que es convertible a ParseResult<T> - o lo sería si no fuera por este error. La solución es devolver y convertir desde un subtipo:

class ParseResult 
{ 
    public static ErrorParseResult Error(string message); 
    ... 
} 

class ErrorParseResult : ParseResult {} 

class ParseResult<T> 
{ 
    public static implicit operator ParseResult<T>(ErrorParseResult result); 
    ... 
} 

¡Y todo está contento!

0

parece que la igualdad de referencia no era su preocupación, entonces se puede decir:

  • Código

    public class Entity { 
        public sealed class To<U> where U : Entity { 
         public static implicit operator To<U>(Entity entity) { 
          return new To<U> { m_handle=entity.Pointer }; 
         } 
    
         public static implicit operator U(To<U> x) { 
          return (U)Activator.CreateInstance(typeof(U), x.m_handle); 
         } 
    
         To() { // not exposed 
         } 
    
         IntPtr m_handle; // not exposed 
        } 
    
        IntPtr Pointer; // not exposed 
    
        public Entity(IntPtr pointer) { 
         this.Pointer=pointer; 
        } 
    } 
    

    public class Body:Entity { 
        public Body(IntPtr pointer) : base(pointer) { 
        } 
    } 
    
    // added for the extra demonstration 
    public class Context:Body { 
        public Context(IntPtr pointer) : base(pointer) { 
        } 
    } 
    

y la

  • prueba

    public static class TestClass { 
        public static void TestMethod() { 
         Entity entity = new Entity((IntPtr)0x1234); 
         Body body = (Entity.To<Body>)entity; 
         Context context = (Body.To<Context>)body; 
        } 
    } 
    

No escribiste los descriptores de acceso pero me tomó la encapsulación en cuenta, para no exponer a sus punteros. Bajo el capó de esta implementación es utilizar una clase intermedia que no está en la cadena de herencia pero encadena la conversión.

Activator implicado aquí es bueno para no añadir extra de restricción new()U como ya están limitadas a Entity y tener un constructor con parámetros. To<U> aunque está expuesto pero sellado sin exponer su constructor, solo puede ser instanciado desde el operador de conversión.

En el código de prueba, la entidad realmente se convirtió en un objeto genérico To<U> y luego el tipo de destino, por lo que es la demostración adicional de body a context. Como To<U> es una clase anidada, puede acceder al Pointer privado de la clase contenedora, por lo que podemos lograr cosas sin exponer el puntero.

Bueno, eso es todo.

Cuestiones relacionadas