web-dev-qa-db-fra.com

Opérateur de conversion défini par l'utilisateur de la classe de base

Introduction

Je suis conscient que "les conversions définies par l'utilisateur vers ou à partir d'une classe de base ne sont pas autorisées". MSDN donne comme explication à cette règle "Vous n'avez pas besoin de cet opérateur".

Je comprends qu’une conversion définie par l’utilisateur to une classe de base n’est pas nécessaire, comme cela a été fait implicitement. Cependant, j'ai besoin d'une conversion de une classe de base.

Dans ma conception actuelle, un wrapper de code non managé, j'utilise un pointeur, stocké dans une classe d'entité . Toutes les classes utilisant un pointeur dérivent de cette classe d'entité, par exemple une classe Body.

J'ai donc:

Méthode 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);
    }
}

Ce casting est l'illégal. (Notez que je n'ai pas pris la peine d'écrire les accesseurs) . Sans lui, le compilateur will me permettra de faire:

Méthode B

(Body)myEntity
...

Cependant, au moment de l'exécution, je vais avoir une exception disant que cette distribution est impossible.

Conclusion

Je suis donc ici, j'ai besoin d'une conversion définie par l'utilisateur de une classe de base, et C # me la refuse. En utilisant la méthode A, le compilateur se plaindra mais le code fonctionnerait logiquement au moment de l'exécution. En utilisant la méthode B, le compilateur ne se plaindra pas mais le code échouera évidemment au moment de l'exécution.

Ce que je trouve étrange dans cette situation est que MSDN me dit que je n’ai pas besoin de cet opérateur et que le compilateur agit comme si c’était possible implicitement (méthode B). Qu'est-ce que je suis supposé faire?

Je suis conscient que je peux utiliser:

Solution A

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

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

Solution B

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

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

Solution C

class Entity
{
    IntPtr Pointer;

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

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

Mais honnêtement, toutes les syntaxes pour celles-ci sont horribles et devraient en fait être des moulages… Donc, quel moyen de faire fonctionner les moulages? Est-ce un défaut de conception C # ou ai-je raté une possibilité? C'est comme si C # ne me faisait pas suffisamment confiance pour écrire ma propre conversion de base en enfant à l'aide de leur système de diffusion.

61
Lazlo

Ce n'est pas un défaut de conception. Voici pourquoi:

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

Si vous étiez autorisé à écrire ici votre propre conversion définie par l'utilisateur, il y aurait deux} conversions valides: une tentative de conversion normale (conversion de référence préservant l'identité) et vos conversions définies par l'utilisateur. conversion.

Lequel devrait être utilisé? Souhaitez-vous vraiment vouloir que ces derniers fassent des choses différentes?

// 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 cette façon, la folie se trouve, IMO. N'oubliez pas que le compilateur décide cela à compile-time, basé uniquement sur les types compile-time des expressions impliquées.

Personnellement, j'irais avec la solution C - et éventuellement même en faire une méthode virtuelle. De cette façon, Bodypourrait le remplacer pour ne renvoyer que this, si vous voulez que ce soit en préservant l'identité si possible mais en créant un nouvel objet si nécessaire.

37
Jon Skeet

Eh bien, lorsque vous transposez Entity en Body, vous ne transformez pas {vraiment} _ _ l'un en face de l'autre, mais vous transmettez plutôt la IntPtr à une nouvelle entité.

Pourquoi ne pas créer un opérateur de conversion explicite à partir 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;
    }
}
17
Igor Zevaka

Vous devriez utiliser votre solution B (l'argument constructeur); Tout d'abord, voici pourquoi pas pour utiliser les autres solutions proposées:

  • La solution A est simplement une enveloppe pour la solution B;
  • La solution C est tout simplement fausse (pourquoi une classe de base devrait-elle savoir comment se convertir en une sous-classe?)

De plus, si la classe Body devait contenir des propriétés supplémentaires, à quoi ces propriétés devraient-elles être initialisées lorsque vous effectuez votre transtypage? Il est de loin préférable d'utiliser le constructeur et d'initialiser les propriétés de la sous-classe, comme le font les conventions dans les langages OO.

9
robyaw

La raison pour laquelle vous ne pouvez pas le faire est que ce n'est pas sûr dans le cas général. Considérez les possibilités. Si vous voulez être capable de faire cela parce que la classe de base et la classe dérivée sont interchangeables, alors vous n’avez vraiment qu’une classe et vous devez fusionner les deux. Si vous souhaitez que votre opérateur de distribution puisse convertir la base dérivée en base dérivée, vous devez tenir compte du fait que toutes les variables tapées comme classe de base ne renverront pas à une instance de la classe dérivée spécifique que vous essayez de convertir. à. C'est pourrait être de cette façon, mais vous devrez d'abord vérifier, ou risquer une exception de distribution invalide. C’est la raison pour laquelle l’abaissement est généralement mal vu et il ne s’agit que de l’abaissement en traînée. Je vous suggère de repenser votre conception.

2
siride

(Invoquer des protocoles de nécromancie ...)

Voici mon cas d'utilisation:

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());
}

Ici, Parsed() peut déduire T à partir de son paramètre, mais Error ne le peut pas, mais il peut renvoyer une ParseResult sans type convertible en ParseResult<T> - ou le serait sinon pour cette erreur. Le correctif consiste à retourner et à convertir à partir d'un sous-type:

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

class ErrorParseResult : ParseResult {}

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

Et tout est content!

1
Simon Buchan

Que diriez-vous:

public class Entity {...}

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

donc dans le code, vous n'avez pas à écrire:

Body someBody = new Body(previouslyUnknownEntity.Pointer);

mais vous pouvez utiliser

Body someBody = new Body(previouslyUnknownEntity);

au lieu.

C'est juste un changement cosmétique , je sais, mais c'est assez clair et vous pouvez changer les internes facilement. Il est également utilisé dans un motif wrapper dont je ne me souviens pas du nom (à des fins légèrement différentes).
Il est également clair que vous créez une nouvelle entité à partir de celle fournie, vous ne devriez donc pas être déroutant comme le serait un opérateur/conversion.

Remarque: n'ayant pas utilisé de compilateur, la possibilité d'une faute de frappe existe.

1
Jaroslav Jandek

Il semble que l'égalité de référence ne vous concerne pas, alors vous pouvez dire: 

  • Code 

    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) {
        }
    }
    

et le

  • Tester

    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;
        }
    }
    

Vous n'avez pas écrit les accesseurs, mais j'ai pris en compte l'encapsulation pour ne pas exposer leurs pointeurs. Sous le capot de cette implémentation, on utilise une classe intermédiaire qui n'est pas dans la chaîne d'héritage mais la chaîne de conversion

Activator impliqué ici est utile pour ne pas ajouter de contrainte new() supplémentaire car U est déjà contraint à Entity et possède un constructeur paramétré. To<U> est exposé mais scellé sans exposer son constructeur, il ne peut être instancié que par l'opérateur de conversion. 

Dans le code de test, l'entité est effectivement convertie en un objet générique To<U>, puis en type de cible. Il en va de même pour la démonstration supplémentaire de body à context. Étant donné que To<U> est une classe imbriquée, il peut accéder à la variable privée Pointer de la classe contenante. Ainsi, nous pouvons accomplir certaines tâches sans exposer le pointeur. 

Eh bien c'est ça. 

0
Ken Kin

vous pouvez utiliser générique, c'est possible comme un coup

public class a<based>
    {
        public static implicit operator b(a<based> v)
        {
            return new b();
        }
    }

    public class b
        : a<b>
    {
    }
0
Milad Sadeghi