web-dev-qa-db-fra.com

Pourquoi ReSharper suggère-t-il de rendre le paramètre de type T contravariant?

ReSharper me propose de rendre le paramètre de type T contravariant en changeant ceci:

interface IBusinessValidator<T> where T: IEntity
{
    void Validate(T entity);
}

En cela:

interface IBusinessValidator<in T> where T: IEntity
{
    void Validate(T entity);
}

Alors, qu'est-ce qui est différent entre <T> et <in T>? Et quel est le but de contravariant ici?

Supposons que j'ai IEntity, Entity, User et Account entités. En supposant que User et Account possèdent à la fois la propriété Name qui doit être validée.

Comment puis-je appliquer l'utilisation de contravariant dans cet exemple?

38
Vu Nguyen

Alors, qu'est-ce qui est différent entre <T> et <in T>?

La différence est que in T vous permet de passer un type plus générique (moins dérivé) que ce qui a été spécifié.

Et quel est le but de contravariant ici?

ReSharper suggère d'utiliser la contravariance ici car il voit que vous passez le paramètre T dans la méthode Validate et souhaite vous permettre d'élargir le type d'entrée en le rendant moins générique.

En général, la contravariance est expliquée en détail dans Contravariance expliqué et dans Exemple du monde réel de covariance et contravariance , et bien sûr dans toute la documentation sur MSDN (il y a grand FAQ par l'équipe C # ).

Il y a un bel exemple via MSDN:

abstract class Shape
{
    public virtual double Area { get { return 0; }}
}

class Circle : Shape
{
    private double r;
    public Circle(double radius) { r = radius; }
    public double Radius { get { return r; }}
    public override double Area { get { return Math.PI * r * r; }}
}

class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
    int IComparer<Shape>.Compare(Shape a, Shape b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.Area.CompareTo(b.Area);
    }
}

class Program
{
    static void Main()
    {
        // You can pass ShapeAreaComparer, which implements IComparer<Shape>, 
        // even though the constructor for SortedSet<Circle> expects  
        // IComparer<Circle>, because type parameter T of IComparer<T> is 
        // contravariant.
        SortedSet<Circle> circlesByArea = 
            new SortedSet<Circle>(new ShapeAreaComparer()) 
                { new Circle(7.2), new Circle(100), null, new Circle(.01) };

        foreach (Circle c in circlesByArea)
        {
            Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area);
        }
    }
}

Comment puis-je appliquer l'utilisation de contravariant dans cet exemple?

Disons que nous avons nos entités:

public class Entity : IEntity
{
    public string Name { get; set; }
}

public class User : Entity
{
    public string Password { get; set; }
}

Nous avons également une interface IBusinessManager et une implémentation BusinessManager, qui accepte un IBusinessValidator:

public interface IBusinessManager<T>
{
    void ManagerStuff(T entityToManage);
}

public class BusinessManager<T> : IBusinessManager<T> where T : IEntity
{
    private readonly IBusinessValidator<T> validator;
    public BusinessManager(IBusinessValidator<T> validator)
    {
        this.validator = validator;
    }

    public void ManagerStuff(T entityToManage)
    {
        // stuff.
    }
}

Supposons maintenant que nous ayons créé un validateur générique pour tout IEntity:

public class BusinessValidator<T> : IBusinessValidator<T> where T : IEntity
{
    public void Validate(T entity)
    {
        if (string.IsNullOrWhiteSpace(entity.Name))
            throw new ArgumentNullException(entity.Name);
    }
}

Et maintenant, nous voulons passer BusinessManager<User> et IBusinessValidator<T>. Parce que c'est contravariant , je peux le passer BusinessValidator<Entity>.

Si nous supprimons le mot clé in, nous obtenons l'erreur suivante:

Not contravariant

Si nous l'incluons, cela compile très bien.

22
Yuval Itzchakov

Pour comprendre la motivation de ReSharper, considérez gobbler d'âne de Marcelo Cantos :

// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

Si Marcelo avait oublié d'utiliser le mot-clé in dans la déclaration de son interface IGobbler, alors le système de type de C # ne reconnaîtrait pas son QuadrupedGobbler comme un gobbler âne, et donc ceci l'affectation à partir du code ci-dessus ne parviendrait pas à compiler:

IGobbler<Donkey> dg = new QuadrupedGobbler();

Notez que cela n'empêcherait pas le QuadrupedGobbler de gober les ânes - par exemple, le code suivant fonctionnerait :

IGobbler<Quadruped> qg = new QuadrupedGobbler();
qg.gobble(MyDonkey());

Cependant, vous ne pourriez pas affecter un QuadrupedGobbler à une variable de type IGobbler<Donkey> ou passez-le à une méthode IGobbler<Donkey> paramètre. Ce serait bizarre et incohérent; si le QuadrupedGobbler peut engloutir les ânes, cela n'en fait-il pas une sorte de gobeur d'âne? Heureusement, ReSharper remarque cette incohérence, et si vous omettez le in dans la déclaration IGobbler, il vous suggérera de l'ajouter - avec la suggestion " Rendre le paramètre de type T contravariant " - permettant d'utiliser un QuadrupedGobbler comme IGobbler<Donkey>.

En général, la même logique décrite ci-dessus s'applique dans tous les cas où une déclaration d'interface contient un paramètre générique qui n'est utilisé que comme type de paramètres de méthode , pas types de retour.

3
Mark Amery