Instead of implementing consume()
for each integer type separately,
you can implement a single generic method:
mutating func consume<T: BinaryInteger>() throws -> T {
let size = MemoryLayout<T>.size
guard idx + size <= bytes.count else {
throw Error.notEnoughBytes
}
defer { idx += size }
return bytes[idx..<idx + size].enumerated().reduce(0) {
$0 + T($1.element) << (8 * $1.offset)
}
}
which can be used for all signed and unsigned integer types (U)Int
, (U)Int8
, ..., (U)Int64
.
Instead of letting the compiler infer the return type from the context, one can alternatively pass it as a parameter:
mutating func consume<T: BinaryInteger>(_: T.Type) throws -> T { ... }
which is then – for example – called as
let crc32 = try buffer.consume(UInt32.self)
I would probably call the method get()
or read()
instead of consume()
.
Defining a local enum Error
type which conforms to the (global)
Error
protocol is possible, but might be confusing to the reader.
I would use a different name for the concrete error type, for example:
enum ReadError: Error {
case notEnoughBytes
}
Finally let's have a look how an error would be reported. The caller does not know the actual error type, so a typical calling sequence is:
do {
let someData = Data(bytes: [1])
var buffer = ConsumableByteArray(data: someData)
let crc32: UInt32 = try buffer.consume()
print(crc32)
} catch {
print(error.localizedDescription)
}
This produces the output:
The operation couldn’t be completed. (MyProg.ConsumableByteArray.ReadError error 0.)
This can be improved by adopting the LocalizedError
protocol
(see for example How to provide a localized description with an Error type in Swift? on
Stack Overflow):
enum ReadError: Error, LocalizedError {
case notEnoughBytes
public var errorDescription: String? {
switch self {
case .notEnoughBytes:
return "Not enough bytes in buffer"
}
}
}
Now the error output of the above program becomes
Not enough bytes in buffer
You can even store additional information about the error in associated values:
enum ReadError: Error, LocalizedError {
case notEnoughBytes(available: Int, needed: Int)
public var errorDescription: String? {
switch self {
case .notEnoughBytes(let available, let needed):
return "Not enough bytes in buffer (available: \(available), needed: \(needed))"
}
}
}
Then by throwing
throw ReadError.notEnoughBytes(available: bytes.count - idx, needed: size)
an error message like
Not enough bytes in buffer (available: 1, needed: 4)
is produced.