Unterhaltsames C #. Fünf Beispiele für Kaffeepausen

Nachdem wir bereits mehr als einen Artikel über die Veeam Academy geschrieben haben , haben wir beschlossen, eine kleine interne Küche zu eröffnen und Ihnen einige Beispiele in C # anzubieten, die wir mit unseren Schülern analysieren. Bei der Zusammenstellung haben wir uns von der Tatsache leiten lassen, dass unser Publikum unerfahrene Entwickler sind, aber es kann auch für erfahrene Programmierer interessant sein, unter die Katze zu schauen. Unser Ziel ist es zu zeigen, wie tief das Kaninchenloch ist, und gleichzeitig die Merkmale der internen Struktur von C # zu erklären.

Auf der anderen Seite freuen wir uns über Kommentare von erfahrenen Kollegen, die entweder auf Mängel in unseren Beispielen hinweisen oder ihre eigenen teilen. Sie verwenden solche Fragen gerne bei Interviews, also haben wir alle sicher etwas zu erzählen.

Wir hoffen, dass unsere Auswahl für Sie nützlich ist, Ihnen hilft, Ihr Wissen aufzufrischen oder einfach nur zu lächeln.

Bild

Beispiel 1


Strukturen in C #. Mit ihnen haben selbst erfahrene Entwickler oft Fragen, die so oft von allen Arten von Online-Tests verwendet werden.

Unser erstes Beispiel ist ein Beispiel für Achtsamkeit und Wissen darüber, wohin sich der using-Block ausdehnt. Und auch ein ziemliches Thema für die Kommunikation während des Interviews.

Betrachten Sie den 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()); } } 

Was wird die Hauptmethode auf der Konsole drucken?
Beachten Sie, dass SDummy eine Struktur ist, die die IDisposable-Schnittstelle implementiert, sodass Variablen vom Typ SDummy im using-Block verwendet werden können.

Gemäß der C # -Sprachenspezifikation wird die Verwendung der Anweisung für signifikante Typen zur Kompilierungszeit zu einem Try-finally-Block erweitert:

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

In unserem Code wird die GetDispose () -Methode innerhalb des using-Blocks aufgerufen, der das Boolesche Feld _dispose zurückgibt, dessen Wert noch nicht für das d-Objekt festgelegt wurde (es wird nur in der Dispose () -Methode festgelegt, die noch nicht aufgerufen wurde), und daher wird der Wert zurückgegeben Der Standardwert ist False. Was weiter?

Und dann das interessanteste.
Eine Zeile in einem finally-Block ausführen
  ((IDisposable)d).Dispose(); 

führt normalerweise zum Boxen. Dies ist beispielsweise hier nicht schwer zu erkennen (wählen Sie oben rechts in den Ergebnissen zuerst C # und dann IL aus):

Bild

In diesem Fall wird die Dispose-Methode bereits für ein anderes Objekt und überhaupt nicht für das d-Objekt aufgerufen.
Führen Sie unser Programm aus und stellen Sie sicher, dass das Programm auf der Konsole wirklich "False False" anzeigt. Aber ist es so einfach? :) :)

In der Tat passiert keine Verpackung. Was laut Eric Lippert zur Optimierung getan wird (siehe hier und hier ).
Aber wenn es keine Verpackung gibt (was an sich überraschend erscheinen mag), warum wird auf dem Bildschirm "Falsch Falsch" und nicht "Falsch Richtig" angezeigt, da "Entsorgen" jetzt auf dasselbe Objekt angewendet werden sollte?!?

Und hier nicht dazu!
Schauen Sie sich an, worauf der C # -Compiler unser Programm erweitert:

 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()); } } 


Es gibt eine neue Variable sDummy2, auf die die Dispose () -Methode angewendet wird!
Woher kommt diese versteckte Variable?
Wenden wir uns noch einmal der Spezifikation zu :
Eine using-Anweisung der Form 'using (Ausdrucks-) Anweisung' hat die gleichen drei möglichen Erweiterungen. In diesem Fall ist ResourceType implizit der Typ zur Kompilierungszeit des Ausdrucks ... Die Variable 'resource' ist in der eingebetteten Anweisung nicht zugänglich und für sie unsichtbar.

T.O. Die sDummy-Variable ist unsichtbar und für die eingebettete Anweisung des using-Blocks nicht zugänglich. Alle Operationen in diesem Ausdruck werden mit einer anderen sDummy2-Variablen ausgeführt.

Infolgedessen gibt die Main-Methode "False False" und nicht "False True" an die Konsole aus, wie viele derjenigen, die dieses Beispiel zum ersten Mal angetroffen haben, glauben. Beachten Sie in diesem Fall, dass keine Verpackung vorhanden ist, sondern eine zusätzliche versteckte Variable erstellt wird.

Die allgemeine Schlussfolgerung lautet: Veränderliche Werttypen sind böse, die am besten vermieden werden.

Ein ähnliches Beispiel wird hier betrachtet . Wenn das Thema interessant ist, empfehlen wir einen Blick.

Ich möchte mich ganz besonders bei SergeyT für wertvolle Kommentare zu diesem Beispiel bedanken.



Beispiel 2


Konstruktoren und die Reihenfolge ihrer Aufrufe sind eines der Hauptthemen jeder objektorientierten Programmiersprache. Manchmal kann eine solche Folge von Aufrufen das Programm im unerwartetsten Moment überraschen und, noch schlimmer, sogar „ausfüllen“.

Betrachten Sie also die MyLogger-Klasse:

 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; } } } 

Angenommen, diese Klasse verfügt über eine Geschäftslogik, die wir zur Unterstützung der Protokollierung benötigen (die Funktionalität ist derzeit nicht so wichtig).

Mal sehen, was in unserer MyLogger-Klasse ist:

  1. Statischer Konstruktor angegeben
  2. Es gibt einen privaten Konstruktor ohne Parameter
  3. Geschlossene statische Variable innerInstance definiert
  4. Und es gibt eine offene statische Eigenschaft von Instance für die Kommunikation mit der Außenwelt

Um die Analyse dieses Beispiels zu vereinfachen, haben wir den Konstruktoren der Klasse eine einfache Konsolenausgabe hinzugefügt.

Außerhalb der Klasse (ohne Tricks wie Reflektion) können wir nur die öffentliche statische Instanzeigenschaft verwenden, die wir folgendermaßen aufrufen können:

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

Was wird dieses Programm ausgeben?
Wir alle wissen, dass ein statischer Konstruktor aufgerufen wird, bevor auf ein Mitglied der Klasse zugegriffen wird (mit Ausnahme von Konstanten). In diesem Fall wird es nur einmal innerhalb der Anwendungsdomäne gestartet.

In unserem Fall wenden wir uns dem Klassenmitglied zu - der Instance-Eigenschaft, die dazu führen sollte, dass der statische Konstruktor zuerst gestartet wird, und dann wird der Konstruktor der Klasseninstanz aufgerufen. Das heißt, Das Programm gibt Folgendes aus:

Statischer Logger-Konstruktor
Instanzlogger-Konstruktor


Nach dem Starten des Programms gelangen wir jedoch auf die Konsole:

Instanzlogger-Konstruktor
Statischer Logger-Konstruktor


Wie so? Instanzkonstruktor arbeitete vor dem statischen Konstruktor?!?
Antwort: Ja!

Und hier ist warum.

Der C # ECMA-334-Standard gibt für statische Klassen Folgendes an:

17.4.5.1: „Wenn in der Klasse ein statischer Konstruktor (§17.11) vorhanden ist, erfolgt die Ausführung der statischen Feldinitialisierer unmittelbar vor der Ausführung dieses statischen Konstruktors.
...
17.11: ... Wenn eine Klasse statische Felder mit Initialisierern enthält, werden diese Initialisierer unmittelbar vor der Ausführung des statischen Konstruktors in Textreihenfolge ausgeführt

(Was in einer freien Übersetzung bedeutet: Wenn die Klasse einen statischen Konstruktor enthält, beginnt die Initialisierung der statischen Felder sofort, BEVOR der statische Konstruktor startet.
...
Wenn die Klasse statische Felder mit Initialisierern enthält, werden diese Initialisierer in der Reihenfolge im Programmtext gestartet, BEVOR der statische Konstruktor ausgeführt wird.)

In unserem Fall wird das statische Feld innerInstance zusammen mit dem Initialisierer deklariert, der der Konstruktor der Klasseninstanz ist. Gemäß dem ECMA-Standard muss der Initialisierer aufgerufen werden, bevor der statische Konstruktor aufgerufen wird. Was in unserem Programm passiert: Der Instanzkonstruktor, der der Initialisierer des statischen Feldes ist, wird VOR dem statischen Konstruktor aufgerufen. Stimmen Sie ganz unerwartet zu.

Beachten Sie, dass dies nur für statische Feldinitialisierer gilt. Im Allgemeinen wird ein statischer Konstruktor aufgerufen, BEVOR der Konstruktor der Klasseninstanz aufgerufen wird.

Wie zum Beispiel hier:

 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(); } } 

Das Programm wird voraussichtlich auf der Konsole ausgegeben:

Statischer Logger-Konstruktor
Instanzlogger-Konstruktor


Bild

Beispiel 3


Programmierer müssen häufig Hilfsfunktionen (Dienstprogramme, Helfer usw.) schreiben, um ihr Leben zu erleichtern. Typischerweise sind solche Funktionen recht einfach und benötigen oft nur wenige Codezeilen. Aber Sie können sogar aus heiterem Himmel stolpern.

Angenommen, wir müssen eine Funktion implementieren, die die Zahl auf Ungerade prüft (d. H. Dass die Zahl ohne Rest nicht durch 2 teilbar ist).

Eine Implementierung könnte folgendermaßen aussehen:

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

Auf den ersten Blick ist alles in Ordnung und zum Beispiel für die Zahlen 5.7 und 11 werden wir voraussichtlich wahr.

Was gibt die Funktion isOddNumber (-5) zurück?
-5 ist eine ungerade Zahl, aber als Antwort auf unsere Funktion erhalten wir False!
Lassen Sie uns herausfinden, was der Grund ist.

Laut MSDN wird über den Rest des% Division-Operators Folgendes geschrieben:
"Für ganzzahlige Operanden ist das Ergebnis von a% b der Wert, der durch a - (a / b) * b erzeugt wird."
In unserem Fall erhalten wir für a = -5, b = 2:
-5% 2 = (-5) - ((-5) / 2) * 2 = -5 + 4 = -1
Aber -1 ist immer nicht gleich 1, was unser Ergebnis False erklärt.

Der% -Operator reagiert empfindlich auf das Vorzeichen von Operanden. Um solche „Überraschungen“ nicht zu erhalten, ist es daher besser, das Ergebnis mit Null zu vergleichen, die kein Vorzeichen hat:

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

Oder Sie erhalten eine separate Funktion zum Überprüfen der Parität und zum Implementieren der Logik:

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


Beispiel 4


Jeder, der in C # programmiert hat, hat sich wahrscheinlich mit LINQ getroffen, was so praktisch ist, um mit Sammlungen zu arbeiten, Abfragen zu erstellen, Daten zu filtern und zu aggregieren ...

Wir werden nicht unter die Haube von LINQ schauen. Vielleicht machen wir es ein anderes Mal.

Betrachten Sie in der Zwischenzeit ein kleines Beispiel:

 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); 

Was wird dieser Code ausgeben?
Wir erhalten auf dem Bildschirm den Wert der Variablen summResult, der gleich dem Anfangswert ist, d.h. 0.

Warum ist das passiert?

Und weil die Definition einer LINQ-Abfrage und der Start dieser Abfrage zwei Operationen sind, die separat ausgeführt werden. Daher bedeutet die Definition einer Anforderung nicht deren Start / Ausführung.

Die Variable summResult wird in einem anonymen Delegaten in der Select-Methode verwendet: Elemente des dataArray-Arrays werden nacheinander sortiert und der Variablen summResult hinzugefügt.

Wir können davon ausgehen, dass unser Code die Summe der Elemente des dataArray-Arrays druckt. Aber LINQ funktioniert nicht so.

Betrachten Sie die Variable selectedData. Das Schlüsselwort var lautet "syntaktischer Zucker", was in vielen Fällen die Größe des Programmcodes verringert und dessen Lesbarkeit verbessert. Der reale Typ der Variable selectedData implementiert die IEnumerable-Schnittstelle. Das heißt, Unser Code sieht folgendermaßen aus:

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

Hier definieren wir die Abfrage (Abfrage), aber die Abfrage selbst startet nicht. Auf ähnliche Weise können Sie mit der Datenbank arbeiten, indem Sie die SQL-Abfrage als Zeichenfolge angeben. Um das Ergebnis zu erhalten, beziehen Sie sich auf die Datenbank und führen Sie diese Abfrage explizit aus.

Das heißt, wir haben bisher nur eine Anfrage gestellt, diese aber nicht gestartet. Aus diesem Grund bleibt der Wert der Variablen summResult unverändert. Eine Abfrage kann beispielsweise mit den Methoden ToArray, ToList oder ToDictionary gestartet werden:

 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); 

Dieser Code zeigt bereits den Wert der Variablen summResult an, der der Summe aller Elemente des dataArray-Arrays entspricht und 15 entspricht.

Wir haben es herausgefunden. Und was wird dieses Programm dann auf dem Bildschirm anzeigen?

 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 

Die Variable groupedData (Zeile 3) implementiert tatsächlich die IEnumerable-Schnittstelle und definiert im Wesentlichen die Anforderung an die dataArray-Datenquelle. Dies bedeutet, dass diese Anforderung explizit ausgeführt werden muss, damit ein anonymer Delegat arbeitet, der den Wert der Variablen summResult ändert. Es gibt jedoch keinen solchen Start in unserem Programm. Daher wird der Wert der Variablen summResult nur in Zeile 2 geändert, und wir können bei unseren Berechnungen nicht alles andere berücksichtigen.

Dann ist es einfach, den Wert der Variablen summResult zu berechnen, der jeweils 15 + 7 beträgt, d.h. 22.

Beispiel 5


Sagen wir gleich - wir betrachten dieses Beispiel bei unseren Vorlesungen an der Akademie nicht, aber manchmal diskutieren wir es während der Kaffeepausen eher als Scherz.

Trotz der Tatsache, dass dies unter dem Gesichtspunkt der Bestimmung des Entwicklerniveaus kaum bezeichnend ist, haben wir dieses Beispiel in mehreren verschiedenen Tests getroffen. Vielleicht wird es aus Gründen der Vielseitigkeit verwendet, da es in C und C ++ sowie in C # und Java gleich funktioniert.

Lassen Sie es also eine Codezeile geben:

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

Was ist der Wert der Variablen i?
Antwort: 1

Sie könnten denken, dass hier numerische Arithmetik über die Größen jedes Typs in Bytes verwendet wird, da die Zeichen "+" und "-" hier für die Typkonvertierung eher unerwartet auftreten.

In C # ist bekannt, dass der Integer-Typ 4 Bytes lang, 8 Byte lang und char 2 ist.

Dann ist es leicht zu glauben, dass unsere Codezeile dem folgenden arithmetischen Ausdruck entspricht:

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

Dies ist jedoch nicht so. Und um durch solch eine falsche Argumentation zu verwirren und zu lenken, kann das Beispiel zum Beispiel folgendermaßen geändert werden:

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

Die Zeichen "+" und "-" werden in diesem Beispiel nicht als binäre arithmetische Operationen verwendet, sondern als unäre Operatoren. Dann ist unsere Codezeile nur eine Folge von expliziten Typkonvertierungen, gemischt mit Aufrufen unärer Operationen, die wie folgt geschrieben werden können:

  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 ) ) ) ) ) ) ); 


Bild

Interessiert an Lernen an der Veeam Academy?


Jetzt gibt es ein Set für frühlingsintensive C # in St. Petersburg, und wir laden alle ein, sich auf der Website der Veeam Academy Online-Tests zu unterziehen .

Der Kurs beginnt am 18. Februar 2019, dauert bis Mitte Mai und ist wie immer völlig kostenlos. Die Registrierung für alle, die sich einem Eingangstest unterziehen möchten, ist bereits auf der Website der Akademie verfügbar: akademy.veeam.ru

Bild

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


All Articles