Skip to main content

[From C# to Swift] 03. Strings and Characters

String Literals & Initialization
#

1. Core Concepts
#

  • Concept Explanation: The String type in Swift is a fast, Unicode-compliant text processing tool. Its syntax design is lightweight, similar to C, but its underlying implementation is highly modern. Unlike Objective-C’s NSString, Swift’s String is a struct (Value Type), not a class.
  • Key Syntax: "" (Double quotes), """ (Multiline strings), String() (Initialization), isEmpty
  • Note:

Swift’s String type is bridged with Foundation’s NSString class. If you import Foundation, you can call NSString methods directly on a String without casting.

2. Example Analysis
#

let someString = "Some string literal value"

// Multiline String Literals
let quotation = """
The White Rabbit put on his spectacles.  "Where shall I begin,
please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""

// Initialize empty string
var emptyString = ""               // Empty string literal
var anotherEmptyString = String()  // Initialization syntax
if emptyString.isEmpty {
    print("Nothing to see here")
}

Logic Explanation: Swift uses double quotes to define single-line strings. Multiline strings are wrapped in three double quotes """, and the opening and closing quotes must be on their own lines. Indentation rules are determined by the position of the closing """, making code formatting much cleaner. The isEmpty property is an efficient way to check if a string is empty.

3. C#
#

Concept Correspondence: C# string also uses double quotes. Swift’s multiline strings are similar to the Raw String Literals ("""...""") introduced in C# 11 or the older Verbatim Strings (@"...").

C# Example:

// C#
string someString = "Some string literal value";

// C# 11 Raw String Literal (Similar to Swift's multiline handling)
string quotation = """
    The White Rabbit put on his spectacles.
    "Begin at the beginning," the King said.
    """;

// Empty string check
string emptyString = "";
bool isEmpty = string.IsNullOrEmpty(emptyString); // or emptyString.Length == 0

Key Differences Analysis:

  • Syntax: Swift’s multiline string indentation handling is very smart; it automatically ignores whitespace that aligns with the closing """. C# only achieved perfect support for this recently (C# 11) via Raw String Literals.
  • Behavior: Swift recommends using the isEmpty property, whereas C# developers are accustomed to using string.IsNullOrEmpty() or checking Length == 0.

Mutability & Value Types
#

1. Core Concepts
#

  • Concept Explanation: This is one of the biggest differences between Swift and many other languages. Swift’s String is a Value Type, not a Reference Type. Furthermore, whether a string is mutable depends entirely on whether it is declared as a variable (var) or a constant (let).
  • Key Syntax: var (Mutable), let (Immutable), struct (Value Type)

2. Example Analysis
#

var variableString = "Horse"
variableString += " and carriage"
// variableString is now "Horse and carriage"

let constantString = "Highlander"
// constantString += " and another Highlander"
// Compile error: a constant string cannot be modified

Logic Explanation: When a String is passed to a function or assigned to another variable, a copy actually occurs. Although the Swift compiler performs copy-on-write optimization (meaning it only copies when the content is actually modified), semantically, you own an independent copy of that string.

3. C#
#

Concept Correspondence: C#’s string is a Reference Type, but it is Immutable. To modify a string, C# typically creates a new object or uses StringBuilder.

C# Example:

// C#
string str = "Horse";
str += " and carriage"; // Actually creates a new string object and repoints to it

// C# has no direct equivalent to 'let' constant declarations to prevent modification via reassignment,
// unless using const (compile-time) or readonly (fields only).

Key Differences Analysis:

  • Syntax: Swift directly supports string modification (concatenation) using var, similar to an easy-to-use version of C#’s StringBuilder, but with the syntax of normal string operations.
  • Behavior: In C#, string variables store a Reference; in Swift, String is a Struct.
    • C#: string a = "hi"; string b = a; -> a and b point to the same memory on the heap.
    • Swift: var a = "hi"; var b = a; b += "!" -> Modifying b does not affect a, because a copy occurred (logically) upon assignment.

Concatenation & Interpolation
#

1. Core Concepts
#

  • Concept Explanation: Swift provides intuitive operators for concatenating strings, as well as powerful string interpolation, allowing variables or expressions to be embedded within strings.
  • Key Syntax: +, +=, append(), \(expression)

2. Example Analysis
#

let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2

var instruction = "look over"
instruction += string2

// String interpolation
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"

Logic Explanation: String interpolation uses the backslash and parentheses syntax \(...). This is more intuitive and strongly typed compared to traditional format strings.

3. C#
#

Concept Correspondence: Equivalent to C# string concatenation and String Interpolation.

C# Example:

// C#
int multiplier = 3;
// C# uses the $ sign and curly braces
string message = $"{multiplier} times 2.5 is {multiplier * 2.5}";

Key Differences Analysis:

  • Syntax: Swift uses \(), C# uses {} (combined with $).
  • Behavior: Both transform into efficient string construction calls at compile time. Note that Swift’s interpolation parentheses cannot contain unescaped backslashes or line breaks.

Unicode, Characters & Counting
#

1. Core Concepts
#

  • Concept Explanation: Swift’s String is built upon Unicode Scalar Values. Most notably, it supports Extended Grapheme Clusters. A “human-readable character” (like é or 🇹🇼) might consist of one or more Unicode scalars, but in Swift, they are all treated as a single Character.
  • Key Syntax: Character, count property, \u{n}

2. Example Analysis
#

let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}"          // e followed by acute accent
// Both are considered the same character in Swift

var word = "cafe"
word += "\u{301}" // Add acute accent
print("the number of characters in \(word) is \(word.count)")
// Prints "the number of characters in café is 4" (Although added, visually it's still one character, length remains unchanged)

Logic Explanation: This is the most powerful yet complex part of Swift string handling. The count property returns “how many characters it looks like to a human,” rather than how many bytes or 16-bit units are used underneath. Because it needs to calculate grapheme cluster boundaries, accessing count may require traversing the entire string and is not an O(1) operation.

3. C# Perspective
#

Concept Correspondence: C#’s char is 16-bit (UTF-16 code unit). C#’s string.Length returns the number of 16-bit units, not the actual character count.

C# Example:

// C#
string word = "cafe";
word += "\u0301"; // Add combining acute accent
Console.WriteLine(word.Length); 
// Prints 5 (Because e and the accent are two separate chars)

Key Differences Analysis:

  • Syntax: No major difference, but Swift’s Unicode escape uses \u{...} which can accommodate varying lengths of Hex, while C# uses \uXXXX or \UXXXXXXXX.
  • Behavior:
    • Emoji Handling: An Emoji (like 🐶) is typically 2 in C# string.Length (Surrogate Pair); in Swift .count is 1.
    • Performance: C#’s Length is O(1); Swift’s count requires string traversal, making it O(n). Care must be taken regarding performance if count is called frequently inside loops.

Accessing and Modifying Strings
#

1. Core Concepts
#

  • Concept Explanation: Due to the aforementioned Unicode complexity (variable character lengths), Swift does not allow using integer indices (e.g., str[0]) to access strings. You must use String.Index.
  • Key Syntax: startIndex, endIndex, index(before:), index(after:), index(_:offsetBy:)

2. Example Analysis
#

let greeting = "Guten Tag!"
// Access the first character
greeting[greeting.startIndex] // G

// Access specific position (e.g., the 7th character)
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index] // a

// Insert
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)

// Remove
welcome.remove(at: welcome.index(before: welcome.endIndex))

Logic Explanation: You cannot write greeting[7] directly; you must first calculate the String.Index object representing the “7th grapheme cluster” and then use it to retrieve the value. This is to prevent random access from cutting through multi-byte characters (like cutting an Emoji in half).

3. C#
#

Concept Correspondence: C# allows integer indexing str[i], but this accesses a char (UTF-16 code unit), which may not be a complete character.

C# Example:

// C#
string greeting = "Guten Tag!";
char c = greeting[7]; // Directly use integer index 'a'

// Insert and Remove (C# strings are immutable, creates new strings)
string welcome = "hello";
welcome = welcome.Insert(welcome.Length, "!");
welcome = welcome.Remove(welcome.Length - 1);

Key Differences Analysis:

  • Syntax: Swift’s indexing syntax is very verbose (index(_:offsetBy:)). This is to force developers to be aware of the cost of string traversal.
  • Behavior: In C#, you are used to for (int i=0; i<str.Length; i++), but in Swift, you should try to use for char in string or higher-order functions. If you need frequent random access, it is recommended to convert the String to an Array (Array(str)), but note that this loses some Unicode handling features and increases memory consumption.

Substrings
#

1. Core Concepts
#

  • Concept Explanation: When you slice a Swift string, the returned type is not String, but Substring. A Substring shares memory with the original string, which is a performance optimization.
  • Key Syntax: prefix(_:), [range], Substring type, String(substring)

2. Example Analysis
#

let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning is of type Substring; it reuses greeting's memory

// Convert back to String for long-term storage
let newString = String(beginning)

Logic Explanation: Although Substring is efficient, it is not suitable for long-term holding. As long as the Substring exists, the memory of the original complete String cannot be released.

3. C#
#

Concept Correspondence: C#’s Substring() method traditionally allocates new memory and copies the string. However, the concept of Span<char> or ReadOnlySpan<char> is very similar to Swift’s Substring—they are both “views” of the original memory.

C# Example:

// C# (Traditional)
string greeting = "Hello, world!";
int idx = greeting.IndexOf(',');
string beginning = greeting.Substring(0, idx); // Allocates new memory

// C# (High-performance Span)
ReadOnlySpan<char> span = greeting.AsSpan();
ReadOnlySpan<char> slice = span.Slice(0, idx); // Zero allocation, similar to Swift Substring

Key Differences Analysis:

  • Behavior: Swift enforces a distinction between String and Substring types to prevent accidental long-term retention of references to large strings. C#’s Substring directly returns a string (copy) unless you explicitly use Span.

Comparing Strings
#

1. Core Concepts
#

  • Concept Explanation: Swift string comparison uses “Canonical Equivalence”. This means that if two strings look the same, they are equal, even if the underlying Unicode composition is different.
  • Key Syntax: ==, !=, hasPrefix(), hasSuffix()

2. Example Analysis
#

// "é" (Single scalar) vs "e" + "́" (Combined scalars)
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"

if eAcuteQuestion == combinedEAcuteQuestion {
    print("These two strings are considered equal")
}

Note:

Swift’s string and character comparison is not Locale-sensitive (it is independent of the language environment).

3. C#
#

Concept Correspondence: C#’s == is based on UTF-16 code unit content comparison (ordinal) and does not perform Unicode normalization.

C# Example:

// C#
string s1 = "\u00E9";
string s2 = "e\u0301";
Console.WriteLine(s1 == s2); // False! Because underlying char sequences differ

// Normalize to achieve Swift's effect
Console.WriteLine(s1.Normalize() == s2.Normalize()); // True

Key Differences Analysis:

  • Behavior: This is another potential source of bugs. Swift’s == is smarter (but slower) and automatically handles Unicode normalization. C#’s == is faster but strictly compares the char sequence. If you need similar comparison in C#, you must manually call String.Normalize().