Lorsqu'on parle de programmation fonctionnelle, tout le monde doit penser aux anciens langages fonctionnels tels que LISP et Haskell, qui ont une syntaxe très flexible et dynamique. Plus récemment, Ruby, JavaScript et F# sont également des langages populaires pour la programmation fonctionnelle. Cependant, étant donné que .net prend en charge les expressions lambda, bien que C# soit un langage de programmation impératif, il n'est pas inférieur en programmation fonctionnelle. Dans le processus d'écriture de code en C#, nous utiliserons intentionnellement ou non des idées telles que les fonctions d'ordre supérieur, les fonctions combinées et la mise en cache de fonctions pures. Même des idées telles que les arbres d'expression proviennent d'idées de programmation fonctionnelle. Nous résumerons donc ensuite les scénarios de programmation fonctionnelle couramment utilisés, qui nous aideront à appliquer ces technologies de manière flexible dans le processus de programmation, à élargir nos idées de conception et à améliorer la qualité du code.
En termes simples, fonctions d'ordre supérieur : une fonction qui utilise une fonction comme paramètre est appelée fonction d'ordre supérieur. Selon cette définition, les expressions LINQ, Where, Select, SelectMany, First et d'autres méthodes largement utilisées dans .net sont toutes des fonctions d'ordre élevé. Alors, quand utiliserons-nous cette conception lorsque nous écrivons notre propre code ?
Exemple : Concevez une fonction pour calculer les frais de propriété, var fee=square*price, et la superficie (carré) est calculée de différentes manières selon la nature de la propriété. Les résidences civiles, les résidences commerciales, etc. doivent être multipliées par différents coefficients. En fonction de ces besoins, nous essayons de concevoir la fonction suivante :
. Quartier résidentiel :
public Func<int,int,decimal> SquareForCivil() { return (width,hight)=>width*hight; }
Zone commerciale et résidentielle :
public Func<int, int, decimal> SquareForBusiness() { return (width, hight) => width * hight*1.2m; }
Ces fonctions ont toutes une signature commune : Func
public decimal PropertyFee(decimal price,int width,int hight, Func<int, int, decimal> square) { return price*square(width, hight); }
N'est-ce pas très simple ? Écrivez un test et voyez
[Test] public void Should_calculate_propertyFee_for_two_area() { //Arrange var calculator = new PropertyFeeCalculator(); //Act var feeForBusiness= calculator.PropertyFee(2m,2, 2, calculator.SquareForBusiness()); var feeForCivil = calculator.PropertyFee(1m, 2, 2, calculator.SquareForCivil()); //Assert feeForBusiness.Should().Be(9.6m); feeForCivil.Should().Be(4m); }
C# utilise une stratégie d'évaluation stricte lors de l'exécution. L'évaluation dite stricte signifie que les paramètres sont évalués avant d'être transmis à la fonction. Cette explication est-elle encore un peu floue ? Regardons un scénario : il y a une tâche qui doit être exécutée, qui nécessite que l'utilisation actuelle de la mémoire soit inférieure à 80 %, et le résultat du calcul précédent est <100. Cette condition peut être remplie avant que la tâche puisse être. exécuté.
Nous pouvons écrire rapidement du code C# qui répond à cette exigence :
public double MemoryUtilization() { //计算目前内存使用率 var pcInfo = new ComputerInfo(); var usedMem = pcInfo.TotalPhysicalMemory - pcInfo.AvailablePhysicalMemory; return (double)(usedMem / Convert.ToDecimal(pcInfo.TotalPhysicalMemory)); } public int BigCalculatationForFirstStep() { //第一步运算 System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine("big calulation"); FirstStepExecuted = true; return 10; } public void NextStep(double memoryUtilization,int firstStepDistance) { //下一步运算 if(memoryUtilization<0.8&&firstStepDistance<100) { Console.WriteLine("Next step"); } }
Lors de l'exécution de NextStep, vous devez transmettre l'utilisation de la mémoire et le résultat du calcul de la première étape (fonction BigCalculationForFirstStep). Comme le montre le code, l'opération de la première étape est une opération très fastidieuse, mais en raison de la stratégie d'évaluation stricte. de C#, pour les instructions If (memoryUtilization<0.8&&firstStepDistance<100), même si l'utilisation de la mémoire est supérieure à 80 %, la première opération doit être effectuée. Évidemment, si l'utilisation de la mémoire est supérieure à 80 %, la valeur de firstStepDistance. n'a plus d'importance, et c'est tout à fait bien. Pas besoin de calculer.
Une évaluation paresseuse signifie donc que les expressions ou parties d’expressions ne sont évaluées que lorsque leurs résultats sont réellement nécessaires. Nous essayons de réécrire cette exigence en utilisant des fonctions d'ordre supérieur :
public void NextStepWithOrderFunction(Func<double> memoryUtilization,Func<int> firstStep) { if (memoryUtilization() < 0.8 && firstStep() < 100) { Console.WriteLine("Next step"); } }
Le code est très simple, utilisez simplement une expression de fonction pour remplacer la valeur de la fonction. Si if (memoryUtilization() < 0.8.. n'est pas satisfait, les fonctions suivantes ne seront pas exécutées. Microsoft a ajouté Lazy< dans la version .net4.0 ; T>, vous pouvez utiliser ce mécanisme dans des scénarios où un tel besoin existe
Le curry est également appelé application partielle. Définition : C'est une technologie qui transforme une fonction qui accepte plusieurs paramètres en une fonction qui accepte un seul paramètre (le premier paramètre de la fonction d'origine), et renvoie une nouvelle fonction qui accepte les paramètres restants et renvoie un résultat PS : Pourquoi. l'explication officielle est-elle si déroutante ?
En voyant une telle définition, il est sans doute difficile pour tout le monde de comprendre de quoi il s'agit, alors commençons par le principe du curry :
Écrivez une fonction qui ajoute deux nombres :
public Func<int, int, int> AddTwoNumber() { return (x, y) => x + y; }
OK, comment utiliser cette fonction ?
var result= _curringReasoning.AddTwoNumber()(1,2);
1 2=3, l'appel est très simple. Pour mettre à niveau nos exigences, nous avons besoin d'une fonction qui nécessite la saisie d'un paramètre (nombre) et calcule le résultat de 10 paramètres d'entrée (nombre). Je suppose que quelqu'un a dit que le code ci-dessus peut répondre complètement à cette exigence. Si vous transmettez 10 comme premier paramètre, ce sera terminé, si vous le pensez, je n'ai rien à faire. D'autres ont peut-être dit que l'écriture d'une autre surcharge ne nécessite qu'un seul paramètre, mais la situation réelle n'est pas autorisée. Nous appelons l'API fournie par d'autres et ne pouvons pas ajouter de surcharge. On peut voir que le scénario d'utilisation de l'application partielle n'est pas un scénario très courant, la meilleure conception est donc de faire correspondre la technologie appropriée dans la scène appropriée :
public Func<int, Func<int, int>> AddTwoNumberCurrying() { Func<int, Func<int, int>> addCurrying = x => y => x + y; return addCurrying; }
//Act var curringResult = curringReasoning.AddTwoNumberCurrying()(10); var result = curringResult(2); //Assert result.Should().Be(12);
这句话:var curringResult = curringReasoning.AddTwoNumberCurrying()(10); 生成的函数就是只接收一个参数(number),且可以计算出10+number的函数。
同样的道理,三个数相加的函数:
public Func<int,int,int,int> AddThreeNumber() { return (x, y, z) => x + y + z; }
局部套用版本:
public Func<int,Func<int,Func<int,int>>> AddThreeNumberCurrying() { Func<int, Func<int, Func<int, int>>> addCurring = x => y => z => x + y + z; return addCurring; }
调用过程:
[Test] public void Three_number_add_test() { //Arrange var curringReasoning = new CurryingReasoning(); //Act var result1 = curringReasoning.AddThreeNumber()(1, 2, 3); var curringResult = curringReasoning.AddThreeNumberCurrying()(1); var curringResult2 = curringResult(2); var result2 = curringResult2(3); //Assert result1.Should().Be(6); result2.Should().Be(6); }
当函数参数多了之后,手动局部套用越来越不容易写,我们可以利用扩展方法自动局部套用:
public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult> func) { return x => y => func(x, y); } public static Func<T1, Func<T2, Func<T3, TResult>>> Curry<T1, T2, T3, TResult>(this Func<T1, T2, T3,TResult> func) { return x => y => z=>func(x, y,z); }
同样的道理,Action<>签名的函数也可以自动套用
有了这些扩展方法,使用局部套用的时候就更加easy了
[Test] public void Should_auto_curry_two_number_add_function() { //Arrange var add = _curringReasoning.AddTwoNumber(); var addCurrying = add.Curry(); //Act var result = addCurrying(1)(2); //Assert result.Should().Be(3); }
好了,局部套用就说到这里,stackoverflow有几篇关于currying使用的场景和定义的文章,大家可以继续了解。
函数式编程还有一些重要的思想,例如:纯函数的缓存,所为纯函数是指函数的调用不受外界的影响,相同的参数调用得到的值始终是相同的。尾递归,单子,代码即数据(.net中的表达式树),部分应用,组合函数,这些思想有的我也仍然在学习中,有的还在思考其最佳使用场景,所以不再总结,如果哪天领会了其思想会补充。
最后我还是想设计一个场景,把高阶函数,lambda表达式,泛型方法结合在一起,我之所以设计这样的例子是因为现在很多的框架,开源的项目都有类似的写法,也正是因为各种技术和思想结合在一起,才有了极富有表达力并且非常优雅的代码。
需求:设计一个单词查找器,该查找器可以查找某个传入的model的某些字段是否包含某个单词,由于不同的model具有不同的字段,所以该查找需要配置,并且可以充分利用vs的智能提示。
这个功能其实就两个方法:
private readonly List<Func<string, bool>> _conditions; public WordFinder<TModel> Find<TProperty>(Func<TModel,TProperty> expression) { Func<string, bool> searchCondition = word => expression(_model).ToString().Split(' ').Contains(word); _conditions.Add(searchCondition); return this; } public bool Execute(string wordList) { return _conditions.Any(x=>x(wordList)); }
使用:
[Test] public void Should_find_a_word() { //Arrange var article = new Article() { Title = "this is a title", Content = "this is content", Comment = "this is comment", Author = "this is author" }; //Act var result = Finder.For(article) .Find(x => x.Title) .Find(x => x.Content) .Find(x => x.Comment) .Find(x => x.Author) .Execute( "content"); //Assert result.Should().Be(true); }
该案例本身不具有实用性,但是大家可以看到,正是各种技术的综合应用才设计出极具语义的api, 如果函数参数改为Expression
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!