All characters together are bounded by the length of the entire string, meaning if you iterate over all characters you don't get O(n^2) behaviour even though there's a nested loop.
But one character can make the entire string, so reading one character of unreasonable length is at worse O(n). It's unlikely to occur in practice outside of maliciously crafted strings, but it's certainly possible to make a couple-of-megabytes character (assuming there is no enforcement of a shorter byte limit).
I made an experiment via the Swift REPL with a gigantic one-character string:
let s = "a" + String(repeating: "\u{302}", count: 20_000_000)
s.unicodeScalars.count // fast!
s.count // slow! (about one second)
s == "a" // fast!
s.first == "a" // slow! (about one second)
s == "e" // slow! (about half a second)
s.first == "e" // slow! (about one second)
s == "é" // slow! (about half a second)
s.first == "é" // slow! (about one second)
s == "/" // slow! (about half a second)
s.first == "/" // slow! (about one second)
Interestingly, s == "a"
remains fast, but none of the others. I wonder what kind of shortcut it takes that isn't used with "e"
or "/"
.
And as a consequence:
"hello world!".contains(s) // about 10 seconds