Swift a ʻUnsafePointer
Les types de pointeurs sont les suivants.
Type de pointeur de base
UnsafePointer<T>
UnsafeMutablePointer<T>
UnsafeRawPointer
UnsafeMutableRawPointer
UnsafeBufferPointer<T>
UnsafeMutableBufferPointer<T>
UnsafeRawBufferPointer
UnsafeMutableRawBufferPointer
Pour le pont de langue
OpaquePointer
CVaListPointer
AutoreleasingUnsafeMutablePointer<T>
Il y en a tellement.
Le type de pointeur le plus basique est ʻUnsafePointer . Représente un pointeur vers une valeur de type T. La valeur T référencée est immuable. ʻUnsafePointer <T> ʻ lui-même est une structure, donc
letet
var` peuvent représenter l'immuabilité du pointeur lui-même.
func f(_ x: UnsafePointer<Int>) {
let a: UnsafePointer<Int> = x
var b: UnsafePointer<Int> = x
}
Ce qui précède est exprimé en langage C comme suit.
void f(const int * const x) {
const int * const a = x;
const int * b = x;
}
Pour ʻUnsafePointer
Mutability
Il existe une version mutable dans laquelle la valeur de la référence T peut être modifiée. Mutable est attaché au nom. À partir de la version immuable, il peut être converti avec un constructeur étiqueté «mutating». Inversement, vous pouvez convertir de la version mutable à la version immuable avec le constructeur sans étiquette.
public struct UnsafePointer<Pointee> : Strideable, Hashable {
...
public init(_ other: UnsafeMutablePointer<Pointee>)
...
}
public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
...
public init(mutating other: UnsafePointer<Pointee>)
...
}
ʻUnsafePointer avait le type T référencé comme paramètre de type, mais certaines versions n'ont pas cette information de type. Les éditions sans informations de type sont nommées avec «Raw». Par exemple,
const void * en langage C apparaît à Swift comme ce ʻUnsafeRawPointer
. Il est utilisé lorsque le type de la référence du pointeur est inconnu.
À l'origine, ʻUnsafePointer seul pouvait provoquer des bogues d'alias stricts dans certains codes, et ʻUnsafeRawPointer
avec la sémantique memcpy était nécessaire pour l'éviter. C'est vrai.
Les détails sont écrits dans le document au moment de la proposition standard. API UnsafeRawPointer
De plus, ce qui suit est facile à comprendre sur l'aliasing strict. (Traduction) Comprendre l'alias strict en C / C ++ ou-pourquoi # $ @ ## @ ^% Le compilateur ne me laisse pas faire ce que je veux!
La conversion entre typé et non typé sera discutée plus tard.
En langage C, les pointeurs sont souvent utilisés pour représenter des tableaux, mais pour les gérer dans un soi-disant tableau, il est nécessaire de connaître le nombre d'éléments ainsi que les pointeurs. Par conséquent, ʻUnsafeBufferPointer
ʻUnsafeBufferPointer Collection
.
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` est pour représenter le type de motif appelé pointeur opaque en langage C. Un pointeur opaque est un pointeur qui est pré-déclaré uniquement par le nom du type, mais qui n'a pratiquement aucune information sur l'accès à la destination référencée car la définition du type n'est pas visible. Il est également utilisé pour masquer les détails à l'intérieur de la bibliothèque de l'extérieur de la bibliothèque. Dans Swift, il n'arrive pas que seul le nom du type soit visible mais la définition soit invisible, mais ce type est utilisé car il faut l'exprimer lorsque le pointeur opaque défini en langage C est lu depuis Swift. Je vais.
Par exemple, supposons que vous ayez la source C suivante.
struct CatImpl;
struct Cat {
CatImpl * impl;
}
void PassCat(const Cat * a);
void PassCatImpl(const CatImpl * b);
Depuis Swift, les deux fonctions ressemblent à ceci:
func PassCat(a: UnsafePointer<Cat>?)
func PassCatImpl(b: OpaquePointer?)
ʻUnsafePointer
public struct UnsafePointer<Pointee> : Strideable, Hashable {
...
public init(_ from: OpaquePointer)
...
}
public struct OpaquePointer : Hashable {
...
public init<T>(_ from: UnsafePointer<T>)
...
}
CVaListPointer
Lorsque nous traitons des arguments de longueur variable en langage C, nous utilisons la notation spéciale ...
et le type spécial va_list
, mais le pointeur pour traiter va_list
est CVaListPointer
.
AutoreleasingUnsafeMutablePointer
Non étudié. En regardant un pointeur avec le modificateur __autoreleasing
dans Objective-C de Swift, je me sens comme ça, mais je ne comprends pas.
Le type de pointeur de Swift ne peut pas être NULL. En d'autres termes, ʻUnsafePointer est un pointeur non nul. Un pointeur Nullable est représenté en utilisant Optional comme ʻUnsafePointer <T>?
. ** Swift peut également gérer des pointeurs avec des mécanismes de sécurité nuls tels que la syntaxe ʻif let`. ** **
Il existe une version facultative du constructeur de conversion entre ʻUnsafePointer , ʻUnsafeMutablePointer <T>
et ʻOpaquePointer`, et si nil est passé comme argument, le constructeur renvoie également nil.
public struct UnsafePointer<Pointee> : Strideable, Hashable {
...
public init?(_ from: OpaquePointer?)
...
public init?(_ other: UnsafeMutablePointer<Pointee>?)
...
}
ʻUnsafeBufferPointer baseAddress
sera nulle.
Les références ʻUnsafePointer sont accessibles avec la propriété
pointee. En langage C, il a été écrit avec l'opérateur de déréférence (
*) et l'opérateur de flèche (
->`). De plus, comme l'accès en indice est possible, la zone de mémoire allouée en continu peut être accédée comme un tableau.
public struct UnsafePointer<Pointee> : Strideable, Hashable {
...
public var pointee: Pointee { get }
...
public subscript(i: Int) -> Pointee { get }
...
}
Ceci est accessible en écriture dans la version mutable.
public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
...
public var pointee: Pointee { get nonmutating set }
...
public subscript(i: Int) -> Pointee { get nonmutating set }
...
}
La version Raw non typée ne possède pas ces propriétés. Si vous n'attribuez pas de type, vous ne pourrez pas accéder à la destination de référence.
Il y a trois états dans la mémoire pointés par ʻUnsafePointer
La distinction entre ces trois états est un concept important qui sous-tend le type de pointeur de Swift. Ces trois états sont indiscernables par le système de types. Le programmeur a besoin de savoir exactement dans quel état se trouve le pointeur.
Cependant, ce n'est pas une spécification ajoutée par Swift, mais un concept qui existe essentiellement pour les pointeurs. Ceci est expliqué ci-dessous.
L'état alloué signifie que la zone mémoire pointée par le pointeur est sécurisée. Inversement, l'état non alloué signifie que le pointeur est NULL ou que la zone mémoire vers laquelle il pointe est libérée.
Ici, la taille de la zone mémoire gérée par ʻUnsafePointer
La mémoire peut être allouée par la méthode statique de la version mutante du pointeur, ʻallocate`.
public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
...
public static func allocate(capacity count: Int) -> UnsafeMutablePointer<Pointee>
...
}
L'argument «count» est le nombre de zones mémoire consécutives à allouer. Cette méthode ajuste l'alignement et la foulée en fonction du type de valeur «Pointe». L'alignement est une contrainte sur le rapport de l'adresse mémoire dans laquelle la valeur est placée à la valeur de l'adresse. Par exemple, si l'alignement est 8, l'adresse mémoire sera toujours un multiple de 8. Vous pouvez l'obtenir avec MemoryLayout <T> .alignment
. La foulée est la valeur du nombre d'octets que chaque valeur est placée avec l'adresse décalée lors de la sécurisation consécutive. Par exemple, si un type a une taille de mémoire de 5 octets mais une foulée de 8 octets, alors 8 octets sont alloués pour chaque élément, laissant 3 octets libres. Vous pouvez l'obtenir avec MemoryLayout <T> .stride
. Ces valeurs sont déterminées par le compilateur et ne peuvent pas être définies arbitrairement par le programmeur.
Pour le ʻUnsafeRawPointer non typé, les paramètres de ʻallocate
sont différents car nous ne connaissons pas ces valeurs.
public struct UnsafeMutableRawPointer : Strideable, Hashable {
public static func allocate(bytes size: Int, alignedTo: Int) -> UnsafeMutableRawPointer
}
Il est conçu pour spécifier le nombre pur d'octets et la valeur d'alignement. Si vous souhaitez réserver en continu, vous devez calculer la taille en tenant compte de la foulée mentionnée ci-dessus.
Les deux valeurs de retour ne sont pas facultatives car le pointeur vers la zone de mémoire allouée est non nul. En outre, ces méthodes statiques sont définies dans la version mutable du type car je ne suis pas satisfait d'allouer une zone de mémoire immuable.
Pour libérer la mémoire allouée, appelez la méthode deallocate
.
public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
public func deallocate(capacity: Int)
}
Initialisé est un concept qui indique si une valeur existe ou non dans la zone mémoire. La zone mémoire qui vient d'être allouée n'est en réalité qu'une zone mémoire, et la valeur n'existe pas encore, elle est dans un état non initialisé.
Qu'il soit initialisé ou non initialisé dépend du moment où vous accédez à la référence du pointeur et ** lisez ** et ** écrivez ** la valeur.
Si vous lisez une valeur dans une zone mémoire non initialisée, vous ne savez pas quel est l'état de la mémoire, donc elle peut contenir des valeurs ridicules et il y a un risque de plantage. Je pense que c'est facile à comprendre. Ce qui est intéressant, c'est quand il s'agit d'écrire.
En général, considérez les variables définies par «var» de Swift. Supposons que vous ayez un type Cat
(type de référence) et un type CatHouse
(type valeur) qui le contient, comme indiqué ci-dessous.
class Cat {
}
struct CatHouse {
var cat: Cat?
}
Supposons que le type ʻApp comme indiqué ci-dessous a une propriété du type
CatHouse, et ceci est réécrit dans la méthode ʻupdate
.
class App {
init (a: CatHouse) {
self.a = a
}
var a: CatHouse
func update(b: CatHouse) {
self.a = b
}
}
A ce moment, le mécanisme ARC de swift augmentera de 1 le compteur de référence de cat
que CatHouse ofb
a, mais une autre chose à retenir est qu'il est à l'origine dans ʻa. Le processus de décrémentation du compteur de référence
cat` que l'ancien CatHouse avait ** se produit. Ainsi, en général, lorsqu'une copie d'une valeur se produit en swift, elle sera effacée par la copie ** l'ancienne valeur sera détruite **.
Maintenant, pensons à écrire une valeur dans la destination de référence du pointeur. Lors de l'écriture d'une valeur dans la destination de référence du pointeur, cela revient à y avoir une variable, il est donc nécessaire de détruire la valeur d'origine. Mais que faire si vous venez de le sécuriser et n'avez pas encore écrit de valeur? Dans cet état, ce serait mauvais si la valeur d'origine était détruite. C'est parce qu'il s'agit d'un état de mémoire aléatoire sans aucune valeur écrite.
Par conséquent, il est nécessaire de faire la distinction entre initialisé et non initialisé. La propriété pointee
de ʻUnsafePointer est une convention qui ne doit être utilisée que lorsqu'elle a été initialisée. Utilisez la méthode ʻinitialize
pour écrire dans la zone mémoire non initialisée, et la méthode deinitialize
pour renvoyer la zone mémoire initialisée à non initialisée.
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
...
}
Maintenant, comme il était possible d'allouer plusieurs éléments dans la zone mémoire, ces méthodes ont un argument count
. Pour ʻinitialize, remplissez les éléments
countavec la valeur spécifiée par l'argument
to. À ce stade, la zone de mémoire d'origine n'est pas détruite. Inversement, la méthode
deinitializedétruit uniquement la valeur. La méthode
move est
deinitializelorsque le nombre d'éléments est égal à un et renvoie la valeur comme valeur de retour. Vous pouvez voir que la sémantique de déplacement est réalisée avec exactement le même nom que
std :: move` en C ++.
Je vais expérimenter. Assurez-vous que les ʻinit et
deinitde
Cat` sont consignés.
class Cat {
init () {
print("init")
}
deinit {
print("deinit")
}
}
Exécutez ensuite la fonction suivante.
func test1() {
var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
defer {
p.deallocate(capacity: 1)
}
p.initialize(to: CatHouse(cat: Cat()))
p.move()
}
J'ai essayé de préfixer «deallocate» avec «defer». La sortie sera la suivante.
init
deinit
Maintenant, faisons une version qui ne «bouge» pas.
func test2() {
var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
defer {
p.deallocate(capacity: 1)
}
p.initialize(to: CatHouse(cat: Cat()))
}
Ensuite, la déinit n'est plus terminée.
init
Bien que la zone de mémoire ait été libérée, le traitement pour réduire le compteur "Cat" qu'elle détenait n'a pas été exécuté car le traitement pour détruire la "CatHouse" qui y était écrite n'a pas été effectué et la mémoire a fui. Je l'ai fait.
Essayez également d'utiliser «pointee» pour effacer les anciennes valeurs.
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
Vous pouvez voir qu'il a été créé deux fois correctement et supprimé deux fois.
Et si vous essayez d'écrire la valeur avant ʻinitialize`?
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
Comme vous pouvez le voir, un «Cat» a perdu de la mémoire. Le pointee
écrit avant ʻinitialize a été écrasé par ** initialisation sans destruction ** pendant ʻinitialize
, donc l'opération de compteur de cat
a été ignorée et a été divulguée. ..
Et avant cela, ce code ** détruisait la zone non initialisée lors de l'écriture dans pointee
**
Il y a donc aussi un risque de crash.
Supposons que vous ayez deux mémoires allouées, dont l'une a été initialisée. En d'autres termes, supposons qu'il y ait une valeur d'un côté. À ce stade, lors du déplacement de la valeur du pointeur existant vers l'autre pointeur, il existe des modèles 2x2 en fonction des conditions suivantes.
--Si le pointeur de destination est initialisé ou non
La copie de types de valeur est rapide dans Swift, mais si vous avez un type de référence comme propriété, par exemple CatHouse
, vous devez incrémenter le compteur de cette référence de 1 lors de la copie, ce qui entraîne la surcharge. Si la valeur de la source de copie est ensuite rejetée, le compteur sera décrémenté de 1 à ce moment-là, de sorte qu'il sera incrémenté de 1 et décrémenté de 1. Par conséquent, s'il y a une opération de rejet de la valeur de la source de copie et en même temps d'affichage de la valeur vers la destination de copie, cette surcharge inutile peut être éliminée. Cela s'appelle une opération de déplacement en C ++, mais le type de pointeur de Swift a une méthode pour ce déplacement.
Comme mentionné précédemment, l'opération d'écriture d'une valeur dans la mémoire non initialisée s'appelait «initialiser». D'autre part, l'opération d'écriture d'une valeur dans la mémoire initialisée est appelée ʻassign. Ces deux sont des copies ordinaires. Et il y a ces versions d'opération de déplacement avec un préfixe appelé
move`.
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)
...
}
Dans la version de déplacement, source
est modifiable. En effet, il est soumis à une opération de rejet.
L'initialisation / la destruction ne peuvent pas être contrôlées pour les systèmes Raw. Parce que le type est inconnu.
Le type BufferPointer
n'a pas de méthodes telles que l'allocation et l'initialisation. Ces opérations de mémoire sont effectuées par type de pointeur et le tampon agit exactement comme une vue sur celui-ci.
La conversion du pointeur typé ʻUnsafePointer en pointeur non typé ʻUnsafeRawPointer
est possible dans le constructeur.
public struct UnsafeRawPointer : Strideable, Hashable {
...
public init<T>(_ other: UnsafePointer<T>)
...
}
Cependant, le pointeur non typé vers la conversion typée n'est pas possible avec le constructeur. Au lieu de cela, il existe deux méthodes dédiées.
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>
...
}
Apparemment, dans le contexte de l'aliasing strict mentionné ci-dessus, ʻUnsafeRawPointer semble avoir le compilateur suivre statiquement avec quel type
T` la zone mémoire est actuellement traitée. C'est ce qu'on appelle une liaison.
«Lorsqu'il est alloué en tant que UnsafeRawPointer», il n'est pas lié et la méthode qui le lie à un type «T» est «bindMemory». En même temps, ʻUnsafePointer est renvoyé. Pour la mémoire qui est déjà liée à
T, vous pouvez utiliser la méthode ʻassumingMemoryBound
.
Il existe également une méthode appelée ʻinitializeMemory` qui initialise la mémoire non initialisée en la tapant dans T.
La transition d'état de liaison ici est décrite dans le document susmentionné. Type de mémoire de liaison
Il fournit les fonctions requises pour les fonctions de langage sans utiliser une syntaxe dédiée pour les pointeurs, gère les informations de type de manière générique, est de sécurité nulle et fournit des méthodes d'opération avec des conventions clairement organisées pour les trois états de la mémoire. , Je pense que c'est très bien fait car il peut également supporter la sémantique de déplacement.
Je pense qu'il est intéressant de le comparer avec Rust et C ++. Dans ces langages, les pointeurs de première classe sont des pointeurs bruts, et les pointeurs intelligents avec des nombres de références, etc. sont fournis sous forme de types génériques. Cependant, dans le cas de Swift, cela est inversé, et le pointeur de première classe est fourni comme un pointeur intelligent et le pointeur brut est fourni comme un type générique. Je pense que ce renversement est un bon équilibre dans la conception d'un langage qui peut être utilisé pour écrire des applications, mais aussi pour les couches basses.