11 Stimmen

C#: Cast auf generische Schnittstelle mit Basistyp

Hier ist der Code:

public interface IValidator<T>
{
   bool IsValid(T obj);
}

public class OrderValidator: IValidator<Order>
{
  // ...
}

public class BaseEntity
{
}

public class Order: BaseEntity
{
}

Das Problem ist, dass ich das nicht kann:

var validator = new OrderValidator();
// this line throws because type can't be converted
var baseValidator = (IValidator<BaseEntity>)validator;
// all this is because I need a list with IValidator<Order>, IValidator<BaseOrder>, etc.
IList<IValidator<BaseEntity>> allValidators = ... 

Wie erhalte und speichere ich eine Liste aller Implementierungen von IValidator<T> für die Basis T - sagen wir, BaseEntity? Derzeit mache ich nicht-generische IValidator, die "Objekt obj" akzeptiert, aber es ist nicht gut und nicht typsicher.

Das Lustige daran ist, dass C# ermöglicht zu kompilieren:

var test = (IValidator<BaseEntity>)new OrderValidator();

scheitert aber zur Laufzeit mit

   Unable to cast object of type 'OrderValidator' to type 'IValidator`1[Orders.Core.BaseEntity]'

Dies ist die gleiche Ausnahme, die Windsor gibt mir (ich habe versucht, sowohl Windsor und manuelle Typen Lookup, dieses Problem ist wirklich nicht im Zusammenhang mit diesem, nur auf die Schnittstellen Casting).

Dank Heinzi weiß ich jetzt, warum ich nicht casten kann - denn IValidator for Order erwartet Order als generischen Typ. Aber wie kann ich eine Liste von IValidator für verschiedene Typen zurückgeben? Der Grund ist, dass die BaseEntity ihren echten Typ nimmt und alle Validatoren sammelt - für alle Typen von GetType() zum Objekt. Ich würde wirklich gerne einen generischen GetValidators() haben und dann darauf operieren.

10voto

Heinzi Punkte 157917

Vielleicht hilft es Ihnen, wenn ich Ihnen erkläre, warum dieser Wurf verboten ist: Nehmen wir an, Sie haben die folgende Funktion

void myFunc(IValidator<BaseEntity> myValidator) {
    myValidator.IsValid(new BaseEntity());
}

Dieser Code würde korrekt kompiliert. Dennoch, wenn Sie eine OrderValidator zu dieser Funktion, würden Sie eine Laufzeitausnahme erhalten, weil OrderValidator.IsValid erwartet einen Auftrag, keine BaseEntity. Die Typsicherheit wäre nicht mehr gegeben, wenn Ihr Cast erlaubt wäre.

EDIT: C# 4 ermöglicht die generische Kovarianz und Kontravarianz aber das würde in Ihrem Fall nicht helfen, da Sie T als Eingabe Parameter. Daher kann nur das Casting auf einen IValidator<SomeSubtypeOfOrder> auf typsichere Weise durchgeführt werden kann.

Um das klarzustellen, Sie können also nicht OrderValidator a IValidator<BaseEntity> denn Ihr OrderValidator kann nur Bestellungen validieren, nicht alle Arten von BaseEntities. Dies ist jedoch das, was man von einem IValidator<BaseEntity> .

4voto

Simon P Stevens Punkte 26735

Die Besetzung funktioniert nicht, weil IValidator<Order> y IValidator<BaseEntity> sind völlig unverbundene Typen. IValidator<Order> ist kein Subtyp von IValidator<BaseEntity> Sie können also nicht geworfen werden.

C# unterstützt die Vererbung mehrerer Schnittstellen, so dass der einfachste Weg, dies zu handhaben ist, um Ihre Bestellung Validator erben von einer Schnittstelle für beide Validator-Typen, so dass es Sie in der Lage sein, es auf jede Schnittstelle wie erforderlich zu casten. Natürlich bedeutet dies, dass Sie beide Schnittstellen implementieren und angeben müssen, wie die Basis zu behandeln ist, wenn ein BaseEntity nicht mit dem Typ übereinstimmt, für den der Validator vorgesehen ist.

Etwa so:

public class OrderValidator : IValidator<Order>, IValidator<BaseEntity>
{
    public bool IsValid(Order obj)
    {
        // do validation
        // ...
        return true;
    }

    public bool IsValid(BaseEntity obj)
    {
        Order orderToValidate = obj as Order;
        if (orderToValidate != null)
        {
            return IsValid(orderToValidate);
        }
        else
        {
            // Eiter do this:
            throw new InvalidOperationException("This is an order validator so you can't validate objects that aren't orders");
            // Or this:
            return false;
            // Depending on what it is you are trying to achive.
        }
    }
}

Dies bezieht sich auf das, was Heinzi nicht werfen können, weil ein IValidator<BaseEntity> muss in der Lage sein zu validieren BaseEntities die Ihre aktuelle OrderValidator nicht tun kann. Durch Hinzufügen dieser mehrfachen Schnittstelle definieren Sie explizit das Verhalten für die Validierung BaseEntities (entweder durch explizites Ignorieren oder Auslösen einer Ausnahme), so dass der Wurf möglich wird.

2voto

Chris Marisic Punkte 31583

Das ist zwar keine direkte Antwort, aber ich würde empfehlen, einen Blick in den Quellcode von StructureMap zu werfen, denn dort wird viel mit offenen generischen Typen gearbeitet. Eigentlich könnte sogar wollen StructureMap verwenden, um Caching Ihrer Validatoren zu behandeln, das ist genau das, was ich tun.

ForRequestedType(typeof (ValidationBase<>)).CacheBy(InstanceScope.Singleton);
Scan(assemblies =>
    {
        assemblies.TheCallingAssembly();
        assemblies.AddAllTypesOf(typeof(IValidation<>));
    });

Dann habe ich eine Fabrikklasse, die die eigentliche Validierung durchführt

public static class ValidationFactory
{
    public static Result Validate<T>(T obj)
    {
        try
        {
            var validator = ObjectFactory.GetInstance<IValidator<T>>();
            return validator.Validate(obj);
        }
        catch (Exception ex)
        {
            ...
        }
    }
}

編集する。 Ich habe einen großen Blog-Beitrag über generische Validierung mit IoC geschrieben, wenn Sie einen Blick darauf werfen, da Sie sagten, Sie verwenden bereits Spring, ich wette, Sie könnten meine Arbeit anpassen, um Ihr Problem zu lösen: Schaffung eines generischen Validierungsrahmens

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X