Swift provides the CustomStringConvertible
and CustomDebugStringConvertible
protocols to allow types to provide custom textual descriptions of themselves.
If you read the documentation for these protocols, you’ll notice that accessing a value’s description
or debugDescription
directly is discouraged. Instead, you’re supposed to use the corresponding String
initializers, String(describing:)
and String(reflecting:)
.
Every value is string-convertible
The reason for this rule is that every value in Swift is convertible to a string, regardless of a type’s conformance to one or both of these protocols. The protocols only exist to allow customization of a type’s textual representation. In particular, don’t ever use these protocols as types or generic constraints.
That said, there’s certainly nothing wrong with calling description
on a concrete value in a local context. For example, there are at least three equivalent ways to convert a list of integers to strings:
(1...5).map { $0.description } // → ["1", "2", "3", "4", "5"]
(1...5).map { "\($0)" } // same
(1...5).map(String.init(describing:)) // same
Which one you prefer is largely a matter of taste.
Don’t use CustomStringConvertible
as a constraint
However, you should absolutely not use the following pattern, where a function argument is constrained to Custom[Debug]StringConvertible
, either with a generic constraint or a plain type specification:
func doSomething1<T>(with x: T) where T: CustomStringConvertible {
// ...
// Call x.description
}
func doSomething2(with x: CustomStringConvertible) {
// ...
// Call x.description
}
Both of these functions compile, but they needlessly constrain the set of accepted inputs — for example, they don’t accept types that only conform to CustomDebugStringConvertible
, even though that’s a perfectly valid way to provide a textual representation.
Instead, the function should accept any type, because anything is printable:
func doSomething3<T>(with x: T) {
// ...
// Call String(describing: x)
}
Note that you can’t call description
in the function body anymore, but you can call String(describing:)
.
How String(describing:)
works
When you call String(describing:)
on a value, the standard library follows a set of rules to find a suitable textual representation for the argument. Let’s take a quick look how this works.
-
String(describing:)
creates a new empty string and passes the argument and the empty string to a function named_print_unlocked
. This is the implementation::extension String { public init<Subject>(describing instance: Subject) { self.init() _print_unlocked(instance, &self) } }
-
_print_unlocked
looks like this:internal func _print_unlocked<T, TargetStream : TextOutputStream>( _ value: T, _ target: inout TargetStream ) { if _isOptional(type(of: value)) { let debugPrintable = value as! CustomDebugStringConvertible debugPrintable.debugDescription.write(to: &target) return } if case let streamableObject as TextOutputStreamable = value { streamableObject.write(to: &target) return } if case let printableObject as CustomStringConvertible = value { printableObject.description.write(to: &target) return } if case let debugPrintableObject as CustomDebugStringConvertible = value { debugPrintableObject.debugDescription.write(to: &target) return } let mirror = Mirror(reflecting: value) _adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false) }
The function first checks if the value to be printed is an
Optional
, and if so, prints the optional’sdebugDescription
. The force-cast toCustomDebugStringConvertible
is safe here because the standard library knows thatOptional
conforms. Printing optionals with their debug description is preferred because optionals are not suitable for display to the user anyway. -
If we’re not dealing with an optional,
_print_unlocked
then tests the value consecutively for conformance toTextOutputStreamable
,CustomStringConvertible
, andCustomDebugStringConvertible
, in that order. It uses the first match to generate the description.This is how a type that only conforms to
CustomDebugStringConvertible
can also be printed withString(describing:)
. -
If no matching conformance was found, the final fallback is a function named
_adHocPrint_unlocked
, which uses the value’sMirror
representation to print its components.
String(reflecting:)
essentially works the same way, only with the protocol conformances checked in a different order.
LosslessStringConvertible
I should also mention a third string conversion protocol: LosslessStringConvertible
. This protocol refines CustomStringConvertible
and adds a semantic constraint:
The
description
property of a conforming type must be a value-preserving representation of the original value.
The string representation of a value that conforms to LosslessStringConvertible
preserves all the information that’s needed to recreate the value. It’s a guarantee that you can use the string representation e.g. to serialize the value without loss of information.
All trivial types in the standard library (Bool
, the integer and floating-point types, Unicode.Scalar
) as well as Character
, String
, and Substring
conform to LosslessStringConvertible
.
An exception to the rule
When you rely on LosslessStringConvertible
semantics, you should absolutely access the description
property directly, despite the above advice to the contrary. As we have seen, the alternative String(describing:)
prefers TextOutputStreamable
’s representation over description
if it’s available, and you can’t be 100 % certain that representation is identical to the value’s description
, however unlikely any differences are.