Wann werden Zeiger in C # /. NET verwendet?

75

Ich weiß, dass C # dem Programmierer die Möglichkeit gibt, auf Zeiger in einem unsicheren Kontext zuzugreifen und diese zu verwenden. Aber wann wird das gebraucht?

Unter welchen Umständen ist die Verwendung von Zeigern unvermeidlich?

Ist es nur aus Leistungsgründen?

Warum stellt C # diese Funktionalität in einem unsicheren Kontext zur Verfügung und entfernt alle verwalteten Vorteile daraus? Ist es möglich, Zeiger zu verwenden, ohne theoretisch die Vorteile einer verwalteten Umgebung zu verlieren?

Joan Venge
quelle
Danke Richard, ich versuche nur mehr zu lernen, indem ich (mehr) Fragen stelle: O
Joan Venge
3
Diese Frage würde Sie wahrscheinlich interessieren: stackoverflow.com/questions/584134/…
Hans Olsson

Antworten:

90

Wann wird das benötigt? Unter welchen Umständen ist die Verwendung von Zeigern unvermeidlich?

Wenn die Nettokosten einer verwalteten, sicheren Lösung nicht akzeptabel sind, die Nettokosten einer unsicheren Lösung jedoch akzeptabel sind. Sie können die Nettokosten oder den Nettonutzen ermitteln, indem Sie den Gesamtnutzen von den Gesamtkosten abziehen. Die Vorteile einer unsicheren Lösung sind: "Keine Zeitverschwendung bei unnötigen Laufzeitprüfungen, um die Richtigkeit sicherzustellen." Die Kosten bestehen darin, (1) Code zu schreiben, der auch bei ausgeschaltetem verwalteten Sicherheitssystem sicher ist, und (2) den Garbage Collector möglicherweise weniger effizient zu machen, da er sich nicht im Speicher bewegen kann, in den ein nicht verwalteter Zeiger vorhanden ist es.

Oder wenn Sie die Person sind, die die Rangierschicht schreibt.

Ist es nur aus Leistungsgründen?

Es erscheint pervers, Zeiger in einer verwalteten Sprache aus anderen Gründen als der Leistung zu verwenden.

Sie können die Methoden in der Marshal-Klasse verwenden, um in den allermeisten Fällen mit nicht verwaltetem Code zusammenzuarbeiten. (Es kann einige Fälle geben, in denen es schwierig oder unmöglich ist, die Rangierausrüstung zur Lösung eines Interop-Problems zu verwenden, aber ich kenne keine.)

Natürlich, wie ich schon sagte, wenn Sie die Person das Schreiben der Marshal - Klasse sind dann bekommt man natürlich nicht zu verwenden , die Rangier - Schicht Ihr Problem zu lösen. In diesem Fall müssten Sie es mithilfe von Zeigern implementieren.

Warum macht C # diese Funktionalität in einem unsicheren Kontext verfügbar und entfernt alle verwalteten Vorteile daraus?

Diese verwalteten Vorteile sind mit Leistungskosten verbunden. Beispielsweise muss die Laufzeit jedes Mal, wenn Sie ein Array nach seinem zehnten Element fragen, eine Überprüfung durchführen, um festzustellen, ob ein zehntes Element vorhanden ist, und eine Ausnahme auslösen, wenn dies nicht der Fall ist. Mit Zeigern werden die Laufzeitkosten eliminiert.

Die entsprechenden Entwicklerkosten sind, dass Sie, wenn Sie es falsch machen, mit Speicherbeschädigungsfehlern umgehen müssen, die Ihre Festplatte formatieren und Ihren Prozess eine Stunde später zum Absturz bringen, anstatt sich zum Zeitpunkt des Fehlers mit einer netten, sauberen Ausnahme zu befassen.

Ist es möglich, Zeiger zu verwenden, ohne theoretisch die Vorteile einer verwalteten Umgebung zu verlieren?

Mit "Vorteilen" meine ich Vorteile wie Speicherbereinigung, Typensicherheit und referenzielle Integrität. Ihre Frage lautet also im Wesentlichen: "Ist es theoretisch möglich, das Sicherheitssystem auszuschalten und dennoch die Vorteile des eingeschalteten Sicherheitssystems zu nutzen?" Nein, klar ist es nicht. Wenn Sie dieses Sicherheitssystem ausschalten, weil Sie nicht mögen, wie teuer es ist, erhalten Sie nicht die Vorteile, wenn es eingeschaltet ist!

Eric Lippert
quelle
Danke Eric für die Antwort. Können Sie mir bitte sagen, was "referenzielle Integrität" bedeutet? Ist es die Verwendung von Referenzen anstelle von Zeigern?
Joan Venge
9
@Joan: Dass sich jede Referenz tatsächlich auf etwas Gültiges bezieht oder null ist . Zeiger haben diese Eigenschaft nicht; Ein Zeiger kann sich auf einen Speicher beziehen, der überhaupt nicht gut ist. Aber verwaltete Referenzen haben diese Eigenschaft. Wenn Sie einen Verweis auf eine Zeichenfolge haben, ist dieses Ding immer entweder null oder eine gültige Zeichenfolge. Es ist garantiert, dass Sie sich nicht in einer Situation befinden, in der Sie einen Nicht-Null-Verweis auf etwas haben, das keine gültige Zeichenfolge ist.
Eric Lippert
Danke Eric, ich verstehe es jetzt.
Joan Venge
2
@masoudkeshavarz: Nein. Mit verwalteten Zeigern ist es unmöglich, einen Zeiger auf einen beliebigen Speicher zu fälschen. Bei nicht verwalteten Zeigern in unsicherem Code werden sie aus einem bestimmten Grund als "nicht verwaltet" und "unsicher" bezeichnet . Mit einem nicht verwalteten Zeiger in unsicherem Code können Sie alles tun, was Sie möchten, einschließlich der Beschädigung der .NET-Laufzeitdatenstrukturen.
Eric Lippert
1
Heilige Hölle, ich habe eine Stunde lang nach einer klaren Antwort gesucht und das ist erstaunlich. Vielen Dank!
krivar
17

Zeiger sind ein inhärenter Widerspruch zur verwalteten, durch Müll gesammelten Umgebung.
Sobald Sie anfangen, mit rohen Zeigern herumzuspielen, hat der GC keine Ahnung, was los ist.

Insbesondere kann nicht festgestellt werden, ob Objekte erreichbar sind, da nicht bekannt ist, wo sich Ihre Zeiger befinden.
Es kann auch keine Objekte im Speicher verschieben, da dies Ihre Zeiger beschädigen würde.

All dies würde durch GC-verfolgte Zeiger gelöst; Das sind Referenzen.

Sie sollten Zeiger nur in chaotischen erweiterten Interop-Szenarien oder für hochentwickelte Optimierungen verwenden.
Wenn Sie fragen müssen, sollten Sie wahrscheinlich nicht.

SLaks
quelle
7
+1 für Wenn Sie fragen müssen, sollten Sie wahrscheinlich nicht . Exzellenter Rat :-)
Darin Dimitrov
1
Ihre Schlussfolgerung ist richtig, aber die meisten Ihrer Erklärungen sind falsch. Zeiger und Referenzen unterscheiden sich nicht aus der Sicht des Garbage Collectors. Was den GC bricht, ist, wenn ein Zeiger oder eine Referenz in einem untypisierten Speicherbereich gespeichert wird, weil der GC nicht mehr weiß, ob es sich nur um einen numerischen Wert oder die Adresse eines verwalteten Objekts handelt.
Ben Voigt
1
@SLaks: Ich habe nicht gesagt, dass Referenzen und Zeiger nicht unterschiedlich sind, ich habe gesagt, dass sie sich nicht aus der Sicht des Garbage Collectors unterscheiden . Dem GC ist es egal, ob Sie die Adresse eines Array-Elements verwendet oder mit einem Zeiger auf ein anderes Element begonnen und arithmetisch nach dem gesucht haben, auf das Sie jetzt zeigen.
Ben Voigt
1
@SLaks: Selbst in nativem C und C ++ ist Zeigerarithmetik nur innerhalb der Grenzen eines einzelnen Objekts / einer einzelnen Zuordnung (z. B. eines Arrays) zulässig. Der Garbage Collector verschiebt ohnehin ganze Objekte zusammen, Zeiger würden nicht brechen.
Ben Voigt
3
@ Slaks: Beträchtlich. Übrigens, Ihr hypothetischer GC-verfolgter Zeiger existiert in anderen .NET-Sprachen (wenn auch mit einigen Einschränkungen - er kann nur eine automatische Variable sein) und unterstützt Arithmetik:interior_ptr
Ben Voigt
5

Der GC kann Referenzen verschieben. Die Verwendung von unsicher hält ein Objekt außerhalb der Kontrolle des GC und vermeidet dies. "Behoben" fixiert ein Objekt, lässt aber den GC den Speicher verwalten.

Wenn Sie per Definition einen Zeiger auf die Adresse eines Objekts haben und der GC ihn verschiebt, ist Ihr Zeiger nicht mehr gültig.

Warum Sie Zeiger benötigen: Der Hauptgrund ist die Arbeit mit nicht verwalteten DLLs, z. B. solchen, die in C ++ geschrieben wurden

Beachten Sie außerdem, dass Sie anfälliger für Heap-Fragmentierung sind, wenn Sie Variablen anheften und Zeiger verwenden.


Bearbeiten

Sie haben das Kernproblem von verwaltetem und nicht verwaltetem Code angesprochen ... Wie wird der Speicher freigegeben?

Sie können Code für die Leistung mischen, wie Sie es beschreiben. Sie können verwaltete / nicht verwaltete Grenzen einfach nicht mit Zeigern überschreiten (dh Sie können keine Zeiger außerhalb des "unsicheren" Kontexts verwenden).

Wie sie gereinigt werden ... Sie müssen Ihr eigenes Gedächtnis verwalten; Objekte, auf die Ihre Zeiger zeigen, wurden mithilfe von (hoffentlich) erstellt / zugewiesen (normalerweise innerhalb der C ++ - DLL) CoTaskMemAlloc(), und Sie müssen diesen Speicher auf dieselbe Weise freigeben, indem CoTaskMemFree()Sie aufrufen , oder es kommt zu einem Speicherverlust. Beachten Sie, dass nur mit zugewiesener Speicher CoTaskMemAlloc()freigegeben werden kann CoTaskMemFree().

Die andere Alternative besteht darin, eine Methode aus Ihrer nativen C ++ - DLL verfügbar zu machen, die einen Zeiger verwendet und freigibt. Auf diese Weise kann die DLL entscheiden, wie der Speicher freigegeben werden soll. Dies funktioniert am besten, wenn eine andere Methode zum Zuweisen von Speicher verwendet wird. Die meisten nativen DLLs, mit denen Sie arbeiten, sind DLLs von Drittanbietern, die Sie nicht ändern können, und sie haben normalerweise keine (wie ich gesehen habe) Funktionen zum Aufrufen.

Ein Beispiel für die Speicherfreigabe von hier :

string[] array = new string[2];
array[0] = "hello";
array[1] = "world";
IntPtr ptr = test(array);
string result = Marshal.PtrToStringAuto(ptr);
Marshal.FreeCoTaskMem(ptr);
System.Console.WriteLine(result);


Noch etwas Lesematerial:

C # Freigabe des von IntPtr referenzierten Speichers In der zweiten Antwort werden die verschiedenen Zuordnungs- / Freigabemethoden erläutert

Wie kann ich IntPtr in C # freigeben? Verstärkt die Notwendigkeit, die Zuordnung auf dieselbe Weise aufzuheben, wie der Speicher zugewiesen wurde

http://msdn.microsoft.com/en-us/library/aa366533%28VS.85%29.aspx Offizielle MSDN-Dokumentation zu den verschiedenen Möglichkeiten zum Zuweisen und Freigeben von Speicher.

Kurz gesagt ... Sie müssen wissen, wie der Speicher zugewiesen wurde, um ihn freizugeben.


Bearbeiten Wenn ich Ihre Frage richtig verstehe, lautet die kurze Antwort Ja. Sie können die Daten an nicht verwaltete Zeiger übergeben, in einem unsicheren Kontext damit arbeiten und die Daten verfügbar haben, sobald Sie den unsicheren Kontext verlassen.

Der Schlüssel ist, dass Sie das verwaltete Objekt, auf das Sie verweisen, mit einem fixedBlock verbinden müssen. Dies verhindert, dass der Speicher, auf den Sie verweisen, vom GC im unsafeBlock verschoben wird . Hier gibt es eine Reihe von Feinheiten, z. B. können Sie einen in einem festen Block initialisierten Zeiger nicht neu zuweisen. Sie sollten sich über unsichere und feste Anweisungen informieren, wenn Sie wirklich Ihren eigenen Code verwalten möchten.

Die Vorteile der Verwaltung Ihrer eigenen Objekte und der Verwendung von Zeigern in der von Ihnen beschriebenen Weise führen jedoch möglicherweise nicht zu einer Leistungssteigerung, wie Sie vielleicht denken. Gründe warum nicht:

  1. C # ist sehr optimiert und sehr schnell
  2. Ihr Zeigercode wird weiterhin als IL generiert, das angepasst werden muss (an diesem Punkt kommen weitere Optimierungen ins Spiel).
  3. Sie schalten den Garbage Collector nicht aus ... Sie halten nur die Objekte, mit denen Sie arbeiten, aus dem Zuständigkeitsbereich des GC heraus. Also alle 100 ms oder so, die GC noch unterbricht Ihren Code und führt seine Funktionen für alle anderen Variablen in Ihrem verwalteten Code.

HTH,
James

James King
quelle
Danke, aber wenn Sie Zeiger verwenden, wie werden sie "gereinigt", wenn Sie fertig sind? Ist es möglich, sie in perf-kritischen Situationen zu verwenden und dann wieder zu verwaltetem Code zu wechseln?
Joan Venge
Danke James für zusätzliche Infos.
Joan Venge
2
@ Joan: Sicher. Aber Sie sind verantwortlich dafür , dass alles aufgeräumt ist, dass es keine Streu Zeiger auf bewegliche Speicher herumliegen, und so weiter. Wenn Sie die Vorteile des Ausschaltens des Sicherheitssystems nutzen möchten, müssen Sie die Kosten dafür übernehmen, was das Sicherheitssystem normalerweise für Sie tut.
Eric Lippert
Danke Eric, das macht Sinn. Aber in Fällen von Leistungsoptimierungen über Zeiger werden die Daten immer noch in die verwaltete Welt zurückgeführt, sobald er / sie fertig ist, oder? Wie verwaltete Daten -> nicht verwaltete Daten -> einige schnelle Vorgänge mit diesen Daten -> verwaltete Daten aus diesen nicht verwalteten Daten erstellen -> nicht verwalteten Speicher bereinigen -> zurück in die verwaltete Welt?
Joan Venge
1
Als weiteren Hinweis können Sie die Speicherbereinigung des Speicherdrucks aus nicht verwaltetem Speicher mithilfe von GC.AddMemoryPressureund explizit benachrichtigen GC.RemoveMemoryPressure. Sie müssen den Speicher immer noch selbst freigeben, aber auf diese Weise berücksichtigt der Garbage Collector nicht verwalteten Speicher, wenn Sie Planungsentscheidungen treffen.
Brian
3

Die häufigsten Gründe für die explizite Verwendung von Zeigern in C #:

  • Arbeiten auf niedriger Ebene (wie die Manipulation von Zeichenfolgen), die sehr leistungsempfindlich sind,
  • Schnittstelle zu nicht verwalteten APIs.

Der Grund, warum die mit Zeigern verknüpfte Syntax aus C # entfernt wurde (nach meinem Wissen und Standpunkt - Jon Skeet würde besser mit B- antworten), war, dass sie sich in den meisten Situationen als überflüssig herausstellte.

Aus Sicht des Sprachdesigns müssen Sie, sobald Sie den Speicher durch einen Garbage Collector verwalten, strenge Einschränkungen einführen, was mit Zeigern möglich ist und was nicht. Die Verwendung eines Zeigers zum Zeigen in die Mitte eines Objekts kann beispielsweise schwerwiegende Probleme für den GC verursachen. Sobald die Einschränkungen vorhanden sind, können Sie die zusätzliche Syntax einfach weglassen und erhalten „automatische“ Referenzen.

Auch der ultra-wohlwollende Ansatz in C / C ++ ist eine häufige Fehlerquelle. In den meisten Situationen, in denen die Mikroleistung überhaupt keine Rolle spielt, ist es besser, strengere Regeln anzubieten und den Entwickler zugunsten weniger Fehler einzuschränken, die sehr schwer zu entdecken wären. Daher sind für gängige Geschäftsanwendungen die sogenannten "verwalteten" Umgebungen wie .NET und Java besser geeignet als Sprachen, die davon ausgehen, dass sie gegen die Bare-Metal-Maschine arbeiten.

Ondrej Tucny
quelle
1
Zeiger werden nicht aus C # entfernt. Vielleicht denkst du über Java nach?
Ben Voigt
Ich meine nicht, dass Zeiger entfernt wurden, aber die zusätzliche Syntax wurde entfernt, dh Sie müssen nicht schreiben obj->Property, obj.Propertyfunktioniert stattdessen. Wird meine Antwort klarstellen.
Ondrej Tucny
1
Ben hat recht; Sie müssen mit Sicherheit Pfeile (und Sterne) verwenden, wenn Sie Zeiger in C # dereferenzieren. Verwechseln Sie Zeiger nicht mit Referenzen . C # unterstützt beide.
Eric Lippert
@ Eric Lippert Heh, ja. Als ich jedoch an eine Referenz als Teilmenge eines Zeigers dachte, wählte ich das Wort "Zeiger" als allgemeinere Variante, um die Entwicklung einer Referenz zu erklären - und eine Sprache ohne Zeiger (deren "sicherer" Teil dazu richtig sein) - vom einfachen alten Zeiger.
Ondrej Tucny
2

Angenommen, Sie möchten zwischen zwei Anwendungen über IPC (Shared Memory) kommunizieren, dann können Sie die Daten im Speicher zusammenfassen und diesen Datenzeiger über Windows Messaging oder ähnliches an die andere Anwendung übergeben. Beim Empfang der Bewerbung können Sie Daten zurückholen.

Nützlich auch beim Übertragen von Daten aus .NET in ältere VB6-Apps, bei denen Sie die Daten in den Speicher übertragen, den Zeiger mithilfe der Win-Meldung an die VB6-App übergeben und mit VB6 copymemory () Daten aus dem verwalteten Speicher in den nicht verwalteten Speicher von VB6-Apps abrufen Raum..

Variable
quelle