web-dev-qa-db-fra.com

idée de correspondance interrupteur / motif

J'ai regardé F # récemment, et même si je ne suis pas susceptible de sauter la clôture de sitôt, il met définitivement en évidence certains domaines où C # (ou le support de bibliothèque) pourrait faciliter la vie.

En particulier, je pense à la capacité de correspondance de motifs de F #, qui permet une syntaxe très riche - beaucoup plus expressive que le commutateur actuel/équivalents C # conditionnels. Je n'essaierai pas de donner un exemple direct (mon F # n'est pas à la hauteur), mais en bref cela permet:

  • correspondance par type (avec vérification de la couverture complète pour les unions discriminées) [notez que cela déduit également le type de la variable liée, donnant l'accès aux membres, etc.]
  • correspondance par prédicat
  • combinaisons des éléments ci-dessus (et éventuellement d'autres scénarios dont je ne suis pas au courant)

Bien qu'il soit intéressant que C # emprunte finalement [ahem] une partie de cette richesse, entre-temps, j'ai regardé ce qui peut être fait au moment de l'exécution - par exemple, il est assez facile de rassembler certains objets pour permettre:

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

où getRentPrice est un Func <Vehicle, int>.

[note - peut-être Switch/Case voici les mauvais termes ... mais cela montre l'idée]

Pour moi, c'est beaucoup plus clair que l'équivalent en utilisant répété if/else, ou un conditionnel ternaire composite (qui devient très compliqué pour les expressions non triviales - des crochets à gogo). Il évite également un lot de casting et permet une extension simple (directement ou via des méthodes d'extension) à des correspondances plus spécifiques, par exemple un InRange ( ...) correspond à l'utilisation de VB Select ... Case "x To y".

J'essaie juste de déterminer si les gens pensent qu'il y a beaucoup d'avantages de constructions comme celles ci-dessus (en l'absence de prise en charge linguistique)?

Notez en outre que j'ai joué avec 3 variantes de ce qui précède:

  • une version Func <TSource, TValue> pour évaluation - comparable aux instructions conditionnelles ternaires composites
  • une version Action <TSource> - comparable à if/else if/else if/else if/else
  • une version Expression <Func <TSource, TValue >> - comme la première, mais utilisable par des fournisseurs LINQ arbitraires

En outre, l'utilisation de la version basée sur l'expression permet la réécriture de l'arborescence d'expression, en insérant essentiellement toutes les branches dans une seule expression conditionnelle composite, plutôt que d'utiliser une invocation répétée. Je n'ai pas vérifié récemment, mais dans certaines premières versions d'Entity Framework, je semble me rappeler que cela était nécessaire, car il n'aimait pas beaucoup InvocationExpression. Il permet également une utilisation plus efficace avec LINQ-to-Objects, car il évite les appels de délégués répétés - les tests montrent une correspondance comme celle ci-dessus (en utilisant le formulaire d'expression) fonctionnant à la même vitesse [légèrement plus rapide, en fait] par rapport au C # équivalent instruction conditionnelle composite. Pour être complet, la version basée sur Func <...> a pris 4 fois plus de temps que l'instruction conditionnelle C #, mais elle est toujours très rapide et ne constituera probablement pas un goulot d'étranglement majeur dans la plupart des cas d'utilisation.

Je me réjouis de toute pensée/entrée/critique/etc sur ce qui précède (ou sur les possibilités de prise en charge du langage C # plus riche ... voici en espérant ;-p).

148
Marc Gravell

Je sais que c'est un vieux sujet, mais en c # 7 vous pouvez faire:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}
22
Marcus Pierce

Bart De Smet's excellent blog a une série de 8 parties sur comment faire exactement ce que vous décrivez. Trouvez la première partie ici .

82
mancaus

Après avoir essayé de faire de telles choses "fonctionnelles" en C # (et même avoir essayé un livre dessus), je suis arrivé à la conclusion que non, à quelques exceptions près, de telles choses n'aident pas trop.

La raison principale est que les langages tels que F # tirent une grande partie de leur puissance de la prise en charge réelle de ces fonctionnalités. Pas "vous pouvez le faire", mais "c'est simple, c'est clair, c'est attendu".

Par exemple, dans la correspondance de modèles, le compilateur vous indique s'il existe une correspondance incomplète ou lorsqu'une autre correspondance ne sera jamais atteinte. Ceci est moins utile avec les types ouverts, mais lors de la correspondance d'une union ou de tuples discriminés, c'est très astucieux. En F #, vous vous attendez à ce que les gens correspondent, et cela a immédiatement du sens.

Le "problème" est qu'une fois que vous commencez à utiliser certains concepts fonctionnels, il est naturel de vouloir continuer. Cependant, tirer parti des tuples, des fonctions, de l'application et du currying de méthodes partielles, de la mise en correspondance de modèles, des fonctions imbriquées, des génériques, de la prise en charge de la monade, etc. en C # devient très moche, très rapidement. C'est amusant, et certaines personnes très intelligentes ont fait des choses très cool en C #, mais en fait en utilisant , cela semble lourd.

Ce que j'ai fini par utiliser souvent (entre projets) en C #:

  • Fonctions de séquence, via des méthodes d'extension pour IEnumerable. Des choses comme ForEach ou Process ("Appliquer"? - faire une action sur un élément de séquence tel qu'il est énuméré) s'intègrent parce que la syntaxe C # le prend bien en charge.
  • Résumé des modèles de déclaration communs. Blocs try/catch/finally compliqués ou autres blocs de code impliqués (souvent très génériques). L'extension de LINQ-to-SQL s'inscrit ici aussi.
  • Tuples, dans une certaine mesure.

** Mais notez: le manque de généralisation automatique et d'inférence de type gêne vraiment l'utilisation même de ces fonctionnalités. **

Tout cela dit, comme quelqu'un l'a mentionné, dans une petite équipe, dans un but précis, oui, peut-être qu'ils peuvent vous aider si vous êtes coincé avec C #. Mais d'après mon expérience, ils se sentaient généralement plus compliqués qu'ils n'en valaient la peine - YMMV.

Quelques autres liens:

37
MichaelGG

On peut soutenir que la raison pour laquelle C # ne facilite pas l'activation du type est qu'il s'agit principalement d'un langage orienté objet, et la manière "correcte" de le faire en termes orientés objet serait de définir une méthode GetRentPrice sur Vehicle et le remplacer dans les classes dérivées.

Cela dit, j'ai passé un peu de temps à jouer avec des langages multi-paradigmes et fonctionnels comme F # et Haskell qui ont ce type de capacité, et je suis tombé sur un certain nombre d'endroits où cela serait utile auparavant (par exemple lorsque vous n'écrivent pas les types dont vous avez besoin pour activer, vous ne pouvez donc pas implémenter une méthode virtuelle sur eux) et c'est quelque chose que je souhaiterais dans la langue avec les unions discriminées.

[Edit: Suppression d'une partie sur les performances car Marc a indiqué qu'elle pourrait être court-circuitée)

Un autre problème potentiel est celui de l'utilisabilité - il est clair dès le dernier appel ce qui se passe si la correspondance ne remplit aucune condition, mais quel est le comportement si elle correspond à deux conditions ou plus? Doit-il lever une exception? Doit-il retourner le premier ou le dernier match?

Une façon que j'ai tendance à utiliser pour résoudre ce genre de problème est d'utiliser un champ de dictionnaire avec le type comme clé et le lambda comme valeur, ce qui est assez laconique à construire en utilisant la syntaxe d'initialisation d'objet; cependant, cela ne tient compte que du type de béton et ne permet pas de prédicats supplémentaires et peut donc ne pas convenir à des cas plus complexes. [Note latérale - si vous regardez la sortie du compilateur C #, il convertit fréquemment les instructions de commutation en tables de sauts basées sur un dictionnaire, donc il ne semble pas y avoir de bonne raison pour laquelle il ne pouvait pas prendre en charge la commutation de types]

25
Greg Beech

Je ne pense pas que ces types de bibliothèques (qui agissent comme des extensions de langage) soient susceptibles d'être largement acceptées, mais elles sont amusantes à jouer et peuvent être vraiment utiles pour les petites équipes travaillant dans des domaines spécifiques où cela est utile. Par exemple, si vous écrivez des tonnes de "règles/logique métier" qui effectuent des tests de type arbitraires comme celui-ci et ainsi de suite, je peux voir comment ce serait pratique.

Je n'ai aucune idée si cela est susceptible d'être une fonctionnalité du langage C # (cela semble douteux, mais qui peut voir l'avenir?).

Pour référence, le F # correspondant est approximativement:

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

en supposant que vous avez défini une hiérarchie de classes dans le sens de

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors
22
Brian

Pour répondre à votre question, oui, je pense que les constructions syntaxiques d'appariement de motifs sont utiles. Pour ma part, j'aimerais voir un support syntaxique en C # pour cela.

Voici mon implémentation d'une classe qui fournit (presque) la même syntaxe que vous décrivez

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var Tuple in cases)
            if (Tuple.Item1(o))
                return Tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

Voici un code de test:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }
13
cdiggins

Correspondance de modèle (comme décrit ici ), son but est de déconstruire les valeurs en fonction de leur spécification de type. Cependant, le concept d'une classe (ou d'un type) en C # n'est pas d'accord avec vous.

Il n'y a pas de mal à concevoir un langage multi-paradigme, au contraire, c'est très agréable d'avoir des lambdas en C #, et Haskell peut faire des choses impératives, par exemple IO. Mais ce n'est pas une solution très élégante, pas à la mode Haskell.

Mais comme les langages de programmation procéduraux séquentiels peuvent être compris en termes de calcul lambda, et que C # correspond bien aux paramètres d'un langage procédural séquentiel, c'est un bon ajustement. Mais, en prenant quelque chose du contexte fonctionnel pur de Haskell, puis en mettant cette fonctionnalité dans un langage qui n'est pas pur, eh bien, faire juste cela, ne garantira pas un meilleur résultat.

Mon point est le suivant: ce qui fait cocher la correspondance de motifs est lié à la conception du langage et au modèle de données. Cela dit, je ne pense pas que la correspondance de modèles soit une fonctionnalité utile de C # car elle ne résout pas les problèmes typiques de C # et ne s'intègre pas bien dans le paradigme de programmation impérative.

9
John Leidegren

À mon humble avis, la manière OO de faire de telles choses est le modèle de visiteur. Vos méthodes de membre visiteur agissent simplement comme des constructions de cas et vous laissez le langage lui-même gérer la répartition appropriée sans avoir à "jeter un œil" aux types.

5
bacila

Bien qu'il ne soit pas très "C-sharpey" d'activer le type, je sais que la construction serait assez utile en général - j'ai au moins un projet personnel qui pourrait l'utiliser (bien que son ATM gérable). Y a-t-il beaucoup de problèmes de performances de compilation, avec la réécriture de l'arborescence d'expression?

4
Simon Buchan

Je pense que cela semble vraiment intéressant (+1), mais une chose à laquelle il faut faire attention: le compilateur C # est assez bon pour optimiser les instructions de commutation. Pas seulement pour les courts-circuits - vous obtenez une IL complètement différente selon le nombre de cas que vous avez, etc.

Votre exemple spécifique fait quelque chose que je trouverais très utile - il n'y a pas de syntaxe équivalente à la casse par type, car (par exemple) typeof(Motorcycle) n'est pas une constante.

Cela devient plus intéressant dans une application dynamique - votre logique ici pourrait être facilement pilotée par les données, donnant une exécution de style "moteur de règles".

3
Keith

Vous pouvez réaliser ce que vous recherchez en utilisant une bibliothèque que j'ai écrite, appelée OneOf

Le principal avantage sur switch (et if et exceptions as control flow) est qu'il est sûr au moment de la compilation - il n'y a pas de gestionnaire par défaut ni de chute

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

C'est sur Nuget et cible net451 et netstandard1.6

0
mcintyre321