DEV Community

Binoy Vijayan
Binoy Vijayan

Posted on • Edited on

Swift Custom Array Implementation Using UnsafeMutablePointer

In this article, we explore a custom array implementation in Swift using UnsafeMutablePointer. This implementation offers insights into manual memory management, dynamic resizing, and conformance to protocols such as CustomDebugStringConvertible and Sequence. The goal is to provide a detailed overview of the internal workings of Swift arrays.

Overview

The custom array MyArray allows storage and manipulation of elements of any type T. It supports dynamic resizing, appending, insertion, and removal of elements, while ensuring memory safety through proper allocation and deallocation.

Key Features

  • Dynamic Resizing: Automatically adjusts the capacity of the array when it reaches its limit.

  • Memory Management: Uses UnsafeMutablePointer for low-level memory operations.

  • Sequence Conformance: Implements the Sequence protocol for iteration.

  • Debug Description: Provides a custom debug description for easier debugging.

Implementation Details

Properties

struct MyArray<T> : CustomDebugStringConvertible, Sequence {
    private var capacity: Int
    private var storage: UnsafeMutablePointer<T>
    private var size: Int

    var count: Int {
        return size
    }

    var isEmpty: Bool {
        return size == 0
    }

Enter fullscreen mode Exit fullscreen mode

capacity: The maximum number of elements the array can hold without resizing.

storage: A pointer to the array's memory storage.

size: The current number of elements in the array.

count: Returns the number of elements in the array.

isEmpty: Checks if the array is empty.

Initialiser

init(initialCapacity: Int = 2) {
        self.capacity = initialCapacity
        self.size = 0
        self.storage = UnsafeMutablePointer<T>.allocate(capacity: initialCapacity)
    }
Enter fullscreen mode Exit fullscreen mode

Resizing

private mutating func resize() {

   if size >= capacity {

     /* Double the capacity */
     let newCapacity = capacity * 2

     /* Allocating new storage with new capacity */
     let newStorage = UnsafeMutablePointer<T>.allocate(capacity: newCapacity)

     /* Copying the existing elements to the new storage */
     for i in 0..<count {
         newStorage[i] = storage[I]
     }

     /* Deallocating old storage */
     storage.deallocate()
     storage = newStorage
     capacity = newCapacity
   }
}
Enter fullscreen mode Exit fullscreen mode

The resize method doubles the capacity when needed and reallocates memory, copying existing elements to the new storage.

Adding Elements

public mutating func append(_ item: T) {
   resize()
   storage[size] = item
   size += 1
}
Enter fullscreen mode Exit fullscreen mode

The append method adds a new element to the end of the array, resizing if necessary.

Inserting Elements

public mutating func insert(_ item: T, at index: Int) {
   guard index >= 0 && index <= size else {
       fatalError("Index out of bounds")
   }
   resize()
   for i in stride(from: count, to: index, by: -1) {
       storage[i] = storage[i - 1]
   }
   storage[index] = item
   size += 1
}

Enter fullscreen mode Exit fullscreen mode

The insert method inserts an element at a specified index, shifting elements as needed.

Removing Elements

@discardableResult
public mutating func remove(at index: Int) -> T {
   guard index >= 0 && index < size else {
       fatalError("Index out of bounds")
   }
   let removedElement = storage[index]
   for i in index..<size - 1 {
      storage[i] = storage[i + 1]
   }
   size -= 1
   return removedElement
}
Enter fullscreen mode Exit fullscreen mode

The remove method removes an element at a specified index and returns it.

Clearing the Array

public mutating func removeAll() {
    // Deallocate the existing elements
    storage.deallocate()
    capacity = 2
    // Reinitialise the storage
    storage = UnsafeMutablePointer<T>.allocate(capacity: capacity)
        size = 0
}
Enter fullscreen mode Exit fullscreen mode

The removeAll method deallocates all elements and resets the array.

Subscript

subscript(index: Int) -> T {
   get {
       guard index >= 0 && index < size else {
           fatalError("Index out of bounds")
       }
       return storage[index]
   }
   set {
       guard index >= 0 && index < size else {
           fatalError("Index out of bounds")
       }
       storage[index] = newValue
   }
}
Enter fullscreen mode Exit fullscreen mode

The subscript allows getting and setting elements at a specified index.

Sequence Conformance

func makeIterator() -> AnyIterator<T> {
   var index = 0
   return AnyIterator {
       guard index < self.size else {
           return nil
       }
       let element = self.storage[index]
       index += 1
       return element
   }
}
Enter fullscreen mode Exit fullscreen mode

The makeIterator method provides an iterator for the array.

Debug Description

var debugDescription: String {
   var result = "["
   for i in 0..<size {
      result += "\(storage[I])"
      if i < size - 1 {
        result += ", "
      }
   }
   result += "]"
   return result
}
Enter fullscreen mode Exit fullscreen mode

The debugDescription property returns a string representation of the array.

Conclusion

This custom array implementation demonstrates how to manage memory manually and provides basic array functionalities. While UnsafeMutablePointer offers powerful capabilities, it requires careful handling to avoid memory leaks and ensure safety. This implementation serves as an educational example and can be extended for more advanced use cases.

Please find the complete source code Here

Top comments (0)