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:
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:
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).
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));
}
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 .
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 #:
** 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:
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]
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
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));
}
}
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.
À 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.
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?
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".
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