2 votes

Forcer le type d'un paramètre générique à être une interface au lieu d'une classe spécifique

Compte tenu de cette interface et de cette mise en œuvre

interface IService {}
class Service : IService {}

avec une méthode générique

void Register<I>(I service)
{
    var type = typeof(I);
}

Comment faire en sorte que les lignes suivantes soient cohérentes en ce qui concerne le type générique ?

Register<IService>(new Service())    // type is 'IService'
Register(new Service());             // type is 'Service'

Deux options sont acceptables :

  • les deux lignes sont compilées en utilisant IServie type
  • la deuxième ligne ne compile pas

7voto

Jon Skeet Points 692016

Vous ne pouvez pas - il n'y a aucun moyen de contraindre un paramètre de type à être une interface.

Vous pouvez bien sûr effectuer un contrôle au moment de l'exécution, et lever une exception si I n'est pas une interface. (En passant, les conventions de nommage suggèrent que cela devrait être T pas I .)

2voto

Grant Thomas Points 28280

Vous voulez dire contraindre le type de I a IService n'est-ce pas ?

Utilisez le site where pour contraindre les types génériques :

void Register<TServiceType>(TServiceType service)
    where TServiceType : IService

Si ce que vous demandez est de restreindre le paramètre à être cualquier interface, alors, comme l'a dit Jon (mais en essayant de rendre ma réponse moins redondante), c'est illégal.

1voto

phoog Points 22667

Comme vous le notez, Register(new Service()); compile bien sûr en Register<Service>(new Service());

En supposant que vous puissiez définir une logique qui choisisse l'une des interfaces implémentées du type concret comme interface à enregistrer (ce qui est une grande supposition), vous pourriez gérer les types concrets plutôt que de les faire exclure par le compilateur. Évidemment, la solution triviale consiste à exiger que le type n'implémente qu'une seule interface, mais il est peu probable que cela soit très utile.

Je pense à quelque chose de ce genre (en reprenant la suggestion de Jon Skeet et en renommant le paramètre de type) :

void Register<T>(T service) 
{ 
    var type = typeof(T); 
    if (type.IsInterface)
    {
        Register(type);
        return;
    }

    var interfaceType = ChooseTheAppropriateInterface(type);
    Register(interfaceType);
} 

void Register(Type typeToRegister)
{
    //...
}

Type ChooseTheAppropriateInterface(Type concreteType)
{
    var interfaces = concreteType.GetInterfaces();
    //... some logic to pick and return the interface to register
}

Tout bien considéré, il est probablement plus simple et plus clair de laisser l'appelant spécifier l'interface souhaitée avec la balise Register<IService>(new Service()); appeler.

EDIT

Je suis d'accord que Register<IService>(new Service()); est la forme la plus claire. Mais comment puis-je faire en sorte que les programmeurs n'omettent pas l'élément <IService> ? Re-sharper, par exemple, peut suggérer que <IService> est redondant.

Pour répondre à cette question, considérons la sémantique de l'appel. L'appel associe un objet ( new Service() ) avec une interface ( IService ). Une façon de forcer les programmeurs à être explicites sur l'identité de l'interface est de faire du type un paramètre formel. En fait, si vous n'appelez aucune des méthodes de l'interface sur l'objet que vous enregistrez, vous n'avez même pas besoin de génériques :

void Register(Type serviceType, object service)
{
    // ... some argument validation

    if (!(serviceType.IsAssignableFrom(service.GetType())))
        throw...

    // ... register logic
}

//usage:
void InitializeServices()
{
    Register(typeof(IService), new Service());
}

Cependant, même sans appeler aucun des membres de l'interface sur l'objet, il existe un autre avantage des génériques : la vérification des types au moment de la compilation. Existe-t-il un moyen d'obtenir une vérification de type à la compilation tout en obligeant le développeur à spécifier le type explicitement ? Oui : avec deux paramètres de type dans la signature de la méthode, mais un seul argument, il n'y a aucun moyen pour le compilateur de déduire les deux types, donc le développeur doit fournir les deux. C'est plus de typage, mais le code est plus explicite.

Avec cette approche, vous pouvez également contraindre le type d'implémentation au type d'interface, afin de vous assurer que le développeur n'appelle pas, par exemple, le type d'interface, Register<IValidationService, SecurityService>(new SecurityService()); :

interface IServiceBase { } // interface that all service interfaces must implement; might not be needed
interface IService : IServiceBase { }
class Service : IService : { }

void Register<TServiceType, TImplementingObject>(TImplementingObject service)
    where TServiceType : IServiceBase  // superfluous if there's no IServiceBase, of course
    where TImplementingObject : TServiceType
{
    // ... implementation
}

//usage:
void InitializeServices()
{
    Register<IService, Service>(new Service());
}

ou même

class NewImprovedService : Service : { }

void InitializeServices()
{
    Register<IService, Service>(new NewImprovedService());
}

Cela nous ramène à la question des indications de types éventuellement redondants, et l'appel est encore plus verbeux que ce que vous avez commencé, mais cela empêche le développeur d'enregistrer par inadvertance le mauvais type de service.

Nous restons cependant confrontés au problème initial, car rien n'empêche le développeur d'appeler

Register<Service, NewImprovedService>(new NewImprovedService());

Cela n'échouera pas tant qu'une vérification de l'exécution pour typeof(TServiceType).IsInterface .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X