Swift hat "UnsafePointer
Die Zeigertypen sind wie folgt.
Grundlegender Zeigertyp
UnsafePointer<T>
UnsafeMutablePointer<T>
UnsafeRawPointer
UnsafeMutableRawPointer
UnsafeBufferPointer<T>
UnsafeMutableBufferPointer<T>
UnsafeRawBufferPointer
UnsafeMutableRawBufferPointer
Für Sprachbrücke
OpaquePointer
CVaListPointer
AutoreleasingUnsafeMutablePointer<T>
Es gibt so viele.
Der grundlegendste Zeigertyp ist "UnsafePointer
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
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>)
...
}
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
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.
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
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 }
...
}
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
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.
Der Zeigertyp von Swift kann nicht NULL sein. Mit anderen Worten, "UnsafePointer
Es gibt eine optionale Version des Konvertierungskonstruktors zwischen "UnsafePointer
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.
Auf Verweise auf "UnsafePointer *
) 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.
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.
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
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)
}
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.
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.
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.
Die Konvertierung vom typisierten Zeiger "UnsafePointer
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
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
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.