Großartiger Kommentar vom Typ Swift-Zeiger

Einführung

Swift hat "UnsafePointer " und seine Begleiter als Typen zur Darstellung von Zeigern. Es wird verwendet, wenn eine C-Sprachbibliothek wie Core Foundation verwendet wird. Diese Zeiger-APIs sind sehr gut durchdacht und wunderbar. Dieser Artikel wird es vorstellen und erklären. Ich denke, es ist interessant für C-Sprachbenutzer und C ++ - Benutzer. (schnell 3.0.2)

Zeigertyp

Die Zeigertypen sind wie folgt.

Grundlegender Zeigertyp

Für Sprachbrücke

Es gibt so viele.

Grundlegende Zeigertypattribute

Der grundlegendste Zeigertyp ist "UnsafePointer ". Stellt einen Zeiger auf einen Wert vom Typ T dar. Der referenzierte T-Wert ist unveränderlich. Da "UnsafePointer " selbst eine Struktur ist, können "let" und "var" die Unveränderlichkeit des Zeigers selbst darstellen.

func f(_ x: UnsafePointer<Int>) {
	let a: UnsafePointer<Int> = x
	var b: UnsafePointer<Int> = x
}

Das Obige wird in C-Sprache wie folgt ausgedrückt.

void f(const int * const x) {
	const int * const a = x;
	const int * b = x;
}

Für "UnsafePointer " wird es durch Hinzufügen von drei Attributen zu einem anderen Typ. Es gibt 8 Kombinationen von 2x2x2.

Mutability

Es gibt eine veränderbare Version, in der der Wert der Referenz T geändert werden kann. Mutable ist an den Namen angehängt. Aus der unveränderlichen Version kann es mit einem Konstruktor mit der Bezeichnung "mutating" konvertiert werden. Umgekehrt können Sie mit dem unbeschrifteten Konstruktor von der veränderlichen Version in die unveränderliche Version konvertieren.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public init(_ other: UnsafeMutablePointer<Pointee>)
    ...
}

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public init(mutating other: UnsafePointer<Pointee>)
    ...
}

Mit und ohne Typ

UnsafePointer <T> hatte den referenzierten Typ T als Typparameter, aber einige Versionen haben diese Typinformationen nicht. Editionen ohne Typinformationen werden mit "Raw" benannt. Zum Beispiel erscheint "const void *" in C-Sprache Swift als dieser "UnsafeRawPointer". Es wird verwendet, wenn der Typ der Zeigerreferenz unbekannt ist.

Ursprünglich eingeführt, könnte "UnsafePointer " allein in einigen Codes zu strengen Aliasing-Fehlern führen, und wir brauchten "UnsafeRawPointer" mit memcpy-Semantik, um dies zu vermeiden. Korrekt.

Details werden zum Zeitpunkt des Standardvorschlags in das Dokument geschrieben. UnsafeRawPointer API

Das Folgende ist auch über striktes Aliasing leicht zu verstehen. (Übersetzung) Strict Aliasing in C / C ++ verstehen oder - warum # $ @ ## @ ^% Der Compiler lässt mich nicht tun, was ich will!

Die Konvertierung zwischen typisiert und untypisiert wird später erläutert.

Zeiger und Puffer

In der Sprache C werden Zeiger häufig zur Darstellung von Arrays verwendet. Um sie jedoch in einem sogenannten Array zu behandeln, muss die Anzahl der Elemente zusammen mit den Zeigern bekannt sein. Daher stellt "UnsafeBufferPointer " ein Array dar, das diesen Zeiger und die Anzahl der Elemente als Menge darstellt. Es gibt vier Arten von "Puffern", die unveränderlich und veränderlich, typisiert und untypisiert sind.

UnsafeBufferPointer <T> empfängt die Startadresse und die Anzahl der Elemente im Konstruktor. Und es erbt das "Collection" -Protokoll.

public struct UnsafeBufferPointer<Element> : Indexable, Collection, RandomAccessCollection {
	...
    public init(start: UnsafePointer<Element>?, count: Int)
    ...
    public var baseAddress: UnsafePointer<Element>? { get }
    ...
    public var count: Int { get }
    ...
}

Form für Brücke

OpaquePointer

OpaquePointer dient zur Darstellung des Mustertyps, der als opaker Zeiger in der C-Sprache bezeichnet wird. Ein undurchsichtiger Zeiger ist ein Zeiger, der nur durch den Namen des Typs vordeklariert ist, jedoch praktisch keine Informationen über den Zugriff auf das referenzierte Ziel enthält, da die Definition des Typs nicht sichtbar ist. Es wird auch verwendet, um die Details innerhalb der Bibliothek von außerhalb der Bibliothek zu verbergen. In Swift kommt es nicht vor, dass nur der Name des Typs sichtbar ist, sondern die Definition unsichtbar. Dieser Typ wird jedoch verwendet, da er ausgedrückt werden muss, wenn der in der C-Sprache definierte undurchsichtige Zeiger aus Swift gelesen wird. Ich werde.

Angenommen, Sie haben die folgende C-Quelle.

struct CatImpl;
struct Cat {
	CatImpl * impl;
}
void PassCat(const Cat * a);
void PassCatImpl(const CatImpl * b);

In Swift sehen die beiden Funktionen folgendermaßen aus:

func PassCat(a: UnsafePointer<Cat>?)
func PassCatImpl(b: OpaquePointer?)

Es kann über den Konstruktor in und aus "UnsafePointer " konvertiert werden.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public init(_ from: OpaquePointer)
    ...
}

public struct OpaquePointer : Hashable {
	...
    public init<T>(_ from: UnsafePointer<T>)
    ...
}

CVaListPointer

Beim Umgang mit Argumenten variabler Länge in C verwenden wir die spezielle Notation "..." und den speziellen Typ "va_list", aber der Zeiger für den Umgang mit "va_list" ist "CVaListPointer".

AutoreleasingUnsafeMutablePointer

Nicht untersucht. Wenn ich einen Zeiger mit dem Modifikator "__autoreleasing" in Objective-C von Swift betrachte, fühle ich mich so, aber ich verstehe nicht.

Optional, Zeiger und Nullbarkeit

Der Zeigertyp von Swift kann nicht NULL sein. Mit anderen Worten, "UnsafePointer " ist ein Nicht-Null-Zeiger. Ein nullbarer Zeiger wird mit Optional als "UnsafePointer ?" Dargestellt. ** Swift kann auch Zeiger mit Null-Sicherheitsmechanismen wie der If-Let-Syntax verarbeiten. ** ** **

Es gibt eine optionale Version des Konvertierungskonstruktors zwischen "UnsafePointer ", "UnsafeMutablePointer " und "OpaquePointer". Wenn nil als Argument übergeben wird, gibt der Konstruktor auch nil zurück.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public init?(_ from: OpaquePointer?)
    ...
    public init?(_ other: UnsafeMutablePointer<Pointee>?)
    ...
}

UnsafeBufferPointer <T> ist ein nullbarer Zeiger seines ursprünglichen Typs. Wenn der vom Konstruktor empfangene Zeiger von Anfang an als optional empfangen und nil an ihn übergeben wird, ist die Eigenschaft baseAddress nil.

Grundlegender Zugang

Auf Verweise auf "UnsafePointer " kann mit der Eigenschaft "pointee" zugegriffen werden. In der Sprache C wurde es mit dem Dereferenzierungsoperator (*) und dem Pfeiloperator (->) geschrieben. Da außerdem ein Indexzugriff möglich ist, kann auf den kontinuierlich zugewiesenen Speicherbereich wie auf ein Array zugegriffen werden.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public var pointee: Pointee { get }
    ...
    public subscript(i: Int) -> Pointee { get }
    ...
}

Dies ist in der veränderlichen Version beschreibbar.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public var pointee: Pointee { get nonmutating set }
	...
    public subscript(i: Int) -> Pointee { get nonmutating set }
	...
}

Die untypisierte Raw-Version verfügt nicht über diese Eigenschaften. Wenn Sie keinen Typ zuweisen, können Sie nicht auf das Referenzziel zugreifen.

Drei Zustände des Zeigers

Es gibt drei Zustände im Speicher, auf die UnsafePointer <T> zeigt.

Die Unterscheidung zwischen diesen drei Zuständen ist ein wichtiges Konzept, das Swifts Zeigertyp untermauert. Diese drei Zustände sind vom Typsystem nicht zu unterscheiden. Der Programmierer muss genau wissen, in welchem Zustand sich der Zeiger befindet, mit dem er sich befasst.

Dies ist jedoch keine von Swift hinzugefügte Spezifikation, sondern ein Konzept, das im Wesentlichen für Zeiger existiert. Dies wird unten erklärt.

Sicherer Speicher

Der zugewiesene Zustand bedeutet, dass der Speicherbereich, auf den der Zeiger zeigt, gesichert ist. Umgekehrt bedeutet der nicht zugewiesene Zustand, dass der Zeiger NULL ist oder der Speicherbereich, auf den er zeigt, freigegeben wird.

Hier entspricht die Größe des Speicherbereichs, der von "UnsafePointer " verarbeitet wird, nicht unbedingt der Größe eines Werts. Es kann auch in Reihe zugewiesene Speicherbereiche verarbeiten, sodass mehrere Elemente gespeichert werden können.

Der Speicher kann durch die statische Methode der mutierten Version des Zeigers "zuweisen" zugewiesen werden.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
   	...
    public static func allocate(capacity count: Int) -> UnsafeMutablePointer<Pointee>
    ...
}

Das Argument "count" ist die Anzahl der aufeinanderfolgenden Speicherbereiche, die zugewiesen werden sollen. Diese Methode passt die Ausrichtung und den Schritt entsprechend dem Werttyp "Pointee" an. Die Ausrichtung ist eine Einschränkung des Verhältnisses der Speicheradresse, in der der Wert platziert wird, zum Wert der Adresse. Wenn die Ausrichtung beispielsweise 8 ist, ist die Speicheradresse immer ein Vielfaches von 8. Sie können es mit MemoryLayout <T> .alignment erhalten. Der Schritt ist der Wert, wie viele Bytes jeder Wert platziert wird, wobei die Adresse beim fortlaufenden Sichern verschoben wird. Wenn ein Typ beispielsweise eine Speichergröße von 5 Bytes, aber einen Schritt von 8 Bytes hat, werden jedem Element 8 Bytes zugewiesen, wobei 3 Bytes frei bleiben. Sie können es mit MemoryLayout <T> .stride erhalten. Diese Werte werden vom Compiler festgelegt und können vom Programmierer nicht beliebig festgelegt werden.

Für den untypisierten UnsafeRawPointer sind die Parameter für allocate unterschiedlich, da wir diese Werte nicht kennen.

public struct UnsafeMutableRawPointer : Strideable, Hashable {
    public static func allocate(bytes size: Int, alignedTo: Int) -> UnsafeMutableRawPointer
}

Es wurde entwickelt, um die reine Anzahl von Bytes und den Ausrichtungswert anzugeben. Wenn Sie kontinuierlich reservieren möchten, müssen Sie die Größe unter Berücksichtigung des oben genannten Schrittes berechnen.

Beide Rückgabewerte sind nicht optional, da der Zeiger auf den zugewiesenen Speicherbereich nicht null ist. Diese statischen Methoden sind auch in der veränderlichen Version des Typs definiert, da ich mit der Zuweisung eines unveränderlichen Speicherbereichs nicht zufrieden bin.

Rufen Sie die Freigabemethode auf, um den zugewiesenen Speicher freizugeben.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    public func deallocate(capacity: Int)
}

Speicherinitialisierungsstatus

Initialisiert ist ein Konzept, das angibt, ob im Speicherbereich ein Wert vorhanden ist oder nicht. Der gerade zugewiesene Speicherbereich ist eigentlich nur ein Speicherbereich, und der Wert existiert dort noch nicht, er befindet sich in einem nicht initialisierten Zustand.

Ob es initialisiert oder nicht initialisiert ist, hängt davon ab, wann Sie auf die Referenz des Zeigers zugreifen und den Wert ** lesen ** und ** schreiben **.

Wenn Sie einen Wert aus einem nicht initialisierten Speicherbereich lesen, wissen Sie nicht, wie der Status im Speicher ist. Daher kann er lächerliche Werte enthalten und es besteht die Gefahr eines Absturzes. Ich denke, das ist leicht zu verstehen. Das Interessante ist, wenn es ums Schreiben geht.

Betrachten Sie im Allgemeinen die Variablen, die durch Swifts "var" definiert sind. Angenommen, Sie haben einen Cat-Typ (Referenztyp) und einen CatHouse-Typ (Werttyp), der ihn enthält, wie unten gezeigt.

class Cat {
}

struct CatHouse {
    var cat: Cat?
}

Angenommen, der unten gezeigte Typ "App" hat eine Eigenschaft vom Typ "CatHouse" und wird in der Methode "update" neu geschrieben.

class App {
    init (a: CatHouse) {
        self.a = a
    }

    var a: CatHouse
    
    func update(b: CatHouse) {
        self.a = b
    }
}

Zu diesem Zeitpunkt erhöht der ARC-Mechanismus von swift den Referenzzähler von "cat", den CatHouse von "b" hat, um 1, aber eine weitere Sache, an die man sich erinnern sollte, ist, dass er ursprünglich in "a" ist. Der Vorgang des Dekrementierens des Referenzzählers "cat", den das alte CatHouse ** hatte, wird ausgeführt. Wenn also eine Kopie eines Werts schnell erstellt wird, wird sie im Allgemeinen durch die Kopie gelöscht ** der alte Wert wird zerstört **.

Lassen Sie uns nun darüber nachdenken, einen Wert in das Referenzziel des Zeigers zu schreiben. Wenn Sie einen Wert in das Referenzziel des Zeigers schreiben, entspricht dies dem Vorhandensein einer Variablen. Daher muss der ursprüngliche Wert zerstört werden. Aber was ist, wenn Sie es gerade gesichert und noch keinen Wert geschrieben haben? In diesem Zustand wäre es schlecht, wenn der ursprüngliche Wert zerstört würde. Dies liegt daran, dass der Wert nicht geschrieben wird und sich in einem zufälligen Speicherzustand befindet.

Daher muss zwischen initialisiert und nicht initialisiert unterschieden werden. Die pointee-Eigenschaft von UnsafePointer <T> ist eine Konvention, die nur verwendet werden sollte, wenn sie initialisiert wurde. Verwenden Sie die Methode "initialize", um in den nicht initialisierten Speicherbereich zu schreiben, und die Methode "deinitialize", um den initialisierten Speicherbereich auf "nicht initialisiert" zurückzusetzen.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public func initialize(to newValue: Pointee, count: Int = default)
    ...
    public func deinitialize(count: Int = default) -> UnsafeMutableRawPointer
    ...
    public func move() -> Pointee
    ...
}

Da es nun möglich war, mehrere Elemente im Speicherbereich zuzuweisen, haben diese Methoden ein Argument "count". Füllen Sie für "Initialisieren" die Anzahl der Elemente mit dem Wert, der durch das Argument "bis" angegeben wird. Zu diesem Zeitpunkt wird der ursprüngliche Speicherbereich nicht zerstört. Umgekehrt zerstört die Methode "deinitialize" nur den Wert. Die "move" -Methode ist "deinitialize", wenn die Anzahl der Elemente eins ist, und gibt den Wert als Rückgabewert zurück. Sie können sehen, dass die Verschiebungssemantik in C ++ mit genau demselben Namen wie "std :: move" realisiert wird.

Ich werde experimentieren. Stellen Sie sicher, dass "init" und "deinit" in "Cat" protokolliert sind.

class Cat {
    init () {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

Führen Sie dann die folgende Funktion aus.

func test1() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
    p.move()
}

Ich habe versucht, "freigeben" mit "verschieben" zu versehen. Die Ausgabe wird wie folgt sein.

init
deinit

Lassen Sie uns nun eine Version erstellen, die sich nicht bewegt.

func test2() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
}

Dann wird deinit nicht mehr durchgeführt.

init

Obwohl der Speicherbereich freigegeben wurde, wurde die Verarbeitung zum Reduzieren des von ihm gehaltenen "Cat" -Zählers nicht ausgeführt, da die Verarbeitung zum Zerstören des dort geschriebenen "CatHouse" nicht durchgeführt wurde und ein Speicherverlust aufgetreten ist. Ich habe es getan.

Versuchen Sie auch, auf dem Weg "pointee" zu verwenden, um alte Werte zu löschen.

func test3() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
    p.pointee = CatHouse(cat: Cat())
    p.move()
}
init
init
deinit
deinit

Sie können sehen, dass es zweimal korrekt erstellt und zweimal gelöscht wurde.

Was ist, wenn Sie versuchen, den Wert vor dem Initialisieren zu schreiben?

func test4() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.pointee = CatHouse(cat: Cat())
    p.initialize(to: CatHouse(cat: Cat()))
    p.move()
}
init
init
deinit

Wie Sie sehen können, hat eine "Katze" Speicher verloren. Der "Pointee", der vor der "Initialisierung" geschrieben wurde, wurde während der "Initialisierung" mit "Initialisierung ohne destruktive Verarbeitung" überschrieben, so dass die Gegenoperation der "Katze" übersprungen und durchgesickert war. ..

Und davor zerstörte dieser Code ** den nicht initialisierten Bereich beim Schreiben in pointee ** Es besteht also auch die Gefahr eines Absturzes.

Werteaustausch zwischen Zeigern und Verschiebungssemantik

Angenommen, Sie haben zwei zugewiesene Speicher, von denen einer initialisiert wurde. Mit anderen Worten, nehmen wir an, es gibt einen Wert auf einer Seite. Zu diesem Zeitpunkt gibt es beim Verschieben des Werts vom vorhandenen Zeiger zum anderen Zeiger abhängig von den folgenden Bedingungen 2x2 Muster.

Das Kopieren von Werttypen ist in Swift schnell, aber wenn Sie einen Referenztyp als Eigenschaft haben, z. B. "CatHouse", müssen Sie den Zähler für diese Referenz beim Kopieren um 1 erhöhen, was den Overhead verursacht. Wenn der Wert der Kopierquelle anschließend verworfen wird, wird der Zähler zu diesem Zeitpunkt um 1 dekrementiert, sodass er um 1 erhöht und um 1 dekrementiert wird. Daher kann dieser nutzlose Overhead beseitigt werden, indem der Wert der Kopierquelle verworfen und der Wert gleichzeitig dem Kopierziel angezeigt wird. Dies wird in C ++ als Verschiebungsoperation bezeichnet, aber der Zeigertyp von Swift verfügt über eine Methode für diese Verschiebung.

Wie bereits erwähnt, wurde der Vorgang des Schreibens eines Werts in den nicht initialisierten Speicher als "Initialisieren" bezeichnet. Andererseits wird der Vorgang des Schreibens eines Wertes in den initialisierten Speicher als "Zuweisen" bezeichnet. Diese beiden sind gewöhnliche Kopien. Und es gibt diese Verschiebungsoperationsversionen mit dem Präfix "Verschieben".

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public func initialize(from source: UnsafePointer<Pointee>, count: Int)
    ...
    public func moveInitialize(from source: UnsafeMutablePointer<Pointee>, count: Int)
    ...
    public func assign(from source: UnsafePointer<Pointee>, count: Int)
    ...
    public func moveAssign(from source: UnsafeMutablePointer<Pointee>, count: Int)
    ...
}

In der Verschiebungsversion ist "Quelle" veränderbar. Dies liegt daran, dass eine Entsorgungsoperation durchgeführt wird.

Die Initialisierung / Zerstörung kann für Raw-Systeme nicht gesteuert werden. Weil der Typ unbekannt ist.

Speicherstatus und Puffertyp

Der Typ "BufferPointer" verfügt nicht über Methoden wie Zuweisung und Initialisierung. Diese Speicheroperationen werden nach Zeigertyp ausgeführt, und der Puffer verhält sich wie eine Ansicht darauf.

Konvertierung zwischen getippt und untypisiert

Die Konvertierung vom typisierten Zeiger "UnsafePointer " zum untypisierten Zeiger "UnsafeRawPointer" ist im Konstruktor möglich.

public struct UnsafeRawPointer : Strideable, Hashable {
	...
    public init<T>(_ other: UnsafePointer<T>)
    ...
}

Die Konvertierung von einem untypisierten Zeiger in einen typisierten Zeiger ist mit dem Konstruktor jedoch nicht möglich. Stattdessen gibt es zwei dedizierte Methoden.

public struct UnsafeRawPointer : Strideable, Hashable {
	...
    public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafePointer<T>
    ...
    public func assumingMemoryBound<T>(to: T.Type) -> UnsafePointer<T>
    ...
}

Anscheinend scheint "UnsafeRawPointer" im Zusammenhang mit dem oben erwähnten strengen Aliasing statisch zu verfolgen, mit welchem Typ "T" sein Speicherbereich derzeit behandelt wird. Dies wird als Bindung bezeichnet.

Wenn es als "UnsafeRawPointer" zugewiesen wird, ist es ungebunden, und die Methode, die es an einen Typ "T" bindet, ist "bindMemory". Gleichzeitig wird "UnsafePointer " zurückgegeben. Für Speicher, der bereits an "T" gebunden ist, können Sie die Methode "acceptMemoryBound" verwenden.

Es gibt auch eine Methode namens "initializeMemory", die nicht initialisierten Speicher initialisiert, während er in T eingegeben wird.

Der Bindungszustandsübergang hier ist in dem oben erwähnten Dokument beschrieben. Bindungsspeichertyp

Ende

Es bietet die für Sprachfunktionen erforderlichen Funktionen ohne Verwendung einer dedizierten Syntax für Zeiger, verarbeitet Typinformationen generisch und ist nullsicher und bietet Operationsmethoden klar organisierte Konventionen für die drei Speicherzustände. Ich denke, dass es sehr gut gemacht ist, weil es auch die Bewegungssemantik unterstützen kann.

Ich finde es interessant, es mit Rust und C ++ zu vergleichen. In diesen Sprachen sind erstklassige Zeiger Rohzeiger, und intelligente Zeiger mit Referenzzählern usw. werden als generische Typen bereitgestellt. Im Fall von Swift ist dies jedoch umgekehrt, und der erstklassige Zeiger wird als intelligenter Zeiger und der Rohzeiger als generischer Typ bereitgestellt. Ich denke, diese Umkehrung ist eine gute Balance beim Entwerfen einer Sprache, die zum Schreiben von Apps, aber auch für niedrige Ebenen verwendet werden kann.

Recommended Posts

Großartiger Kommentar vom Typ Swift-Zeiger
Konvertiert vom String-Zeiger der C-Sprache zum Swift-String-Typ
[Swift] Aufzählungstyp für gemeinsam genutzte Typen
[Swift] Zusammenfassung über den Bool-Typ