Les fonctions
Concept de base
Imaginez une fonction comme une petite machine à laquelle on confie une tâche précise. On lui fournit des éléments en entrée, elle exécute des étapes bien définies, puis elle nous renvoie éventuellement un résultat. C'est une manière d'organiser sa pensée et son code : on découpe un problème en gestes simples, réutilisables, et on leur donne un nom parlant.
Une fonction est un bloc d'instructions qui porte un nom, qui peut prendre des paramètres en entrée et peut renvoyer une valeur en sortie.
Pourquoi utiliser des fonctions ?
- Clarifier le code en le découpant en morceaux cohérents et faciles à lire
- Éviter la répétition (DRY: Don't Repeat Yourself) et donc réduire les erreurs
- Nommer l'intention : un bon nom raconte ce que fait le code
- Tester plus facilement chaque pièce du programme, indépendamment des autres
Définition d'une fonction
Exemple de signature simple
void Affiche(string texte)
{
Console.WriteLine(texte);
}Pour appeler cette fonction :
Affiche("Bonjour");Avec plusieurs paramètres
void AfficheNFois(string texte, int nb)
{
for (int i = 0; i < nb; i++)
{
Console.WriteLine(texte);
}
}Pour appeler cette fonction :
AfficheNFois("Bonjour", 4);Signature de fonction
La signature d'une fonction est sa carte d'identité : elle décrit précisément comment identifier et utiliser cette fonction. Elle comprend :
- Le nom de la fonction
- Le nombre de paramètres
- Le type de chaque paramètre
- L'ordre des paramètres
Important
Le type de retour ne fait PAS partie de la signature en C#. Deux fonctions ne peuvent pas avoir la même signature même si elles retournent des types différents.
Exemples de signatures
// Signature : Affiche(string)
void Affiche(string texte)
// Signature : Addition(int, int)
int Addition(int a, int b)
// Signature : CalculerMoyenne(double, double, double)
double CalculerMoyenne(double note1, double note2, double note3)
// Signature : ConvertirEnMajuscules(string)
string ConvertirEnMajuscules(string texte)Signatures distinctes vs identiques
Ces fonctions ont des signatures différentes (surcharge valide) :
void Afficher(int nombre) // Signature : Afficher(int)
void Afficher(string texte) // Signature : Afficher(string)
void Afficher(int a, int b) // Signature : Afficher(int, int)
void Afficher(string nom, int age) // Signature : Afficher(string, int)Ces fonctions ont la même signature (erreur de compilation) :
void Calculer(int a, int b) // Signature : Calculer(int, int)
int Calculer(int x, int y) // ❌ ERREUR ! Même signature
double Calculer(int nb1, int nb2) // ❌ ERREUR ! Même signatureLe compilateur ne peut pas distinguer ces fonctions car elles ont la même signature, même si :
- Les noms des paramètres sont différents (
a,bvsx,yvsnb1,nb2) - Les types de retour sont différents (
voidvsintvsdouble)
Pourquoi la signature est-elle importante ?
- Identification unique : la signature permet au compilateur de savoir quelle fonction appeler
- Surcharge de fonctions : on peut avoir plusieurs fonctions avec le même nom si leurs signatures sont différentes
- Résolution d'appel : le compilateur choisit automatiquement la bonne fonction selon les arguments fournis
// Le compilateur choisit automatiquement selon les arguments
Afficher(42); // Appelle Afficher(int)
Afficher("Hello"); // Appelle Afficher(string)
Afficher(1, 2); // Appelle Afficher(int, int)
Afficher("Alice", 25); // Appelle Afficher(string, int)Fonction avec valeur de retour
Une fonction peut renvoyer une valeur avec le mot-clé return. Reprenons notre métaphore de la « petite machine » : parfois, on lui demande simplement d'agir (afficher un texte), parfois on lui demande de calculer et de nous rapporter un résultat.
int Somme(int a, int b)
{
int resultat = a + b;
return resultat;
}💡 Astuce de lecture : le type placé avant le nom de la fonction (int ici) annonce la nature de la valeur promise en retour. Si la fonction ne renvoie rien, on écrit void.
Pour utiliser la valeur renvoyée :
int total = Somme(5, 3);
Console.WriteLine(total); // Affiche 8Le passage de paramètres
Quand on appelle une fonction, on lui « apporte » des informations: ce sont les paramètres. Deux manières principales existent pour les transmettre, et elles ne racontent pas la même histoire.
Comportement par défaut : passage par valeur
En C#, le passage par valeur est le comportement par défaut. Cela signifie que :
- Par valeur: on donne une copie. La fonction travaille sur son exemplaire à elle.
- Par référence: on donne un lien vers l'original. La fonction peut alors modifier la chose d'origine.
Pour forcer un passage par référence, il faut explicitement utiliser les mots-clés ref ou out.
Métaphore: prêter un livre
- Par valeur (défaut), c'est comme donner une photocopie: si l'autre surligne, votre livre reste intact.
- Par référence (avec
refouout), c'est prêter votre vrai livre: s'il est annoté, votre exemplaire change.
Par valeur
La fonction reçoit une copie du paramètre, les modifications sont locales seulement.
void Increment(int nombre)
{
nombre++; // Modification locale uniquement
Console.WriteLine("Dans la fonction : " + nombre);
}
int x = 5;
Increment(x);
Console.WriteLine("Après appel : " + x); // Affiche toujours 5Par référence
La variable locale et le paramètre partagent la même instance. On utilise ref ou out.
Différence entre ref et out
ref (référence) :
- La variable doit être initialisée avant l'appel de la fonction
- La fonction peut lire ET modifier la valeur
- Utilisé quand on veut modifier une variable existante
out (sortie) :
- La variable peut ne pas être initialisée avant l'appel
- La fonction doit obligatoirement assigner une valeur au paramètre
- Utilisé quand la fonction doit "retourner" plusieurs valeurs
Avec ref
void Increment(ref int nombre)
{
nombre++; // Modifie la variable d'origine
}
int x = 5;
Increment(ref x);
Console.WriteLine(x); // Affiche 6Erreurs fréquentes
- Oublier d’écrire
refà l’appel comme dans la définition:Increment(ref x). - Confondre « modifier la chose » et « changer vers une nouvelle chose »: sur un type référence, on peut modifier l’objet pointé même si on a passé « par valeur » (voir plus bas).
- Utiliser
outsans initialiser la variable à l’appel: c’est normal,outpromet qu’elle sera assignée dans la fonction.
Visualisation du passage de paramètres (version haute et compacte)
Reconnaître le passage par valeur/référence d'un type valeur/référence
| Types \ Passage de paramètre | Passage par valeur | Passage par référence |
|---|---|---|
| Type valeur | f(int i) | f(ref int i) |
| Type référence | f(List<int> l) | f(ref List<int> l) |
Types valeur vs types référence
Pour bien comprendre ce qui se passe « sous le capot », il faut se rappeler qu'il faut distinguer deux familles de types. Cette distinction explique pourquoi, parfois, une modification semble « rester » après l’appel d’une fonction… et parfois pas.
Les comportements du passage de paramètres varient selon qu'il s'agit de types valeur (int, bool, struct...) ou de types référence (objets, tableaux, chaînes...).
Types valeur : passage par valeur vs référence
Les types valeur (int, bool, char, double, struct...) stockent directement leur contenu dans la variable. Imaginez-les comme des boîtes qui contiennent réellement la valeur.
Passage par valeur (comportement par défaut)
Quand on passe un type valeur à une fonction, C# fait une photocopie de la valeur :
void TesterPassageValeur(int nombre)
{
Console.WriteLine($"Reçu : {nombre}");
nombre = 100; // On modifie la copie
Console.WriteLine($"Modifié dans fonction : {nombre}");
}
int monNombre = 42;
Console.WriteLine($"Avant appel : {monNombre}");
TesterPassageValeur(monNombre);
Console.WriteLine($"Après appel : {monNombre}"); // Toujours 42 !Résultat :
Avant appel : 42
Reçu : 42
Modifié dans fonction : 100
Après appel : 42Pourquoi ? La fonction a reçu une copie de monNombre. Modifier cette copie ne change pas l'original.
Passage par référence (avec ref ou out)
Avec ref, on donne l'adresse de la boîte originale à la fonction :
void TesterPassageReference(ref int nombre)
{
Console.WriteLine($"Reçu : {nombre}");
nombre = 100; // On modifie l'original !
Console.WriteLine($"Modifié dans fonction : {nombre}");
}
int monNombre = 42;
Console.WriteLine($"Avant appel : {monNombre}");
TesterPassageReference(ref monNombre); // N'oubliez pas 'ref' !
Console.WriteLine($"Après appel : {monNombre}"); // Maintenant 100 !Résultat :
Avant appel : 42
Reçu : 42
Modifié dans fonction : 100
Après appel : 100Métaphore : C'est comme donner les clés de votre maison vs donner une photo de votre maison. Avec les clés (ref), la personne peut changer votre décoration !
Types référence : passage par valeur vs référence
Les types référence (tableaux, objets, listes, strings...) fonctionnent différemment. La variable ne contient pas l'objet lui-même, mais une adresse vers l'endroit où l'objet est stocké en mémoire.
Imaginez un type référence comme un marque-page dans un livre :
- Le marque-page (la variable) indique une page
- Le contenu de la page (l'objet) existe quelque part dans le livre (la mémoire)
Passage par valeur (comportement par défaut)
Même "par valeur", on peut modifier le contenu de l'objet :
void ModifierContenu(int[] tableau)
{
Console.WriteLine($"Tableau reçu : [{string.Join(", ", tableau)}]");
tableau[0] = 999; // On modifie le contenu à travers la référence
Console.WriteLine($"Tableau modifié : [{string.Join(", ", tableau)}]");
}
int[] monTableau = {1, 2, 3};
Console.WriteLine($"Avant appel : [{string.Join(", ", monTableau)}]");
ModifierContenu(monTableau);
Console.WriteLine($"Après appel : [{string.Join(", ", monTableau)}]");Résultat :
Avant appel : [1, 2, 3]
Tableau reçu : [1, 2, 3]
Tableau modifié : [999, 2, 3]
Après appel : [999, 2, 3]Visualisation en mémoire :
Pourquoi ça marche ? La fonction reçoit une copie de l'adresse, mais les deux variables pointent vers le même objet en mémoire ! Modifier l'objet via une des deux adresses affecte ce que voient les deux variables.
Mais attention : on ne peut pas modifier l'adresse elle-même :
void TenterChangerReference(int[] tableau)
{
tableau = new int[] {10, 20, 30}; // Nouvelle adresse locale uniquement
Console.WriteLine($"Nouveau tableau dans fonction : [{string.Join(", ", tableau)}]");
}
int[] monTableau = {1, 2, 3};
Console.WriteLine($"Avant appel : [{string.Join(", ", monTableau)}]");
TenterChangerReference(monTableau);
Console.WriteLine($"Après appel : [{string.Join(", ", monTableau)}]"); // Inchangé !Résultat :
Avant appel : [1, 2, 3]
Nouveau tableau dans fonction : [10, 20, 30]
Après appel : [1, 2, 3]Visualisation en mémoire :
Explication : Dans la fonction, tableau reçoit une copie de l'adresse vers {1, 2, 3}. Quand on fait tableau = new int[] {10, 20, 30}, on change seulement la variable locale tableau pour qu'elle pointe vers un nouveau tableau. La variable monTableau dans le programme principal garde son adresse originale.
Passage par référence (avec ref)
Avec ref, on peut modifier la variable qui contient l'adresse :
void ChangerReference(ref int[] tableau)
{
tableau = new int[] {10, 20, 30}; // On change la variable originale
Console.WriteLine($"Nouveau tableau : [{string.Join(", ", tableau)}]");
}
int[] monTableau = {1, 2, 3};
Console.WriteLine($"Avant appel : [{string.Join(", ", monTableau)}]");
ChangerReference(ref monTableau);
Console.WriteLine($"Après appel : [{string.Join(", ", monTableau)}]");Résultat :
Avant appel : [1, 2, 3]
Nouveau tableau : [10, 20, 30]
Après appel : [10, 20, 30]Visualisation en mémoire :
Explication : Avec ref, la fonction reçoit un accès direct à la variable monTableau elle-même. Elle peut donc changer ce vers quoi pointe cette variable. Le paramètre tableau n'est pas une copie, mais un alias de monTableau.
Résumé des comportements :
- Types valeur par valeur : copie de la valeur → modifications locales seulement
- Types valeur par référence : accès direct à la variable → modifications permanentes
- Types référence par valeur : copie de l'adresse → peut modifier l'objet, pas la variable
- Types référence par référence : accès direct à la variable ET à l'objet → contrôle total
Exemple concret avec tableau
Voici comment les tableaux (types référence) se comportent avec différentes approches :
// Modification du tableau original
void ModifierTableau(int[] param)
{
param[2] = 9; // Modifie le tableau original
}
// Création d'un nouveau tableau
void ModifierNouveauTableau(int[] param)
{
int[] nouveauArr = new int[] {param[0], param[1], 9};
// nouveauArr pointe vers un nouveau tableau
// param n'est pas modifié
}
// Dans le programme principal
int[] arr = new int[] {1, 2, 3};
ModifierTableau(arr);
Console.WriteLine(arr[2]); // Affiche 9 (modifié)
arr = new int[] {1, 2, 3};
ModifierNouveauTableau(arr);
Console.WriteLine(arr[2]); // Affiche 3 (non modifié)Points importants à retenir
Types valeur (int, bool, char, struct...) :
- Passés par valeur : la fonction reçoit une copie, les modifications sont locales
- Passés par référence (ref/out) : la fonction modifie la variable originale
Types référence (tableaux, objets, listes...) :
- Passés par valeur : la fonction reçoit une copie de la référence, mais peut modifier l'objet original
- Passés par référence (ref/out) : la fonction peut changer la référence elle-même
Quand utiliser le passage par référence ?
- Pour modifier plusieurs valeurs dans une fonction
- Pour éviter la copie de grandes structures de données
- Pour retourner plusieurs valeurs depuis une fonction
void CalculerStatistiques(int[] donnees, out double moyenne, out double ecartType)
{
moyenne = donnees.Average();
double sommeCarres = 0;
foreach (int valeur in donnees)
{
sommeCarres += Math.Pow(valeur - moyenne, 2);
}
ecartType = Math.Sqrt(sommeCarres / donnees.Length);
}
// Utilisation:
int[] mesures = new int[] {5, 7, 9, 12, 15};
double moy, ecart;
CalculerStatistiques(mesures, out moy, out ecart);
Console.WriteLine($"Moyenne: {moy}, Écart-type: {ecart}");Avec out
void ObtenirDonnees(out int valeur)
{
valeur = 42; // Doit obligatoirement assigner une valeur
}
int resultat; // Pas besoin d'initialiser avec out
ObtenirDonnees(out resultat);
Console.WriteLine(resultat); // Affiche 42Exemple comparatif ref vs out
// Avec ref : variable doit être initialisée
void DoublerAvecRef(ref int nombre)
{
Console.WriteLine($"Valeur reçue : {nombre}"); // Peut lire la valeur
nombre = nombre * 2; // Modifie la valeur
}
// Avec out : variable sera initialisée dans la fonction
void CreerNombreAvecOut(out int nombre)
{
// Console.WriteLine(nombre); // ERREUR! Ne peut pas lire avant d'assigner
nombre = 100; // Doit obligatoirement assigner
}
// Utilisation
int x = 5; // Doit être initialisé pour ref
DoublerAvecRef(ref x);
Console.WriteLine(x); // Affiche 10
int y; // Pas besoin d'initialiser pour out
CreerNombreAvecOut(out y);
Console.WriteLine(y); // Affiche 100Différence entre ref et out
ref: la variable doit être initialisée avant l'appel et peut être lue dans la fonctionout: la variable peut ne pas être initialisée avant l'appel, mais doit obligatoirement recevoir une valeur dans la fonction
Paramètres optionnels
void Message(string texte, bool majuscules = false)
{
if (majuscules)
{
Console.WriteLine(texte.ToUpper());
}
else
{
Console.WriteLine(texte);
}
}Utilisation :
Message("Hello"); // Paramètre optionnel non fourni
Message("Hello", true); // Paramètre optionnel fourniParamètres nommés
Les paramètres nommés permettent de spécifier explicitement le nom du paramètre lors de l'appel d'une fonction. Cela améliore la lisibilité du code et permet de passer les paramètres dans n'importe quel ordre.
Syntaxe
void CreerPersonne(string nom, string prenom, int age, string ville = "Inconnue")
{
Console.WriteLine($"{prenom} {nom}, {age} ans, habite à {ville}");
}Appel avec paramètres nommés
// Appel classique (ordre des paramètres important)
CreerPersonne("Dupont", "Jean", 25, "Bruxelles");
// Appel avec paramètres nommés (ordre libre)
CreerPersonne(age: 25, nom: "Dupont", prenom: "Jean", ville: "Bruxelles");
// Mélange paramètres positionnels et nommés
CreerPersonne("Dupont", "Jean", age: 25, ville: "Bruxelles");
// Avec paramètre optionnel omis
CreerPersonne(nom: "Martin", prenom: "Marie", age: 30);Avantages des paramètres nommés
Clarté du code : le nom du paramètre indique explicitement ce que représente la valeur
csharp// Peu clair CalculerPrix(100, 0.21, true, false); // Beaucoup plus clair CalculerPrix(montant: 100, tva: 0.21, inclureTva: true, appliquerRemise: false);Ordre flexible : possibilité de passer les paramètres dans n'importe quel ordre
csharpvoid ConfigurerServeur(string host, int port, bool ssl, string database) { // Configuration du serveur } // Ordre des paramètres libre avec noms ConfigurerServeur(ssl: true, database: "mydb", host: "localhost", port: 8080);Sécurité : évite les erreurs dues à l'inversion de paramètres de même type
csharpvoid DeplacerPoint(int x, int y) { Console.WriteLine($"Déplacement vers ({x}, {y})"); } // Risque d'erreur DeplacerPoint(5, 10); // x=5, y=10 ou x=10, y=5 ? // Pas d'ambiguïté DeplacerPoint(x: 5, y: 10); // Clairement x=5, y=10
Règles importantes
- Les paramètres positionnels doivent venir avant les paramètres nommés
- Une fois qu'un paramètre nommé est utilisé, tous les suivants doivent aussi être nommés
// ✅ Correct
MaFonction(param1, param2, nom3: valeur3, nom4: valeur4);
// ❌ Incorrect - paramètre positionnel après un paramètre nommé
MaFonction(param1, nom2: valeur2, param3, nom4: valeur4);Surcharge de fonction
La surcharge permet de définir plusieurs fonctions avec le même nom mais des paramètres différents.
int Addition(int a, int b)
{
return a + b;
}
double Addition(double a, double b)
{
return a + b;
}Le compilateur choisit automatiquement la bonne fonction selon les types des arguments.
Fonctions récursives
Une fonction récursive est une fonction qui s'appelle elle-même pour résoudre un problème. C'est comme une poupée russe : pour ouvrir une grande poupée, on ouvre une plus petite à l'intérieur, puis une encore plus petite, jusqu'à arriver à la plus petite qui ne contient plus rien.
Principe fondamental
Toute fonction récursive doit respecter deux règles absolues :
- Cas de base (condition d'arrêt) : une situation où la fonction ne s'appelle plus elle-même
- Réduction du problème : à chaque appel, le problème doit devenir plus simple et se rapprocher du cas de base
Exemple simple : compte à rebours
Commençons par un exemple très simple pour comprendre le mécanisme :
void CompteARebours(int n)
{
// Cas de base : on s'arrête à 0
if (n <= 0)
{
Console.WriteLine("Fini !");
return;
}
// Action pour le niveau actuel
Console.WriteLine(n);
// Appel récursif avec un problème plus petit
CompteARebours(n - 1);
}Trace d'exécution pour CompteARebours(3) :
Sortie console :
3
2
1
Fini !La pile d'appels en action
Quand une fonction s'appelle elle-même, chaque appel crée une nouvelle "couche" dans la pile d'appels. Visualisons ce qui se passe en mémoire :
Exemple détaillé : calcul de factorielle
La factorielle est un exemple classique de récursion. Rappel mathématique :
5! = 5 × 4 × 3 × 2 × 1 = 120n! = n × (n-1)!0! = 1et1! = 1(cas de base)
int Factorielle(int n)
{
// Cas de base : factorielle de 0 ou 1 vaut 1
if (n <= 1)
{
Console.WriteLine($"Cas de base : {n}! = 1");
return 1;
}
// Cas récursif : n! = n × (n-1)!
Console.WriteLine($"Calcul de {n}! = {n} × {n-1}!");
int resultatSousProbleme = Factorielle(n - 1);
int resultatActuel = n * resultatSousProbleme;
Console.WriteLine($"Retour : {n}! = {n} × {resultatSousProbleme} = {resultatActuel}");
return resultatActuel;
}Trace complète pour Factorielle(4) :
Sortie console complète :
Calcul de 4! = 4 × 3!
Calcul de 3! = 3 × 2!
Calcul de 2! = 2 × 1!
Cas de base : 1! = 1
Retour : 2! = 2 × 1 = 2
Retour : 3! = 3 × 2 = 6
Retour : 4! = 4 × 6 = 24Visualisation de la pile d'appels avec les variables
Voyons comment les variables locales s'accumulent dans la pile :
Exemple avec accumulation : somme des nombres
Voici un autre exemple pour bien comprendre comment les valeurs s'accumulent :
int SommeJusquA(int n)
{
// Cas de base
if (n <= 0)
{
Console.WriteLine("Cas de base : somme jusqu'à 0 = 0");
return 0;
}
// Cas récursif : somme(n) = n + somme(n-1)
Console.WriteLine($"Calcul : somme({n}) = {n} + somme({n-1})");
int sommeReste = SommeJusquA(n - 1);
int resultat = n + sommeReste;
Console.WriteLine($"Retour : somme({n}) = {n} + {sommeReste} = {resultat}");
return resultat;
}Pour SommeJusquA(4) (calcule 4+3+2+1+0) :
Récursion avec chaînes de caractères
Exemple d'inversion d'une chaîne de caractères :
string InverserChaine(string texte)
{
// Cas de base : chaîne vide ou un seul caractère
if (texte.Length <= 1)
{
Console.WriteLine($"Cas de base : '{texte}' -> '{texte}'");
return texte;
}
// Cas récursif : dernier caractère + inversion du reste
char premierChar = texte[0];
string reste = texte.Substring(1);
Console.WriteLine($"Traitement de '{texte}' : '{premierChar}' + inverser('{reste}')");
string resteInverse = InverserChaine(reste);
string resultat = resteInverse + premierChar;
Console.WriteLine($"Retour : inverser('{reste}') + '{premierChar}' = '{resteInverse}' + '{premierChar}' = '{resultat}'");
return resultat;
}Pour InverserChaine("ABC") :
Récursion avec structures conditionnelles : nombre de Fibonacci
Les nombres de Fibonacci illustrent une récursion avec deux appels :
int Fibonacci(int n)
{
// Affichage pour comprendre l'exécution
Console.WriteLine($" {"".PadLeft(n * 2)}Fibonacci({n})");
// Cas de base
if (n <= 1)
{
Console.WriteLine($" {"".PadLeft(n * 2)}-> Cas de base : Fibonacci({n}) = {n}");
return n;
}
// Cas récursif : F(n) = F(n-1) + F(n-2)
Console.WriteLine($" {"".PadLeft(n * 2)}-> Fibonacci({n}) = Fibonacci({n-1}) + Fibonacci({n-2})");
int fib1 = Fibonacci(n - 1);
int fib2 = Fibonacci(n - 2);
int resultat = fib1 + fib2;
Console.WriteLine($" {"".PadLeft(n * 2)}-> Fibonacci({n}) = {fib1} + {fib2} = {resultat}");
return resultat;
}Arbre d'appels pour Fibonacci(4) :
Erreurs courantes et débogage
1. Oubli du cas de base
// ❌ ERREUR : récursion infinie !
int MauvaisComptage(int n)
{
Console.WriteLine(n);
return MauvaisComptage(n - 1); // Pas de condition d'arrêt !
}Résultat : StackOverflowException - La pile d'appels déborde.
2. Cas de base incorrect
// ❌ ERREUR : condition mal formulée
int FactorielleBuggee(int n)
{
if (n == 1) // Que se passe-t-il si n = 0 ?
return 1;
return n * FactorielleBuggee(n - 1);
}Problème : Si on appelle FactorielleBuggee(0), le cas de base n'est jamais atteint !
3. Progression incorrete vers le cas de base
// ❌ ERREUR : n ne diminue jamais
int ProgressionIncorrecte(int n)
{
if (n <= 0)
return 0;
return n + ProgressionIncorrecte(n); // Oups ! Devrait être n-1
}Conseils pour bien écrire une fonction récursive
- Identifiez le cas de base d'abord : quand doit-on s'arrêter ?
- Vérifiez la progression : le paramètre se rapproche-t-il toujours du cas de base ?
- Testez avec de petites valeurs : commencez par n=0, n=1, n=2...
- Ajoutez des traces : utilisez
Console.WriteLinepour voir ce qui se passe - Pensez "diviser pour régner" : comment décomposer le problème en sous-problèmes plus simples ?
Récursion vs itération
Souvent, un problème récursif peut aussi être résolu de manière itérative :
// Version récursive de la factorielle
int FactorielleRecursive(int n)
{
if (n <= 1) return 1;
return n * FactorielleRecursive(n - 1);
}
// Version itérative de la factorielle
int FactorielleIterative(int n)
{
int resultat = 1;
for (int i = 2; i <= n; i++)
{
resultat *= i;
}
return resultat;
}Quand utiliser la récursion ?
- Quand le problème se décompose naturellement en sous-problèmes similaires
- Pour des structures d'arbre ou des algorithmes "diviser pour régner"
- Quand la solution récursive est plus claire et plus facile à comprendre
Quand éviter la récursion ?
- Pour de très grandes valeurs (risque de débordement de pile)
- Quand la version itérative est beaucoup plus efficace
- Quand il y a beaucoup de calculs redondants (comme Fibonacci naïf)
Attention
Les fonctions récursives doivent toujours avoir :
- Un cas de base clairement défini
- Une progression vers ce cas de base à chaque appel récursif
- Une limite raisonnable pour éviter le débordement de pile (
StackOverflowException)
Débogage de la récursion
Pour comprendre une fonction récursive qui ne fonctionne pas :
- Ajoutez des
Console.WriteLineavec le niveau d'indentation - Vérifiez que le cas de base est atteint
- Tracez les valeurs des paramètres à chaque appel
- Testez avec les plus petites valeurs possibles
Fonctions anonymes et expressions lambda
Les fonctions anonymes et expressions lambda permettent de définir des fonctions sans nom.
// Fonction anonyme
Func<int, int, int> addition = delegate(int a, int b) { return a + b; };
// Expression lambda équivalente
Func<int, int, int> addition = (a, b) => a + b;
Console.WriteLine(addition(5, 3)); // Affiche 8En résumé
- Une fonction est une pièce de code qui porte un nom, peut recevoir des paramètres et, parfois, rend une valeur avec
return. - Le passage par valeur est le comportement par défaut en C# : la fonction reçoit une copie des paramètres.
- Pour un passage par référence, il faut explicitement utiliser
refouout:ref: la variable doit être initialisée avant l'appel, la fonction peut la lire et la modifierout: la variable peut ne pas être initialisée, mais la fonction doit obligatoirement lui assigner une valeur
- Les paramètres nommés permettent d'appeler une fonction en spécifiant explicitement le nom des paramètres (ex:
f(a: 5, b: 10)), améliorant la lisibilité et permettant un ordre flexible. - Les types valeur (int, bool, struct) se comportent différemment des types référence (tableaux, objets): gardez cette différence à l'esprit.
Petite checklist avant d'écrire une fonction
- Ai-je un nom clair qui dit ce que la fonction fait ?
- Quels paramètres sont vraiment nécessaires et de quel type ?
- La fonction doit-elle renvoyer une valeur ? Laquelle ?
- Ai-je besoin d'un passage par référence (
ref/out) ou le passage par valeur (défaut) suffit-il ? - Y a-t-il des effets de bord attendus (modifications d’objets) ? Sont-ils souhaitables et documentés ?
