Pourquoi le mot-clé «out» est-il utilisé dans deux contextes apparemment disparates?

11

En C #, le outmot - clé peut être utilisé de deux manières différentes.

  1. Comme modificateur de paramètre dans lequel un argument est passé par référence

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
  2. En tant que modificateur de paramètre de type pour spécifier la covariance .

    // Covariant interface. 
    interface ICovariant<out R> { }
    
    // Extending covariant interface. 
    interface IExtCovariant<out R> : ICovariant<R> { }
    
    // Implementing covariant interface. 
    class Sample<R> : ICovariant<R> { }
    
    class Program
    {
        static void Test()
        {
            ICovariant<Object> iobj = new Sample<Object>();
            ICovariant<String> istr = new Sample<String>();
    
            // You can assign istr to iobj because 
            // the ICovariant interface is covariant.
            iobj = istr;
        }
    }

Ma question est: pourquoi?

Pour un débutant, la connexion entre les deux ne semble pas intuitive . L'utilisation avec des génériques ne semble avoir rien à voir avec le passage par référence.

J'ai d'abord appris ce qui outétait lié au passage d'arguments par référence, ce qui a nui à ma compréhension de l'utilisation de la définition de la covariance avec les génériques.

Existe-t-il un lien entre ces utilisations qui me manque?

Rowan Freeman
la source
5
La connexion est légèrement plus compréhensible si vous regardez l'utilisation de la covariance et de la contravariance dans System.Func<in T, out TResult>délégué .
rwong
4
De plus, la plupart des concepteurs de langues essaient de minimiser le nombre de mots-clés, et l'ajout d'un nouveau mot-clé dans une langue existante avec une grande base de code est douloureux (conflit possible avec un code existant en utilisant ce mot comme nom)
Basile Starynkevitch

Réponses:

20

Il y a une connexion, mais elle est un peu lâche. En C #, les mots clés «in» et «out» comme leur nom l'indique représentent l'entrée et la sortie. C'est très clair dans le cas des paramètres de sortie, mais moins net ce qu'il a à voir avec les paramètres du modèle.

Jetons un coup d'œil au principe de substitution de Liskov :

...

Le principe de Liskov impose des exigences standard aux signatures qui ont été adoptées dans les nouveaux langages de programmation orientés objet (généralement au niveau des classes plutôt que des types; voir le sous-typage nominal vs structurel pour la distinction):

  • Contravariance des arguments de méthode dans le sous-type.
  • Covariance des types de retour dans le sous-type.

...

Voyez comment la contravariance est associée à l'entrée et la covariance est associée à la sortie? En C # si vous marquez une variable de modèle avec outpour la rendre covariante, mais veuillez noter que vous ne pouvez le faire que si le paramètre de type mentionné n'apparaît qu'en sortie (type de retour de fonction). Donc, ce qui suit n'est pas valide:

interface I<out T>
{
  void func(T t); //Invalid variance: The type parameter 'T' must be
                  //contravariantly valid on 'I<T>.func(T)'.
                  //'T' is covariant.

}

De même, si vous marquez un paramètre de type avec in, cela signifie que vous ne pouvez l'utiliser que comme entrée (paramètre de fonction). Donc, ce qui suit n'est pas valide:

interface I<in T>
{
  T func(); //Invalid variance: The type parameter 'T' must
            //be covariantly valid on 'I<T>.func()'. 
            //'T' is contravariant.

}

Donc, pour résumer, la connexion avec le outmot-clé est qu'avec les paramètres de fonction, cela signifie qu'il s'agit d'un paramètre de sortie , et pour les paramètres de type, cela signifie que le type n'est utilisé que dans le contexte de sortie .

System.Funcest également un bon exemple de ce que rwong a mentionné dans son commentaire. Dans System.Functous les paramètres d'entrée sont flagués avec in, et le paramètre de sortie est flagué avec out. La raison est exactement ce que j'ai décrit.

Gábor Angyal
la source
2
Bonne réponse! M'a sauvé un peu… attends-le… taper! Soit dit en passant: la partie du LSP que vous avez citée était en fait connue bien avant Liskov. Ce sont juste les règles de sous-typage standard pour les types de fonctions. (Les types de paramètres sont contravariants, les types de retour sont covariants). La nouveauté de l'approche de Liskov était a) de formuler les règles non pas en termes de co- / contravariance mais en termes de substituabilité comportementale (telle que définie par les pré- / post-conditions) et b) la règle d'histoire , qui permet d'appliquer tout ce raisonnement aux types de données mutables, ce qui n'était pas possible auparavant.
Jörg W Mittag
10

@ Gábor a déjà expliqué le lien (contravariance pour tout ce qui entre ", covariance pour tout ce qui sort"), mais pourquoi réutiliser des mots clés?

Eh bien, les mots clés sont très chers. Vous ne pouvez pas les utiliser comme identifiants dans vos programmes. Mais il n'y a que tant de mots dans la langue anglaise. Ainsi, parfois vous rencontrez des conflits et vous devez renommer maladroitement vos variables, méthodes, champs, propriétés, classes, interfaces ou structures pour éviter de vous heurter à un mot clé. Par exemple, si vous modélisez une école, comment appelez-vous une classe? Vous ne pouvez pas l'appeler une classe, car classc'est un mot-clé!

Ajouter un mot-clé à une langue est encore plus cher. Il rend fondamentalement illégal tout le code qui utilise ce mot-clé comme identifiant, brisant la compatibilité descendante partout.

Les mots clés inet outexistaient déjà, ils pouvaient donc être réutilisés.

Ils auraient pu ajouter des mots clés contextuels qui ne sont que des mots clés dans le contexte d'une liste de paramètres de type, mais quels mots clés auraient-ils choisis? covariantet contravariant? +et -(comme Scala, par exemple)? superet extendscomme Java l'a fait? Pourriez-vous vous souvenir du haut de votre tête quels paramètres sont covariants et contravariants?

Avec la solution actuelle, il existe un joli mnémonique: les paramètres de type de sortie obtiennent le outmot - clé, les paramètres de type d'entrée obtiennent le inmot - clé. Notez la symétrie sympa avec les paramètres de méthode: les paramètres de sortie obtiennent le outmot - clé, les paramètres d'entrée obtiennent le inmot - clé (enfin, en fait, pas de mot-clé du tout, puisque l'entrée est la valeur par défaut, mais vous avez l'idée).

[Remarque: si vous regardez l'historique des modifications, vous verrez que j'ai en fait inversé les deux dans ma phrase d'introduction. Et j'ai même eu un vote positif pendant cette période! Cela vous montre à quel point ce mnémonique est vraiment important.]

Jörg W Mittag
la source
La façon de se souvenir de la co-versus contre-variance est de considérer ce qui se passe si une fonction dans une interface prend un paramètre d'un type d'interface générique. Si l'on a un interface Accepter<in T> { void Accept(T it);};, un Accepter<Foo<T>>acceptera Tcomme paramètre d'entrée s'il l' Foo<T>accepte comme paramètre de sortie, et vice versa. Ainsi, la contre- variance. En revanche, interface ISupplier<out T> { T get();};un Supplier<Foo<T>>aura n'importe quelle sorte de variance Foo- donc une co- variance.
supercat