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_unlockedlooks 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 toCustomDebugStringConvertibleis safe here because the standard library knows thatOptionalconforms. 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_unlockedthen 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
CustomDebugStringConvertiblecan also be printed withString(describing:). -
If no matching conformance was found, the final fallback is a function named
_adHocPrint_unlocked, which uses the value’sMirrorrepresentation 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
descriptionproperty 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.