7 gefährliche Fehler, die in C # /. NET leicht zu machen sind

Vor Beginn des Kurses „C # ASP.NET Core Developer“ wurde eine Übersetzung des Artikels erstellt .




C # ist eine großartige Sprache , und das .NET Framework ist auch sehr gut. Durch die starke Eingabe in C # können Sie die Anzahl der Fehler reduzieren, die Sie im Vergleich zu anderen Sprachen hervorrufen können. Außerdem hilft das allgemeine intuitive Design im Vergleich zu JavaScript (wo wahr falsch ist ) sehr. Jede Sprache hat jedoch ihren eigenen Rechen, auf den man leicht treten kann, zusammen mit falschen Vorstellungen über das erwartete Verhalten der Sprache und Infrastruktur. Ich werde versuchen, einige dieser Fehler im Detail zu beschreiben.

1. Verstehe die verzögerte (faule) Ausführung nicht


Ich glaube, dass erfahrene Entwickler diesen .NET-Mechanismus kennen, aber er kann weniger sachkundige Kollegen überraschen. Kurz gesagt, die Methoden / Operatoren, die jedes Ergebnis zurückgeben IEnumerable<T>und yieldzum Zurückgeben verwenden , werden nicht in der Codezeile ausgeführt, die sie tatsächlich aufruft. Sie werden ausgeführt, wenn auf die resultierende Sammlung auf irgendeine Weise zugegriffen wird *. Beachten Sie, dass die meisten LINQ-Ausdrücke ihre Ergebnisse schließlich mit Ausbeute zurückgeben .

Betrachten Sie als Beispiel den folgenden ungeheuren Unit-Test.

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Ensure_Null_Exception_Is_Thrown()
{
   var result = RepeatString5Times(null);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Ensure_Invalid_Operation_Exception_Is_Thrown()
{
   var result = RepeatString5Times("test");
   var firstItem = result.First();
}
private IEnumerable<string> RepeatString5Times(string toRepeat)
{
   if (toRepeat == null)
       throw new ArgumentNullException(nameof(toRepeat));
   for (int i = 0; i < 5; i++)
   {   
       if (i == 3)
            throw new InvalidOperationException("3 is a horrible number");
       yield return $"{toRepeat} - {i}";
   }
}

Beide Tests schlagen fehl. Der erste Test schlägt fehl, da das Ergebnis nirgendwo verwendet wird und der Hauptteil der Methode niemals ausgeführt wird. Der zweite Test wird aus einem anderen, etwas weniger trivialen Grund fehlschlagen. Jetzt erhalten wir das erste Ergebnis des Aufrufs unserer Methode, um sicherzustellen, dass die Methode tatsächlich ausgeführt wird. Der Mechanismus für die verzögerte Ausführung beendet die Methode jedoch so schnell wie möglich. In diesem Fall haben wir nur das erste Element verwendet. Sobald wir die erste Iteration bestanden haben, stoppt die Methode ihre Ausführung (daher wird i == 3 niemals wahr sein).

Die verzögerte Ausführung ist tatsächlich ein interessanter Mechanismus, insbesondere weil es einfach ist, LINQ-Abfragen zu verketten und Daten nur dann abzurufen, wenn Ihre Abfrage einsatzbereit ist.

2. , Dictionary ,


Dies ist besonders unangenehm und ich bin mir sicher, dass ich irgendwo Code habe, der auf dieser Annahme beruht. Wenn Sie Elemente zur Liste hinzufügen List<T>, werden diese in derselben Reihenfolge gespeichert, in der Sie sie hinzufügen - logisch. Manchmal muss ein anderes Objekt mit einem Element in der Liste verknüpft sein. Die naheliegende Lösung besteht darin, ein Wörterbuch zu verwenden Dictionary<TKey,TValue>, mit dem Sie einen zugehörigen Wert für den Schlüssel angeben können.

Anschließend können Sie das Wörterbuch mit foreach durchlaufen. In den meisten Fällen verhält es sich wie erwartet. Sie greifen auf die Elemente in derselben Reihenfolge zu, in der sie dem Wörterbuch hinzugefügt wurden. Dieses Verhalten ist jedoch undefiniert - d.h. Es ist ein glücklicher Zufall, auf den man sich nicht verlassen kann und den man immer erwartet. Dies wird in erklärtMicrosoft-Dokumentation , aber ich denke, nur wenige Leute haben diese Seite sorgfältig studiert.

Um dies zu veranschaulichen, lautet die Ausgabe im folgenden Beispiel wie folgt:

dritte
Sekunde


var dict = new Dictionary<string, object>();       
dict.Add("first", new object());
dict.Add("second", new object());
dict.Remove("first");
dict.Add("third", new object());
foreach (var entry in dict)
{
    Console.WriteLine(entry.Key);
}

Glaube mir nicht? Überprüfen Sie hier online .

3. Flusssicherheit nicht berücksichtigen


Multithreading ist großartig. Bei korrekter Implementierung können Sie die Leistung Ihrer Anwendung erheblich verbessern. Sobald Sie jedoch Multithreading eingeben, sollten Sie mit allen Objekten, die Sie ändern, sehr, sehr vorsichtig sein, da Sie möglicherweise auf scheinbar zufällige Fehler stoßen, wenn Sie nicht vorsichtig genug sind.

Einfach ausgedrückt sind viele Basisklassen in der .NET-Bibliothek nicht threadsicher.- Dies bedeutet, dass Microsoft nicht garantiert, dass Sie diese Klasse mit mehreren Threads parallel verwenden können. Dies wäre kein großes Problem, wenn Sie sofort damit verbundene Probleme finden könnten. Die Art des Multithreading impliziert jedoch, dass auftretende Probleme sehr instabil und unvorhersehbar sind - höchstwahrscheinlich führen keine zwei Ausführungen zum gleichen Ergebnis.

Betrachten Sie beispielsweise diesen Codeblock, der einfach, aber nicht threadsicher ist List<T>.

var items = new List<int>();
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
   tasks.Add(Task.Run(() => {
       for (int k = 0; k < 10000; k++)
       {
           items.Add(i);
       }
   }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(items.Count);

Daher fügen wir der Liste jeweils 10.000 Mal Zahlen von 0 bis 4 hinzu, was bedeutet, dass die Liste letztendlich 50.000 Elemente enthalten sollte . Sollte ich? Nun, es gibt eine kleine Chance, dass es am Ende so sein wird - aber unten sind die Ergebnisse von 5 meiner verschiedenen Starts:

28191
23536
44346
40007
40476

Hier können Sie es selbst online überprüfen .

Dies liegt in der Tat daran, dass die Add-Methode nicht atomar ist, was bedeutet, dass der Thread die Methode unterbrechen kann, wodurch letztendlich die Größe des Arrays geändert werden kann, während ein anderer Thread gerade ein Element hinzufügt, oder ein Element mit demselben Index hinzugefügt wird als der andere Thread. Die IndexOutOfRange-Ausnahme kam einige Male zu mir, wahrscheinlich weil sich die Größe des Arrays während des Hinzufügens geändert hat. Was machen wir hier? Wir können das Schlüsselwort lock verwenden, um sicherzustellen, dass nur ein Thread gleichzeitig ein (Add) -Element zur Liste hinzufügen kann. Dies kann jedoch die Leistung erheblich beeinträchtigen. Microsoft, nette Leute, bietet einige großartige Sammlungen, dieSie sind threadsicher und hinsichtlich der Leistung hoch optimiert. Ich habe bereits einen Artikel veröffentlicht, in dem beschrieben wird, wie Sie sie verwenden können .

4. Missbrauch des verzögerten (verzögerten) Ladens in LINQ


Das verzögerte Laden ist eine großartige Funktion sowohl für LINQ to SQL als auch für LINQ to Entities (Entity Framework), mit der Sie verwandte Tabellenzeilen nach Bedarf laden können. In einem meiner anderen Projekte habe ich eine Tabelle "Module" und eine Tabelle "Ergebnisse" mit einer Eins-zu-Viele-Beziehung (ein Modul kann viele Ergebnisse haben).



Wenn ich ein bestimmtes Modul erhalten möchte, möchte ich sicher nicht, dass das Entity Framework jedes Ergebnis zurückgibt, das die Modultabelle enthält! Daher ist er klug genug, eine Abfrage auszuführen, um nur dann Ergebnisse zu erhalten, wenn ich sie brauche. Der folgende Code führt also zwei Abfragen aus - eine zum Empfangen des Moduls und eine zum Empfangen der Ergebnisse (für jedes Modul).

using (var db = new DBEntities())
{
   var modules = db.Modules;
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

Was ist jedoch, wenn ich Hunderte von Modulen habe? Dies bedeutet, dass für jedes Modul eine separate SQL-Abfrage zum Empfangen von Ergebnisdatensätzen ausgeführt wird! Dies wird natürlich den Server belasten und Ihre Anwendung erheblich verlangsamen. Im Entity Framework ist die Antwort sehr einfach: Sie können angeben, dass Ihre Abfrage bestimmte Ergebnisse enthält. Im folgenden geänderten Code wird nur eine SQL-Abfrage ausgeführt, die jedes Modul und jedes Ergebnis für dieses Modul enthält (kombiniert zu einer Abfrage, die das Entity Framework in Ihrem Modell intelligent anzeigt).

using (var db = new DBEntities())
{
   var modules = db.Modules.Include(b => b.Results);
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

5. Verstehen Sie nicht, wie LINQ to SQL / Entity Frameworks Abfragen übersetzt


Da wir das LINQ-Thema angesprochen haben, sollte erwähnt werden, wie unterschiedlich Ihr Code ausgeführt wird, wenn er sich in einer LINQ-Abfrage befindet. Auf hoher Ebene wird erklärt, dass der gesamte Code in einer LINQ-Abfrage mithilfe von Ausdrücken in SQL übersetzt wird. Dies scheint offensichtlich, aber es ist sehr, sehr leicht, den Kontext zu vergessen, in dem Sie sich befinden, und letztendlich Probleme in Ihre Codebasis einzuführen. Im Folgenden habe ich eine Liste zusammengestellt, um einige typische Hindernisse zu beschreiben, auf die Sie stoßen können.

Die meisten Methodenaufrufe funktionieren nicht.

Stellen Sie sich also vor, Sie haben die folgende Abfrage, um den Namen aller Module durch einen Doppelpunkt zu trennen und den zweiten Teil zu erfassen.

var modules = from m in db.Modules
              select m.Name.Split(':')[1];

Bei den meisten LINQ-Anbietern tritt eine Ausnahme auf. Es gibt keine SQL-Übersetzung für die Split-Methode. Einige Methoden werden möglicherweise unterstützt, z. B. das Hinzufügen von Tagen zu einem Datum. Dies hängt jedoch alles von Ihrem Anbieter ab.

Diejenigen, die funktionieren, können zu unerwarteten Ergebnissen führen ...

Nehmen Sie den folgenden LINQ-Ausdruck (ich habe keine Ahnung, warum Sie dies in der Praxis tun würden, aber stellen Sie sich bitte vor, dass dies eine vernünftige Anfrage ist).

int modules = db.Modules.Sum(a => a.ID);

Wenn die Modultabelle Zeilen enthält, erhalten Sie die Summe der Bezeichner. Klingt richtig! Aber was ist, wenn Sie es stattdessen mit LINQ to Objects ausführen? Wir können dies tun, indem wir die Sammlung von Modulen in eine Liste konvertieren, bevor wir unsere Summenmethode ausführen.

int modules = db.Modules.ToList().Sum(a => a.ID);

Schock, Horror - es wird genau das Gleiche tun! Was ist jedoch, wenn Sie keine Zeilen in der Modultabelle hatten? LINQ to Objects gibt 0 zurück, und die Entity Framework / LINQ to SQL-Version löst eine InvalidOperationException aus , die besagt, dass "int?" Nicht konvertiert werden kann. in "int" ... wie. Dies liegt daran, dass beim Ausführen von SUM in SQL für eine leere Menge NULL anstelle von 0 zurückgegeben wird. Daher wird stattdessen versucht, ein nullbares int zurückzugeben. Hier sind einige Tipps, um dies zu beheben, wenn Sie auf ein solches Problem stoßen .

Wissen, wann Sie nur das gute alte SQL verwenden müssen.

Wenn Sie eine äußerst komplexe Anfrage ausführen, sieht Ihre übersetzte Anfrage möglicherweise so aus, als würde etwas ausgespuckt, immer wieder aufgefressen. Leider habe ich keine Beispiele zu demonstrieren, aber nach der vorherrschenden Meinung mag ich es wirklich, verschachtelte Ansichten zu verwenden, was die Codepflege zu einem Albtraum macht.

Wenn Sie außerdem auf Leistungsengpässe stoßen, können Sie diese nur schwer beheben, da Sie keine direkte Kontrolle über das generierte SQL haben. Führen Sie dies entweder in SQL aus oder delegieren Sie es an den Datenbankadministrator, falls Sie oder Ihr Unternehmen einen haben!

6. Falsche Rundung


Nun zu etwas, das etwas einfacher ist als die vorherigen Absätze, aber ich habe es immer vergessen und endete mit unangenehmen Fehlern (und, wenn es mit Finanzen zusammenhängt, einem wütenden Flossen- / Gendirektor).

Das .NET Framework enthält eine hervorragende statische Methode in der Math- Klasse namens Round , die einen numerischen Wert verwendet und auf die angegebene Dezimalstelle rundet. Es funktioniert die meiste Zeit perfekt, aber was tun, wenn Sie versuchen, 2,25 auf die erste Dezimalstelle zu runden? Ich nehme an, Sie erwarten wahrscheinlich eine Runde auf 2,3 - daran sind wir alle gewöhnt, oder? In der Praxis stellt sich heraus, dass .NET Banker-Rundungen verwendetwas das gegebene Beispiel auf 2.2 rundet! Dies liegt an der Tatsache, dass Banker auf die nächste gerade Zahl gerundet werden, wenn sich die Zahl im „Mittelpunkt“ befindet. Glücklicherweise kann dies in der Math.Round-Methode leicht überschrieben werden.

Math.Round(2.25,1, MidpointRounding.AwayFromZero)

7. Schreckliche Klasse 'DBNull'


Dies kann für manche zu unangenehmen Erinnerungen führen - ORM verbirgt diesen Schmutz vor uns, aber wenn Sie in die Welt des nackten ADO.NET (SqlDataReader und dergleichen) eintauchen, werden Sie DBNull.Value treffen.

Ich bin mir nicht 100% sicher, warumNULL-Werte aus der Datenbank werden wie folgt verarbeitet (bitte kommentieren Sie unten, wenn Sie wissen!), Aber Microsoft hat beschlossen, ihnen einen speziellen Typ DBNull (mit einem statischen Feld Wert) zu präsentieren. Ich kann einen der Vorteile davon nennen - Sie erhalten keine unangenehmen NullReferenceException-Ausnahmen, wenn Sie auf ein Datenbankfeld zugreifen, das NULL ist. Sie sollten jedoch nicht nur die sekundäre Methode zur Überprüfung auf NULL-Werte unterstützen (die leicht zu vergessen ist und zu schwerwiegenden Fehlern führen kann), sondern auch die großartigen Funktionen von C # verlieren, die Ihnen bei der Arbeit mit null helfen. Was könnte so einfach sein wie

reader.GetString(0) ?? "NULL";

was schließlich wird ...

reader.GetString(0) != DBNull.Value ? reader.GetString(0) : "NULL";

Pfui.

Hinweis


Dies sind nur einige der nicht trivialen „Rechen“, auf die ich in .NET gestoßen bin. Wenn Sie mehr wissen, würde ich gerne von Ihnen unten hören.



ASP.NET Core:



All Articles