C # divertissant. Cinq exemples de pauses café

Ayant déjà écrit plus d'un article sur Veeam Academy , nous avons décidé d'ouvrir une petite cuisine interne et de vous proposer quelques exemples en C # que nous analysons avec nos étudiants. Lors de leur compilation, nous avons été guidés par le fait que notre public est des développeurs débutants, mais il peut également être intéressant pour les programmeurs expérimentés de regarder sous le chat. Notre objectif est de montrer la profondeur du trou du lapin, tout en expliquant les caractéristiques de la structure interne de C #.

D'un autre côté, nous serons heureux d'entendre les commentaires de collègues expérimentés qui souligneront les défauts de nos exemples ou partageront les leurs. Ils aiment utiliser ces questions lors des entretiens, alors nous avons certainement tous quelque chose à dire.

Nous espérons que notre sélection vous sera utile, vous aidera à rafraîchir vos connaissances ou tout simplement à sourire.

image

Exemple 1


Structures en C #. Avec eux, même les développeurs expérimentés ont souvent des questions, qui sont si souvent utilisées par toutes sortes de tests en ligne.

Notre premier exemple est un exemple de pleine conscience et de connaissance de ce dans quoi le bloc d'utilisation se développe. Et aussi tout à fait un sujet de communication lors de l'entretien.

Considérez le code:

public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } static void Main(string[] args) { var d = new SDummy(); using (d) { Console.WriteLine(d.GetDispose()); } Console.WriteLine(d.GetDispose()); } } 

Qu'imprimera la méthode Main sur la console?
Notez que SDummy est une structure qui implémente l'interface IDisposable, de sorte que les variables de type SDummy peuvent être utilisées dans le bloc using.

Selon la spécification du langage C #, l' utilisation de l'instruction pour les types significatifs au moment de la compilation se développe en un bloc try-finally:

  try { Console.WriteLine(d.GetDispose()); } finally { ((IDisposable)d).Dispose(); } 

Ainsi, dans notre code, la méthode GetDispose () est appelée à l'intérieur du bloc using, qui renvoie le champ booléen _dispose, dont la valeur n'a pas encore été définie pour l'objet d (elle est définie uniquement dans la méthode Dispose (), qui n'a pas encore été appelée) et donc la valeur est renvoyée La valeur par défaut est False. Et ensuite?

Et puis le plus intéressant.
Exécuter une ligne dans un bloc enfin
  ((IDisposable)d).Dispose(); 

mène normalement à la boxe. Ce n'est pas difficile à voir, par exemple, ici (en haut à droite dans Résultats, sélectionnez d'abord C #, puis IL):

image

Dans ce cas, la méthode Dispose est déjà appelée pour un autre objet, et pas du tout pour l'objet d.
Exécutez notre programme et voyez que le programme affiche vraiment «False False» sur la console. Mais est-ce aussi simple que cela? :)

En fait, aucun emballage ne se produit. Ce qui, selon Eric Lippert, est fait dans un souci d'optimisation (voir ici et ici ).
Mais, s'il n'y a pas d'emballage (ce qui en soi peut sembler surprenant), pourquoi «False False» et non «False True» à l'écran, car Dispose devrait maintenant être appliqué au même objet?!?

Et là pas pour ça!
Jetez un œil à ce que le compilateur C # étend notre programme en:

 public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } private static void Main(string[] args) { SDummy sDummy = default(SDummy); SDummy sDummy2 = sDummy; try { Console.WriteLine(sDummy.GetDispose()); } finally { ((IDisposable)sDummy2).Dispose(); } Console.WriteLine(sDummy.GetDispose()); } } 


Il existe une nouvelle variable sDummy2, à laquelle la méthode Dispose () est appliquée!
D'où vient cette variable cachée?
Passons à nouveau aux spécifications :
Une instruction using de la forme «instruction using (expression)» a les trois mêmes extensions possibles. Dans ce cas, ResourceType est implicitement le type à la compilation de l'expression ... La variable 'resource' est inaccessible et invisible à l'instruction incorporée.

T.O. la variable sDummy est invisible et inaccessible à l'instruction incorporée du bloc using, et toutes les opérations à l'intérieur de cette expression sont effectuées avec une autre variable sDummy2.

Par conséquent, la méthode Main renvoie à la console «False False», et non «False True», comme le croient ceux qui ont rencontré cet exemple pour la première fois. Dans ce cas, n'oubliez pas qu'il n'y a pas de packaging, mais une variable cachée supplémentaire est créée.

La conclusion générale est la suivante: les types de valeurs mutables sont mauvais, il vaut mieux les éviter.

Un exemple similaire est considéré ici . Si le sujet est intéressant, nous vous recommandons fortement de jeter un œil.

Je voudrais remercier tout spécialement SergeyT pour ses précieux commentaires sur cet exemple.



Exemple 2


Les constructeurs et la séquence de leurs appels est l'un des principaux sujets de tout langage de programmation orienté objet. Parfois, une telle séquence d'appels peut surprendre et, pire encore, «remplir» le programme au moment le plus inattendu.

Considérez donc la classe MyLogger:

 class MyLogger { static MyLogger innerInstance = new MyLogger(); static MyLogger() { Console.WriteLine("Static Logger Constructor"); } private MyLogger() { Console.WriteLine("Instance Logger Constructor"); } public static MyLogger Instance { get { return innerInstance; } } } 

Supposons que cette classe possède une logique métier dont nous avons besoin pour prendre en charge la journalisation (la fonctionnalité n'est pas si importante en ce moment).

Voyons ce qu'il y a dans notre classe MyLogger:

  1. Constructeur statique spécifié
  2. Il existe un constructeur privé sans paramètres
  3. Variable statique fermée innerInstance définie
  4. Et il existe une propriété statique ouverte d'Instance pour communiquer avec le monde extérieur

Pour faciliter l'analyse de cet exemple, nous avons ajouté une sortie de console simple aux constructeurs de la classe.

En dehors de la classe (sans utiliser des astuces comme la réflexion), nous ne pouvons utiliser que la propriété publique statique Instance, que nous pouvons appeler comme ceci:

 class Program { public static void Main() { var logger = MyLogger.Instance; } } 

Que produira ce programme?
Nous savons tous qu'un constructeur statique est appelé avant d'accéder à n'importe quel membre de la classe (à l'exception des constantes). Dans ce cas, il n'est lancé qu'une seule fois dans le domaine d'application.

Dans notre cas, nous nous tournons vers le membre de la classe - la propriété Instance, qui devrait provoquer le démarrage du constructeur statique en premier, puis le constructeur de l'instance de classe sera appelé. C'est-à-dire le programme affichera:

Constructeur d'enregistreurs statiques
Constructeur de l'enregistreur d'instance


Cependant, après avoir démarré le programme, nous obtenons sur la console:

Constructeur de l'enregistreur d'instance
Constructeur d'enregistreurs statiques


Comment ça? Le constructeur d'instance a fonctionné avant le constructeur statique?!?
Réponse: oui!

Et voici pourquoi.

La norme C # ECMA-334 stipule ce qui suit pour les classes statiques:

17.4.5.1: «Si un constructeur statique (§17.11) existe dans la classe, l'exécution des initialiseurs de champ statique se produit immédiatement avant d'exécuter ce constructeur statique.
...
17.11: ... Si une classe contient des champs statiques avec des initialiseurs, ces initialiseurs sont exécutés dans l'ordre textuel immédiatement avant d'exécuter le constructeur statique

(Ce qui dans une traduction libre signifie: s'il y a un constructeur statique dans la classe, alors l'initialisation des champs statiques commence immédiatement AVANT le démarrage du constructeur statique.
...
Si la classe contient des champs statiques avec des initialiseurs, ces initialiseurs sont lancés dans l'ordre dans le texte du programme AVANT l'exécution du constructeur statique.)

Dans notre cas, le champ statique innerInstance est déclaré avec l'initialiseur, qui est le constructeur de l'instance de classe. Selon la norme ECMA, l'initialiseur doit être appelé AVANT d'appeler le constructeur statique. Ce qui se passe dans notre programme: le constructeur d'instance, étant l'initialiseur du champ statique, est appelé AVANT le constructeur statique. D'accord, de façon assez inattendue.

Notez que cela n'est vrai que pour les initialiseurs de champs statiques. En général, un constructeur statique est appelé AVANT d'appeler le constructeur de l'instance de classe.

Comme, par exemple, ici:

 class MyLogger { static MyLogger() { Console.WriteLine("Static Logger Constructor"); } public MyLogger() { Console.WriteLine("Instance Logger Constructor"); } } class Program { public static void Main() { var logger = new MyLogger(); } } 

Et le programme devrait sortir sur la console:

Constructeur d'enregistreurs statiques
Constructeur de l'enregistreur d'instance


image

Exemple 3


Les programmeurs doivent souvent écrire des fonctions auxiliaires (utilitaires, aides, etc.) pour leur faciliter la vie. En règle générale, ces fonctions sont assez simples et ne prennent souvent que quelques lignes de code. Mais vous pouvez trébucher même à l'improviste.

Supposons que nous devons implémenter une fonction qui vérifie l'étrangeté du nombre (c'est-à-dire que le nombre n'est pas divisible par 2 sans reste).

Une implémentation pourrait ressembler à ceci:

 static bool isOddNumber(int i) { return (i % 2 == 1); } 

À première vue, tout va bien et, par exemple, pour les nombres 5.7 et 11, nous nous attendons vraisemblablement à True.

Que renverra la fonction isOddNumber (-5)?
-5 est un nombre impair, mais en réponse à notre fonction, nous obtenons Faux!
Voyons quelle est la raison.

Selon MSDN , ce qui suit est écrit sur le reste de l'opérateur de division%:
"Pour les opérandes entiers, le résultat de% b est la valeur produite par a - (a / b) * b"
Dans notre cas, pour a = -5, b = 2 on obtient:
-5% 2 = (-5) - ((-5) / 2) * 2 = -5 + 4 = -1
Mais -1 n'est pas toujours égal à 1, ce qui explique notre résultat Faux.

L'opérateur% est sensible au signe des opérandes. Par conséquent, afin de ne pas recevoir de telles «surprises», il est préférable de comparer le résultat avec zéro, qui n'a aucun signe:

 static bool isOddNumber(int i) { return (i % 2 != 0); } 

Ou obtenez une fonction distincte pour vérifier la parité et implémenter la logique à travers elle:

 static bool isEvenNumber(int i) { return (i % 2 == 0); } static bool isOddNumber(int i) { return !isEvenNumber(i); } 


Exemple 4


Tous ceux qui ont programmé en C # ont probablement rencontré LINQ, ce qui est si pratique pour travailler avec des collections, créer des requêtes, filtrer et agréger des données ...

Nous ne regarderons pas sous le capot de LINQ. Peut-être que nous le ferons une autre fois.

En attendant, considérons un petit exemple:

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; var selectedData = dataArray.Select( x => { summResult += x; return x; }); Console.WriteLine(summResult); 

Que produira ce code?
Nous obtenons à l'écran la valeur de la variable summResult, qui est égale à la valeur initiale, c'est-à-dire 0.

Pourquoi est-ce arrivé?

Et parce que la définition d'une requête LINQ et le lancement de cette requête sont deux opérations qui sont effectuées séparément. Ainsi, la définition d'une requête ne signifie pas son lancement / exécution.

La variable summResult est utilisée à l'intérieur d'un délégué anonyme dans la méthode Select: les éléments du tableau dataArray sont triés séquentiellement et ajoutés à la variable summResult.

Nous pouvons supposer que notre code imprimera la somme des éléments du tableau dataArray. Mais LINQ ne fonctionne pas de cette façon.

Considérez la variable selectedData. Le mot clé var est «sucre syntaxique», ce qui dans de nombreux cas réduit la taille du code du programme et améliore sa lisibilité. Et le type réel de la variable selectedData implémente l'interface IEnumerable. C'est-à-dire notre code ressemble à ceci:

  IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; }); 

Ici, nous définissons la requête (Query), mais la requête elle-même ne démarre pas. De la même manière, vous pouvez travailler avec la base de données en spécifiant la requête SQL sous forme de chaîne, mais pour obtenir le résultat, reportez-vous à la base de données et exécutez cette requête de manière explicite.

Autrement dit, jusqu'à présent, nous avons seulement déposé une demande, mais nous ne l'avons pas lancée. C'est pourquoi la valeur de la variable summResult reste inchangée. Une requête peut être lancée, par exemple, à l'aide des méthodes ToArray, ToList ou ToDictionary:

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; //        selectedData IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; }); //   selectedData selectedData.ToArray(); //    summResult Console.WriteLine(summResult); 

Ce code affichera déjà la valeur de la variable summResult, égale à la somme de tous les éléments du tableau dataArray, égale à 15.

Nous l'avons compris. Et puis qu'est-ce que ce programme affichera à l'écran?

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; //1 var summResult = dataArray.Sum() + dataArray.Skip(3).Take(2).Sum(); //2 var groupedData = dataArray.GroupBy(x => x).Select( //3 x => { summResult += x.Key; return x.Key; }); Console.WriteLine(summResult); //4 

La variable groupedData (ligne 3) implémente en fait l'interface IEnumerable et définit essentiellement la demande à la source de données dataArray. Cela signifie que pour qu'un délégué anonyme fonctionne, ce qui modifie la valeur de la variable summResult, cette demande doit être exécutée explicitement. Mais il n'y a pas un tel lancement dans notre programme. Par conséquent, la valeur de la variable summResult ne sera modifiée qu'à la ligne 2, et nous ne pouvons pas prendre en compte tout le reste dans nos calculs.

Ensuite, il est facile de calculer la valeur de la variable summResult, qui est, respectivement, 15 + 7, c'est-à-dire 22.

Exemple 5


Disons tout de suite - nous ne considérons pas cet exemple lors de nos conférences à l'Académie, mais parfois nous en discutons pendant les pauses café plutôt comme une blague.

Malgré le fait qu'il ne soit guère indicatif du point de vue de la détermination du niveau du développeur, nous avons rencontré cet exemple dans plusieurs tests différents. Peut-être qu'il est utilisé pour la polyvalence, car il fonctionne de la même manière en C et C ++, ainsi qu'en C # et Java.

Qu'il y ait donc une ligne de code:

 int i = (int)+(char)-(int)+(long)-1; 

Quelle sera la valeur de la variable i?
Réponse: 1

Vous pourriez penser que l'arithmétique numérique est utilisée ici sur les tailles de chaque type en octets, car les signes «+» et «-» sont plutôt inattendus ici pour la conversion de type.

En C #, le type entier est connu pour être de 4 octets de long, 8 de long, car 2.

Il est alors facile de penser que notre ligne de code sera équivalente à l'expression arithmétique suivante:

 int i = (4)+(2)-(4)+(8)-1; 

Mais ce n'est pas le cas. Et pour confondre et diriger par un tel faux raisonnement, l'exemple peut être changé, par exemple, comme ceci:

 int i = (int)+(char)-(int)+(long)-sizeof(int); 

Les signes «+» et «-» sont utilisés dans cet exemple non pas comme des opérations arithmétiques binaires, mais comme des opérateurs unaires. Ensuite, notre ligne de code n'est qu'une séquence de conversions de type explicites mélangées à des appels à des opérations unaires, qui peuvent être écrites comme suit:

  int i = (int)( // call explicit operator int(char), ie char to int +( // call unary operator + (char)( // call explicit operator char(int), ie int to char -( // call unary operator - (int)( // call explicit operator int(long), ie long to int +( // call unary operator + (long)( // call explicit operator long(int), ie int to long -1 ) ) ) ) ) ) ); 


image

Intéressé par l'apprentissage à la Veeam Academy?


Maintenant, il y a un ensemble pour le printemps intensif sur C # à Saint-Pétersbourg, et nous invitons tout le monde à subir des tests en ligne sur le site Web de la Veeam Academy.

Le cours commence le 18 février 2019, se poursuit jusqu'à la mi-mai et sera, comme toujours, entièrement gratuit. L'inscription pour toute personne qui souhaite subir un test d'entrée est déjà disponible sur le site Web de l'Académie: academy.veeam.ru

image

Source: https://habr.com/ru/post/fr439478/


All Articles