Pointer manipulation and usage in Swift are described in detail

  • 2020-05-12 06:16:24
  • OfStack

Apple expects Pointers in Swift to minimize debuting, so Pointers in Swift are mapped to a generic type and are abstract. This makes it difficult to use Pointers in Swift to some extent, especially for developers who are not familiar with Pointers and have little experience with them (including myself), using Pointers in Swift is a real challenge. In this article, I hope to start with the basics and summarize some common ways and scenarios for using Pointers in Swift. This article assumes that you at least know what a pointer is. If you're not sure about the concept of a pointer itself, it should be helpful to read this 5 minute C tutorial on Pointers (or its Chinese version).

preliminary

In Swift, Pointers are represented by a special type, UnsafePointer < T > . It follows Cocoa's 1 consistent immutable principle, UnsafePointer < T > It's also immutable. And, of course, it also has a variable, UnsafeMutablePointer < T > . Most of the time, Pointers in C are introduced into Swift in these two types: C's const modified pointer corresponds to UnsafePointer (the most common would be const char * for C strings), and other mutable Pointers correspond to UnsafeMutablePointer. In addition, there is an UnsafeBufferPointer in Swift that represents a set of continuous data Pointers < T > , an opaque pointer COpaquePointer that represents a non-complete structure, and so on. In addition, as you may have noticed, you can be sure that the pointer types to the content are all generic struct, and you can use this generic to constrain the type of pointer to provide a definite security.

For 1 UnsafePointer < T > Type, which we can evaluate by the memory property, if the pointer is mutable UnsafeMutablePointer < T > Type, we can also assign it via memory. For example, if we want to write a counter that USES Pointers to manipulate memory directly, we can do this:


func incrementor(ptr: UnsafeMutablePointer<Int>) { 
    ptr.memory += 1 

var a = 10 
incrementor(&a) 
a  // 11 

Similar to C's pointer, we can pass a pointer to a variable to a method that accepts a pointer as an argument by putting an & before the variable name. In incrementor above we changed what the pointer was pointing to by directly manipulating the memory property.

Similar to this is the use of the inout keyword for Swift. We also use the ampersand symbol to indicate the address when we pass variables into the functions of the inout parameter. But the difference is that inside the body of the function we don't have to deal with pointer types, we can just manipulate the parameters.


func incrementor1(inout num: Int) { 
    num += 1 

var b = 10 
incrementor1(&b) 
b  // 11 

There is no way to get an instance of UnsafePointer directly from this symbol in Swift, although the meaning of ampersand is the "address of a variable" as in C. Note that this 1 is different from C:


// Unable to compile  
let a = 100 
let b = &a 

Pointer initialization and memory management

In Swift you cannot directly fetch the address of an existing object, but you can still create a new UnsafeMutablePointer object. Unlike the automatic memory management of other objects in Swift, the management of Pointers requires us to manually apply for and release memory. There are three possible states of 1 UnsafeMutablePointer memory:

1. Memory is not allocated, which means it is an null pointer, or it has been freed before;
2. Memory is allocated, but the value has not been initialized;
3. Memory is allocated and the value has been initialized.

Only the pointer in the third state is guaranteed to work properly. The initialization method for UnsafeMutablePointer (init) does all the work of converting from other types to UnsafeMutablePointer. If we want to create a new pointer, we need to use alloc: this class method. This method takes 1 num: Int as a parameter, and requests the system for memory of the corresponding generic type for the number of num. The following code applies for an Int memory and returns a pointer to that memory:


var intPtr = UnsafeMutablePointer<Int>.alloc(1) 
// "UnsafeMutablePointer(0x7FD3A8E00060)" 

The next thing you should do is initialize the contents of this pointer. You can do this using the initialize: method:

intPtr.initialize(10) 
// intPtr.memory for 10 

After initialization, we can manipulate the memory value pointed to by memory.

After use, it is best to release both the contents to which the pointer points and the pointer itself as soon as possible. The destroy used in conjunction with initialize is used to destroy the objects to which the pointer is pointing, while the dealloc used in conjunction with alloc is used to free previously requested memory. They should all be paired:


intPtr.destroy() 
intPtr.dealloc(1) 
intPtr = nil 

Note: destroy is not necessary here for "trivial values" like Int mapped to int in C, because these values are assigned to constant segments. However, for objects like classes or instances of structs, memory leaks can occur if the pairing is not guaranteed to be initialized and destroyed. So without special consideration, whatever is in memory, it's a good habit to make sure that initialize: matches destroy.

Pointer to an array

When Swift passes an array as a parameter to C API, Swift has already helped us with the conversion, which is a good example from Apple's official blog:


import Accelerate 
let a: [Float] = [1, 2, 3, 4] 
let b: [Float] = [0.5, 0.25, 0.125, 0.0625] 
var result: [Float] = [0, 0, 0, 0] 
vDSP_vadd(a, 1, b, 1, &result, 1, 4) 
// result now contains [1.5, 2.25, 3.125, 4.0625] 

For an C API that accepts an const array like a 1, the required type is UnsafePointer, and an array that does not const corresponds to UnsafeMutablePointer. When used, we directly pass the Swift array into the const parameter (a and b in the above example). For mutable arrays, ampersand is passed in (result in the above example).

For parameter passing, Swift is simplified and very convenient to use. But if we want to use Pointers to manipulate arrays directly, as we did with memory, we need to use a special type: UnsafeMutableBufferPointer.

Buffer Pointer is a contiguous pointer to memory, usually used to represent a collection type such as an array or dictionary.


var array = [1, 2, 3, 4, 5] 
var arrayPtr = UnsafeMutableBufferPointer<Int>(start: &array, count: array.count) 
// baseAddress Is the first 1 A pointer to an element  
var basePtr = arrayPtr.baseAddress as UnsafeMutablePointer<Int> 
basePtr.memory // 1 
basePtr.memory = 10 
basePtr.memory // 10 
// Under the 1 An element  
var nextPtr = basePtr.successor() 
nextPtr.memory // 2 

Pointer manipulation and conversion

withUnsafePointer

As mentioned above, in Swift you can't use the ampersand to get the address directly as you can in C. If we want to pointer to a variable, we can use the helper method withUnsafePointer. This method takes two arguments, the first of which is of any type of inout and the second of which is a closure. Swift converts the first input into a pointer, and then takes the converted Unsafe pointer as an argument to invoke the closure. It works like this:


var test = 10 
test = withUnsafeMutablePointer(&test, { (ptr: UnsafeMutablePointer<Int>) -> Int in 
    ptr.memory += 1 
    return ptr.memory 
}) 
test // 11 

Here we have done exactly the same thing as incrementor at the beginning of article 1, except that we do not need a method call to convert the value to a pointer. The benefits of doing this are obvious for pointer operations that only happen once, and it makes the intent of "we just want to do something with this pointer" clearer.

unsafeBitCast

unsafeBitCast is a very dangerous operation that forces the memory pointed to by a pointer to be bitwise converted to the target's type. Because this conversion takes place outside of Swift's type management, the compiler can't be sure that the type you get is actually correct. You have to know exactly what you're doing. Such as:


let arr = NSArray(object: "meow") 
let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), CFString.self) 
str // " meow "  

Since NSArray can store any NSObject object, when we use CFArrayGetValueAtIndex to evaluate it, we will get 1 UnsafePointer < Void > . Since we know that this is an String object, we can cast it directly to CFString.

A more common usage scenario for unsafeBitCast1 is conversion between different types of Pointers. Since the pointer itself occupies a fixed size of 1, there is no fatal problem in converting the pointer's type. This is common when working with some C API. For example, there are many C API that require the input to be void *, which corresponds to UnsafePointer in Swift < Void > . We can convert any pointer to UnsafePointer in the following manner.


func incrementor1(inout num: Int) { 
    num += 1 

var b = 10 
incrementor1(&b) 
b  // 11 
0

conclusion

Swift is designed with security as an important principle, and while it may be a bit verbose, it is important to reiterate that the direct use and manipulation of Pointers in Swift should be used as a last resort. They are never guaranteed to be safe. Moving from the traditional C code and the seamlessly integrated Objective-C code to Swift is not a small project, and our code base is sure to have some collaboration with C from time to time. We can certainly choose to rewrite some of the stale code using Swift, but for areas like security or performance critical, we may have no choice but to continue using C API. If we want to continue using those API, it would be helpful to know a little bit about basic Swift pointer manipulation and usage.

For new code, try to avoid Unsafe types, which means you can avoid a lot of unnecessary hassle. The biggest benefit Swift brings to developers is that it allows us to do faster and more focused development with more advanced programming ideas. Only by respecting this idea can we better enjoy the advantages of this new language. Obviously, this idea does not include using UnsafePointer everywhere.


Related articles: