Code C# bogué : les 10 erreurs les plus courantes dans la programmation C#
Publié: 2022-03-11À propos de C dièse
C# est l'un des nombreux langages qui ciblent le Microsoft Common Language Runtime (CLR). Les langages qui ciblent le CLR bénéficient de fonctionnalités telles que l'intégration inter-langage et la gestion des exceptions, une sécurité renforcée, un modèle simplifié pour l'interaction des composants et des services de débogage et de profilage. Parmi les langages CLR d'aujourd'hui, C# est le plus largement utilisé pour les projets de développement professionnels complexes qui ciblent les environnements de bureau, mobiles ou de serveur Windows.
C# est un langage orienté objet et fortement typé. La vérification stricte des types en C #, à la fois au moment de la compilation et de l'exécution, entraîne le signalement le plus tôt possible de la majorité des erreurs de programmation C # typiques et la localisation assez précise de leur emplacement. Cela peut faire gagner beaucoup de temps dans la programmation C Sharp, par rapport à la recherche de la cause d'erreurs déroutantes qui peuvent se produire longtemps après que l'opération incriminée ait eu lieu dans des langages plus libéraux avec leur application de la sécurité de type. Cependant, de nombreux codeurs C# rejettent involontairement (ou négligemment) les avantages de cette détection, ce qui entraîne certains des problèmes abordés dans ce didacticiel C#.
À propos de ce didacticiel de programmation C Sharp
Ce didacticiel décrit 10 des erreurs de programmation C# les plus courantes commises ou des problèmes à éviter par les programmeurs C# et leur fournit de l'aide.
Bien que la plupart des erreurs abordées dans cet article soient spécifiques à C #, certaines sont également pertinentes pour d'autres langages qui ciblent le CLR ou utilisent la bibliothèque de classes Framework (FCL).
Erreur courante de programmation C # n ° 1 : utiliser une référence comme une valeur ou vice versa
Les programmeurs de C++, et de nombreux autres langages, ont l'habitude de contrôler si les valeurs qu'ils attribuent aux variables sont simplement des valeurs ou des références à des objets existants. En programmation C Sharp, cependant, cette décision est prise par le programmeur qui a écrit l'objet, et non par le programmeur qui instancie l'objet et l'affecte à une variable. C'est un "gotcha" commun pour ceux qui essaient d'apprendre la programmation C#.
Si vous ne savez pas si l'objet que vous utilisez est un type valeur ou un type référence, vous pourriez avoir des surprises. Par exemple:
Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue
Comme vous pouvez le voir, les objets Point
et Pen
ont été créés exactement de la même manière, mais la valeur de point1
est restée inchangée lorsqu'une nouvelle valeur de coordonnée X
a été attribuée à point2
, tandis que la valeur de pen1
a été modifiée lorsqu'une nouvelle couleur a été attribuée à pen2
. On peut donc en déduire que point1
et point2
contiennent chacun leur propre copie d'un objet Point
, alors que pen1
et pen2
contiennent des références au même objet Pen
. Mais comment pouvons-nous le savoir sans faire cette expérience ?
La réponse est de regarder les définitions des types d'objets (ce que vous pouvez facilement faire dans Visual Studio en plaçant votre curseur sur le nom du type d'objet et en appuyant sur F12) :
public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type
Comme indiqué ci-dessus, en programmation C#, le mot clé struct
est utilisé pour définir un type valeur, tandis que le mot clé class
est utilisé pour définir un type référence. Pour ceux qui ont une formation en C++, qui ont été bercés par un faux sentiment de sécurité par les nombreuses similitudes entre les mots-clés C++ et C#, ce comportement est probablement une surprise qui peut vous amener à demander de l'aide à partir d'un didacticiel C#.
Si vous allez dépendre d'un comportement qui diffère entre les types valeur et référence - comme la possibilité de passer un objet en tant que paramètre de méthode et que cette méthode modifie l'état de l'objet - assurez-vous que vous avez affaire à la type d'objet correct pour éviter les problèmes de programmation C#.
Erreur de programmation C# courante n° 2 : mauvaise compréhension des valeurs par défaut pour les variables non initialisées
En C#, les types de valeur ne peuvent pas être nuls. Par définition, les types valeur ont une valeur, et même les variables non initialisées des types valeur doivent avoir une valeur. C'est ce qu'on appelle la valeur par défaut pour ce type. Cela conduit au résultat suivant, généralement inattendu lors de la vérification si une variable n'est pas initialisée :
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
Pourquoi point1
n'est-il pas nul ? La réponse est que Point
est un type valeur et que la valeur par défaut d'un Point
est (0,0), non nulle. Ne pas le reconnaître est une erreur très facile (et courante) à commettre en C#.
De nombreux types de valeur (mais pas tous) ont une propriété IsEmpty
que vous pouvez vérifier pour voir si elle est égale à sa valeur par défaut :
Console.WriteLine(point1.IsEmpty); // True
Lorsque vous vérifiez si une variable a été initialisée ou non, assurez-vous de savoir quelle valeur une variable non initialisée de ce type aura par défaut et ne comptez pas sur le fait qu'elle soit nulle.
Erreur de programmation C# courante #3 : Utilisation de méthodes de comparaison de chaînes inappropriées ou non spécifiées
Il existe de nombreuses façons de comparer des chaînes en C#.
Bien que de nombreux programmeurs utilisent l'opérateur ==
pour la comparaison de chaînes, c'est en fait l'une des méthodes les moins souhaitables à utiliser, principalement parce qu'il ne spécifie pas explicitement dans le code le type de comparaison souhaité.
Au lieu de cela, la méthode préférée pour tester l'égalité des chaînes dans la programmation C # est avec la méthode Equals
:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
La première signature de méthode (c'est-à-dire sans le paramètre comparisonType
) est en fait la même que l'utilisation de l'opérateur ==
, mais a l'avantage d'être explicitement appliquée aux chaînes. Il effectue une comparaison ordinale des chaînes, qui est essentiellement une comparaison octet par octet. Dans de nombreux cas, c'est exactement le type de comparaison que vous souhaitez, en particulier lorsque vous comparez des chaînes dont les valeurs sont définies par programme, telles que des noms de fichiers, des variables d'environnement, des attributs, etc. Dans ces cas, tant qu'une comparaison ordinale est en effet le bon type de comparaison pour cette situation, le seul inconvénient de l'utilisation de la méthode Equals
sans un type de comparisonType
est que quelqu'un qui lit le code peut ne pas savoir quel type de comparaison vous faites.
Cependant, l'utilisation de la signature de la méthode Equals
qui inclut un type de comparisonType
chaque fois que vous comparez des chaînes rendra non seulement votre code plus clair, mais vous fera également réfléchir explicitement au type de comparaison que vous devez effectuer. C'est une chose intéressante à faire, car même si l'anglais ne fournit pas beaucoup de différences entre les comparaisons ordinales et sensibles à la culture, d'autres langues en fournissent beaucoup, et ignorer la possibilité d'autres langues vous ouvre beaucoup de potentiel pour erreurs sur la route. Par exemple:
string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
La pratique la plus sûre consiste à toujours fournir un paramètre comparisonType
à la méthode Equals
. Voici quelques directives de base :
- Lorsque vous comparez des chaînes qui ont été saisies par l'utilisateur ou qui doivent être affichées pour l'utilisateur, utilisez une comparaison dépendante de la culture (
CurrentCulture
ouCurrentCultureIgnoreCase
). - Lorsque vous comparez des chaînes de programmation, utilisez la comparaison ordinale (
Ordinal
ouOrdinalIgnoreCase
). -
InvariantCulture
etInvariantCultureIgnoreCase
ne doivent généralement pas être utilisés, sauf dans des circonstances très limitées, car les comparaisons ordinales sont plus efficaces. Si une comparaison tenant compte de la culture est nécessaire, elle doit généralement être effectuée par rapport à la culture actuelle ou à une autre culture spécifique.
En plus de la méthode Equals
, les chaînes fournissent également la méthode Compare
, qui vous donne des informations sur l'ordre relatif des chaînes au lieu d'un simple test d'égalité. Cette méthode est préférable aux opérateurs <
, <=
, >
et >=
, pour les mêmes raisons que celles évoquées ci-dessus, afin d'éviter les problèmes C#.
Erreur courante de programmation C # # 4: utiliser des instructions itératives (au lieu de déclaratives) pour manipuler des collections
Dans C# 3.0, l'ajout de LINQ (Language-Integrated Query) au langage a changé à jamais la façon dont les collections sont interrogées et manipulées. Depuis lors, si vous utilisez des instructions itératives pour manipuler des collections, vous n'avez pas utilisé LINQ alors que vous auriez probablement dû le faire.
Certains programmeurs C # ne connaissent même pas l'existence de LINQ, mais heureusement, ce nombre devient de plus en plus petit. Cependant, beaucoup pensent encore qu'en raison de la similitude entre les mots-clés LINQ et les instructions SQL, sa seule utilisation est dans le code qui interroge les bases de données.
Bien que l'interrogation de la base de données soit une utilisation très répandue des instructions LINQ, elles fonctionnent en fait sur n'importe quelle collection énumérable (c'est-à-dire, tout objet qui implémente l'interface IEnumerable). Ainsi, par exemple, si vous aviez un tableau de comptes, au lieu d'écrire une liste C # foreach :
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
tu pourrais juste écrire :
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Bien qu'il s'agisse d'un exemple assez simple de la façon d'éviter ce problème de programmation C # courant, il existe des cas où une seule instruction LINQ peut facilement remplacer des dizaines d'instructions dans une boucle itérative (ou des boucles imbriquées) dans votre code. Et moins de code général signifie moins de possibilités d'introduction de bogues. Gardez à l'esprit, cependant, qu'il peut y avoir un compromis en termes de performances. Dans les scénarios critiques pour les performances, en particulier lorsque votre code itératif est capable de faire des hypothèses sur votre collection que LINQ ne peut pas, assurez-vous de faire une comparaison des performances entre les deux méthodes.
Erreur courante de programmation C # n ° 5 : ne pas tenir compte des objets sous-jacents dans une instruction LINQ
LINQ est idéal pour résumer la tâche de manipulation des collections, qu'il s'agisse d'objets en mémoire, de tables de base de données ou de documents XML. Dans un monde parfait, vous n'auriez pas besoin de savoir quels sont les objets sous-jacents. Mais l'erreur ici est de supposer que nous vivons dans un monde parfait. En fait, des instructions LINQ identiques peuvent renvoyer des résultats différents lorsqu'elles sont exécutées sur exactement les mêmes données, si ces données se trouvent dans un format différent.
Par exemple, considérez l'énoncé suivant :
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Que se passe-t-il si l'un des account.Status
de l'objet est égal à « Actif » (notez le A majuscule) ? Eh bien, si myAccounts
était un objet DbSet
(qui a été configuré avec la configuration par défaut insensible à la casse), l'expression where
correspondrait toujours à cet élément. Cependant, si myAccounts
se trouvait dans un tableau en mémoire, il ne correspondrait pas et donnerait donc un résultat différent pour total.
Mais attendez une minute. Lorsque nous avons parlé de comparaison de chaînes plus tôt, nous avons vu que l'opérateur ==
effectuait une comparaison ordinale de chaînes. Alors pourquoi dans ce cas l'opérateur ==
effectue-t-il une comparaison insensible à la casse ?
La réponse est que lorsque les objets sous-jacents dans une instruction LINQ sont des références à des données de table SQL (comme c'est le cas avec l'objet Entity Framework DbSet dans cet exemple), l'instruction est convertie en une instruction T-SQL. Les opérateurs suivent alors les règles de programmation T-SQL, et non les règles de programmation C#, de sorte que la comparaison dans le cas ci-dessus finit par être insensible à la casse.
En général, même si LINQ est un moyen utile et cohérent d'interroger des collections d'objets, en réalité, vous devez toujours savoir si votre instruction sera ou non traduite en autre chose que C # sous le capot pour vous assurer que le comportement de votre code sera être comme prévu lors de l'exécution.
Erreur courante de programmation C # # 6: Être confus ou truqué par les méthodes d'extension
Comme mentionné précédemment, les instructions LINQ fonctionnent sur tout objet qui implémente IEnumerable. Par exemple, la fonction simple suivante additionnera les soldes de n'importe quel ensemble de comptes :
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
Dans le code ci-dessus, le type du paramètre myAccounts est déclaré comme IEnumerable<Account>
. Étant donné que myAccounts
référence à une méthode Sum
(C# utilise la "notation par points" familière pour référencer une méthode sur une classe ou une interface), nous nous attendons à voir une méthode appelée Sum()
sur la définition de l'interface IEnumerable<T>
. Cependant, la définition de IEnumerable<T>
ne fait référence à aucune méthode Sum
et ressemble simplement à ceci :
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Alors, où est définie la méthode Sum()
? C# est fortement typé, donc si la référence à la méthode Sum
n'était pas valide, le compilateur C# la signalerait certainement comme une erreur. On sait donc qu'il doit exister, mais où ? De plus, où sont les définitions de toutes les autres méthodes fournies par LINQ pour interroger ou agréger ces collections ?
La réponse est que Sum()
n'est pas une méthode définie sur l'interface IEnumerable
. Il s'agit plutôt d'une méthode statique (appelée « méthode d'extension ») qui est définie sur la classe System.Linq.Enumerable
:
namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }
Alors, qu'est-ce qui différencie une méthode d'extension de toute autre méthode statique et qu'est-ce qui nous permet d'y accéder dans d'autres classes ?
La caractéristique distinctive d'une méthode d'extension est le modificateur this
sur son premier paramètre. C'est la « magie » qui l'identifie au compilateur comme une méthode d'extension. Le type du paramètre qu'il modifie (dans ce cas IEnumerable<TSource>
) désigne la classe ou l'interface qui apparaîtra alors pour implémenter cette méthode.
(En passant, il n'y a rien de magique dans la similitude entre le nom de l'interface IEnumerable
et le nom de la classe Enumerable
sur laquelle la méthode d'extension est définie. Cette similitude n'est qu'un choix stylistique arbitraire.)
Avec cette compréhension, nous pouvons également voir que la fonction sumAccounts
que nous avons introduite ci-dessus aurait pu être implémentée comme suit :
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
Le fait que nous aurions pu l'implémenter de cette manière soulève plutôt la question de savoir pourquoi avoir des méthodes d'extension ? Les méthodes d'extension sont essentiellement une commodité du langage de programmation C# qui vous permet « d'ajouter » des méthodes à des types existants sans créer un nouveau type dérivé, recompiler ou modifier le type d'origine.

Les méthodes d'extension sont introduites dans la portée en incluant un using [namespace];
déclaration en haut du fichier. Vous devez savoir quel espace de noms C# inclut les méthodes d'extension que vous recherchez, mais c'est assez facile à déterminer une fois que vous savez ce que vous recherchez.
Lorsque le compilateur C# rencontre un appel de méthode sur une instance d'un objet et ne trouve pas cette méthode définie sur la classe d'objet référencée, il examine ensuite toutes les méthodes d'extension qui sont dans la portée pour essayer d'en trouver une qui correspond à la méthode requise Signature et classe. S'il en trouve un, il passera la référence d'instance comme premier argument à cette méthode d'extension, puis le reste des arguments, s'il y en a, seront passés comme arguments suivants à la méthode d'extension. (Si le compilateur C# ne trouve aucune méthode d'extension correspondante dans la portée, il génère une erreur.)
Les méthodes d'extension sont un exemple de "sucre syntaxique" de la part du compilateur C#, qui nous permet d'écrire du code qui est (généralement) plus clair et plus maintenable. Plus clair, c'est-à-dire si vous êtes au courant de leur utilisation. Sinon, cela peut être un peu déroutant, surtout au début.
Bien qu'il y ait certainement des avantages à utiliser des méthodes d'extension, elles peuvent causer des problèmes et demander de l'aide à la programmation C # pour les développeurs qui ne les connaissent pas ou ne les comprennent pas correctement. Cela est particulièrement vrai lorsque vous consultez des exemples de code en ligne ou tout autre code pré-écrit. Lorsqu'un tel code produit des erreurs de compilation (parce qu'il invoque des méthodes qui ne sont clairement pas définies sur les classes sur lesquelles elles sont invoquées), la tendance est de penser que le code s'applique à une version différente de la bibliothèque, ou à une bibliothèque complètement différente. Beaucoup de temps peut être consacré à la recherche d'une nouvelle version, ou d'une "bibliothèque manquante" fantôme, qui n'existe pas.
Même les développeurs qui sont familiers avec les méthodes d'extension se font parfois prendre, lorsqu'il existe une méthode portant le même nom sur l'objet, mais que sa signature de méthode diffère de manière subtile de celle de la méthode d'extension. Beaucoup de temps peut être perdu à chercher une faute de frappe ou une erreur qui n'existe tout simplement pas.
L'utilisation de méthodes d'extension dans les bibliothèques C# devient de plus en plus répandue. En plus de LINQ, Unity Application Block et le framework API Web sont des exemples de deux bibliothèques modernes très utilisées par Microsoft qui utilisent également des méthodes d'extension, et il y en a beaucoup d'autres. Plus le cadre est moderne, plus il est probable qu'il intégrera des méthodes d'extension.
Bien sûr, vous pouvez également écrire vos propres méthodes d'extension. Sachez cependant que même si les méthodes d'extension semblent être invoquées comme les méthodes d'instance régulières, ce n'est en réalité qu'une illusion. En particulier, vos méthodes d'extension ne peuvent pas référencer les membres privés ou protégés de la classe qu'elles étendent et ne peuvent donc pas remplacer complètement l'héritage de classe plus traditionnel.
Erreur courante de programmation C # # 7: utiliser le mauvais type de collection pour la tâche à accomplir
C# fournit une grande variété d'objets de collection, les suivants n'étant qu'une liste partielle :
Array
, ArrayList
, BitArray
, BitVector32
, Dictionary<K,V>
, HashTable
, HybridDictionary
, List<T>
, NameValueCollection
, OrderedDictionary
, Queue, Queue<T>
, SortedList
, Stack, Stack<T>
, StringCollection
, StringDictionary
.
Bien qu'il puisse y avoir des cas où trop de choix est aussi mauvais que pas assez de choix, ce n'est pas le cas avec les objets de collection. Le nombre d'options disponibles peut certainement fonctionner à votre avantage. Prenez un peu plus de temps pour rechercher et choisir le type de collecte optimal pour votre objectif. Cela se traduira probablement par de meilleures performances et moins de marge d'erreur.
S'il existe un type de collection spécifiquement ciblé sur le type d'élément que vous avez (comme une chaîne ou un bit), penchez-vous vers celui-ci en premier. L'implémentation est généralement plus efficace lorsqu'elle est ciblée sur un type d'élément spécifique.
Pour tirer parti de la sécurité des types de C #, vous devez généralement préférer une interface générique à une interface non générique. Les éléments d'une interface générique sont du type que vous spécifiez lorsque vous déclarez votre objet, alors que les éléments des interfaces non génériques sont de type objet. Lorsque vous utilisez une interface non générique, le compilateur C# ne peut pas vérifier le type de votre code. De plus, lorsqu'il s'agit de collections de types de valeur primitifs, l'utilisation d'une collection non générique entraînera un boxing/unboxing répété de ces types, ce qui peut entraîner un impact négatif significatif sur les performances par rapport à une collection générique du type approprié.
Un autre problème courant en C# consiste à écrire votre propre objet de collection. Cela ne veut pas dire que ce n'est jamais approprié, mais avec une sélection aussi complète que celle offerte par .NET, vous pouvez probablement gagner beaucoup de temps en utilisant ou en étendant celui qui existe déjà, plutôt que de réinventer la roue. En particulier, la bibliothèque de collections génériques C5 pour C # et CLI offre un large éventail de collections supplémentaires "prêtes à l'emploi", telles que des structures de données arborescentes persistantes, des files d'attente prioritaires basées sur le tas, des listes de tableaux indexés par hachage, des listes liées, et bien plus encore.
Erreur courante de programmation C# #8 : Négliger de libérer des ressources
L'environnement CLR utilise un ramasse-miettes, vous n'avez donc pas besoin de libérer explicitement la mémoire créée pour un objet. En fait, vous ne pouvez pas. Il n'y a pas d'équivalent de l'opérateur de delete
C++ ou de la fonction free()
en C . Mais cela ne signifie pas que vous pouvez simplement oublier tous les objets une fois que vous avez fini de les utiliser. De nombreux types d'objets encapsulent d'autres types de ressources système (par exemple, un fichier disque, une connexion à une base de données, un socket réseau, etc.). Laisser ces ressources ouvertes peut rapidement épuiser le nombre total de ressources système, dégrader les performances et finalement conduire à des erreurs de programme.
Bien qu'une méthode de destructeur puisse être définie sur n'importe quelle classe C#, le problème avec les destructeurs (également appelés finaliseurs en C#) est que vous ne pouvez pas savoir avec certitude quand ils seront appelés. Ils sont appelés par le ramasse-miettes (sur un thread séparé, ce qui peut entraîner des complications supplémentaires) à un moment indéterminé dans le futur. Essayer de contourner ces limitations en forçant la récupération de place avec GC.Collect()
n'est pas une bonne pratique C#, car cela bloquera le thread pendant une durée inconnue pendant qu'il collecte tous les objets éligibles à la collecte.
Cela ne veut pas dire qu'il n'y a pas de bonnes utilisations pour les finaliseurs, mais la libération de ressources de manière déterministe n'en fait pas partie. Au lieu de cela, lorsque vous travaillez sur une connexion de fichier, de réseau ou de base de données, vous souhaitez explicitement libérer la ressource sous-jacente dès que vous en avez terminé.
Les fuites de ressources sont une préoccupation dans presque tous les environnements. Cependant, C # fournit un mécanisme robuste et simple à utiliser qui, s'il est utilisé, peut rendre les fuites beaucoup plus rares. Le framework .NET définit l'interface IDisposable
, qui se compose uniquement de la méthode Dispose()
. Tout objet qui implémente IDisposable
s'attend à ce que cette méthode soit appelée chaque fois que le consommateur de l'objet a fini de le manipuler. Il en résulte une libération explicite et déterministe des ressources.
Si vous créez et supprimez un objet dans le contexte d'un seul bloc de code, il est fondamentalement inexcusable d'oublier d'appeler Dispose()
, car C# fournit une instruction using
qui garantira que Dispose()
est appelé quelle que soit la façon dont le bloc de code est quitté (qu'il s'agisse d'une exception, d'une instruction de retour ou simplement de la fermeture du bloc). Et oui, c'est la même instruction using
mentionnée précédemment qui est utilisée pour inclure les espaces de noms C# en haut de votre fichier. Il a un deuxième objectif, totalement indépendant, dont de nombreux développeurs C# ne sont pas conscients ; à savoir, pour s'assurer que Dispose()
est appelé sur un objet lorsque le bloc de code est quitté :
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
En créant un bloc using
dans l'exemple ci-dessus, vous savez avec certitude que myFile.Dispose()
sera appelé dès que vous en aurez terminé avec le fichier, que Read()
lève ou non une exception.
Erreur courante de programmation C # # 9: évitez les exceptions
C # continue son application de la sécurité de type dans le runtime. Cela vous permet d'identifier de nombreux types d'erreurs en C# beaucoup plus rapidement que dans des langages tels que C++, où des conversions de type erronées peuvent entraîner l'attribution de valeurs arbitraires aux champs d'un objet. Cependant, encore une fois, les programmeurs peuvent gaspiller cette fonctionnalité intéressante, ce qui entraîne des problèmes C#. Ils tombent dans ce piège car C# fournit deux manières différentes de faire les choses, une qui peut lever une exception et une qui ne le fera pas. Certains se détourneront de la route d'exception, pensant que ne pas avoir à écrire un bloc try/catch leur permet d'économiser du codage.
Par exemple, voici deux manières différentes d'effectuer un cast de type explicite en C# :
// METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;
L'erreur la plus évidente qui pourrait se produire avec l'utilisation de la méthode 2 serait un échec de vérification de la valeur de retour. Cela entraînerait probablement une éventuelle NullReferenceException, qui pourrait éventuellement apparaître beaucoup plus tard, ce qui rendrait beaucoup plus difficile la recherche de la source du problème. En revanche, la méthode 1 aurait immédiatement levé une InvalidCastException
rendant la source du problème beaucoup plus immédiatement évidente.
De plus, même si vous vous souvenez de vérifier la valeur de retour dans la méthode 2, qu'allez-vous faire si vous trouvez qu'elle est nulle ? La méthode que vous écrivez est-elle un endroit approprié pour signaler une erreur ? Y a-t-il autre chose que vous pouvez essayer si ce casting échoue ? Si ce n'est pas le cas, lancer une exception est la bonne chose à faire, vous pouvez donc aussi bien la laisser se produire aussi près que possible de la source du problème.
Voici quelques exemples d'autres paires de méthodes courantes où l'une lève une exception et l'autre non :
int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty
Certains développeurs C# sont tellement "opposés aux exceptions" qu'ils supposent automatiquement que la méthode qui ne lève pas d'exception est supérieure. Bien qu'il existe certains cas où cela peut être vrai, ce n'est pas du tout correct en tant que généralisation.
Comme exemple spécifique, dans un cas où vous avez une autre action légitime (par exemple, par défaut) à prendre si une exception aurait été générée, alors l'approche sans exception pourrait être un choix légitime. Dans un tel cas, il peut en effet être préférable d'écrire quelque chose comme ceci :
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }
au lieu de:
try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
Cependant, il est incorrect de supposer que TryParse
est donc nécessairement la « meilleure » méthode. Parfois c'est le cas, parfois non. C'est pourquoi il y a deux manières de procéder. Utilisez la bonne pour le contexte dans lequel vous vous trouvez, en vous rappelant que les exceptions peuvent certainement être votre amie en tant que développeur.
Erreur courante de programmation C # # 10: permettre aux avertissements du compilateur de s'accumuler
Bien que ce problème ne soit certainement pas spécifique à C #, il est particulièrement flagrant dans la programmation C # car il abandonne les avantages de la vérification de type stricte offerte par le compilateur C #.
Les avertissements sont générés pour une raison. Bien que toutes les erreurs du compilateur C# signifient un défaut dans votre code, de nombreux avertissements le sont également. Ce qui différencie les deux est que, dans le cas d'un avertissement, le compilateur n'a aucun problème à émettre les instructions que votre code représente. Même ainsi, il trouve votre code un peu louche, et il y a une probabilité raisonnable que votre code ne reflète pas précisément votre intention.
Un exemple simple et courant pour les besoins de ce didacticiel de programmation C # est lorsque vous modifiez votre algorithme pour éliminer l'utilisation d'une variable que vous utilisiez, mais que vous oubliez de supprimer la déclaration de variable. Le programme fonctionnera parfaitement, mais le compilateur signalera la déclaration de variable inutile. Le fait que le programme fonctionne parfaitement amène les programmeurs à négliger de corriger la cause de l'avertissement. De plus, les codeurs profitent d'une fonctionnalité de Visual Studio qui leur permet de masquer facilement les avertissements dans la fenêtre "Liste d'erreurs" afin qu'ils puissent se concentrer uniquement sur les erreurs. Il ne faut pas longtemps avant qu'il y ait des dizaines d'avertissements, tous parfaitement ignorés (ou pire encore, cachés).
Mais si vous ignorez ce type d'avertissement, tôt ou tard, quelque chose comme ça pourrait très bien se retrouver dans votre code :
class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }
Et à la vitesse à laquelle Intellisense nous permet d'écrire du code, cette erreur n'est pas aussi improbable qu'elle en a l'air.
Vous avez maintenant une grave erreur dans votre programme (bien que le compilateur ne l'ait signalée que comme un avertissement, pour les raisons déjà expliquées), et selon la complexité de votre programme, vous pourriez perdre beaucoup de temps à le retrouver. Si vous aviez prêté attention à cet avertissement en premier lieu, vous auriez évité ce problème avec une simple solution de cinq secondes.
N'oubliez pas que le compilateur C Sharp vous donne beaucoup d'informations utiles sur la robustesse de votre code… si vous écoutez. N'ignorez pas les avertissements. Ils ne prennent généralement que quelques secondes à réparer, et en réparer de nouveaux lorsqu'ils se produisent peut vous faire gagner des heures. Entraînez-vous à vous attendre à ce que la fenêtre "Liste d'erreurs" de Visual Studio affiche "0 erreur, 0 avertissement", de sorte que tout avertissement vous rende suffisamment mal à l'aise pour y répondre immédiatement.
Bien sûr, il existe des exceptions à chaque règle. En conséquence, il peut arriver que votre code semble un peu louche pour le compilateur, même s'il correspond exactement à ce que vous vouliez qu'il soit. Dans ces cas très rares, utilisez #pragma warning disable [warning id]
uniquement autour du code qui déclenche l'avertissement, et uniquement pour l'ID d'avertissement qu'il déclenche. Cela supprimera cet avertissement, et cet avertissement uniquement, afin que vous puissiez toujours rester attentif aux nouveaux.
Emballer
C# est un langage puissant et flexible avec de nombreux mécanismes et paradigmes qui peuvent grandement améliorer la productivité. Comme pour tout outil logiciel ou langage, cependant, avoir une compréhension ou une appréciation limitée de ses capacités peut parfois être plus un obstacle qu'un avantage, laissant quelqu'un dans l'état proverbial de "savoir assez pour être dangereux".
L'utilisation d'un didacticiel C Sharp comme celui-ci pour se familiariser avec les nuances clés de C #, telles que (mais sans s'y limiter) les problèmes soulevés dans cet article, aidera à l'optimisation de C # tout en évitant certains de ses pièges les plus courants. Langue.
Lectures complémentaires sur le blog Toptal Engineering :
- Questions d'entretien essentielles sur C#
- C# contre C++ : qu'y a-t-il au cœur ?