Skip to main content

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

Learning Swift from a C# Perspective

Swift : 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 is designed to be lightweight, similar to C, but the underlying implementation is very 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
  • Official Note:

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

2. Example Analysis
#

Source Documentation Code:

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."
"""

// Initializing an empty string
var emptyString = ""               // Empty string literal
var anotherEmptyString = String()  // Initializer 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 """, keeping the code layout clean. The isEmpty property is an efficient way to check if a string is empty.

3. C# Developer Perspective
#

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

C# Comparison Code:

// 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 aligning with the closing """. Perfect support for this in C# was only recently achieved with Raw String Literals in C# 11.
  • Behavior: Swift recommends using the isEmpty property, whereas C# developers are accustomed to 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
#

Source Documentation Code:

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 copying only happens when content is actually modified), semantically, you possess an independent copy of that string.

3. C# Developer Perspective
#

Concept Mapping: 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# Comparison Code:

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

// C# does not have a direct equivalent to 'let' for constants preventing reassignment-induced modification,
// 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 syntactically looks like standard string operations.
  • Behavior: This is the biggest trap. In C#, a string variable holds 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 upon assignment (logically).

Concatenation & Interpolation
#

1. Core Concepts
#

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

2. Example Analysis
#

Source Documentation Code:

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 than traditional format strings.

3. C# Developer Perspective
#

Concept Mapping: Equivalent to String Concatenation and String Interpolation in C#.

C# Comparison Code:

// 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 \(), while C# uses {} (combined with $).
  • Behavior: Both compile down to efficient string construction calls. 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 be composed of one or more Unicode scalars, but in Swift, they are treated as a single Character.
  • Key Syntax: Character, count property, \u{n}

2. Example Analysis
#

Source Documentation Code:

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)")
// Output "the number of characters in café is 4" (Visually it's still one word, length remains 4)

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

3. C# Developer Perspective
#

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

C# Comparison Code:

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

Key Differences Analysis:

  • Syntax: No major difference, but Swift Unicode escapes use \u{...} allowing variable length hex, whereas C# uses \uXXXX or \UXXXXXXXX.
  • Behavior: Extremely Important!
    • Emoji Handling: An Emoji (e.g., 🐶) usually has a string.Length of 2 (Surrogate Pair) in C#; in Swift, .count is 1.
    • Performance: C#’s Length is O(1); Swift’s count requires iteration, making it O(n). Be mindful of performance when calling count frequently within loops.

Accessing and Modifying Strings
#

1. Core Concepts
#

  • Concept Explanation: Due to the Unicode complexity mentioned above (characters having variable lengths), Swift does not allow integer indexing (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
#

Source Documentation Code:

let greeting = "Guten Tag!"
// Access 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 prevents random access from slicing a multi-byte character (like cutting an Emoji in half).

3. C# Developer Perspective
#

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

C# Comparison Code:

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

// Insert and Remove (C# string is immutable, creates new string)
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 forces 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 prefer for char in string or high-order functions. If you need frequent random access, consider converting 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
#

Source Documentation Code:

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

// If storing long-term, convert back to String
let newString = String(beginning)

Logic Explanation: While Substring is efficient, it is not suitable for long-term storage. As long as a Substring exists, the memory for the entire original String cannot be released.

3. C# Developer Perspective
#

Concept Mapping: Traditionally, C#’s Substring() allocates new memory and copies the string. However, in newer C# versions (Core/Standard), the concept of Span<char> or ReadOnlySpan<char> is very similar to Swift’s Substring—they are “views” of the original memory.

C# Comparison Code:

// 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 type distinction between String and Substring, preventing accidental long-term retention of references to large strings. C#’s Substring directly returns a string (causing a copy), unless you explicitly use Span.

Comparing Strings
#

1. Core Concepts
#

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

2. Example Analysis
#

Source Documentation Code:

// "é" (Single scalar) vs "e" + "́" (Combined scalar)
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")
}

Official Note:

Swift’s string and character comparisons are not locale-sensitive.

3. C# Developer Perspective
#

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

C# Comparison Code:

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

// To achieve Swift's effect, normalization is required
Console.WriteLine(s1.Normalize() == s2.Normalize()); // True

Key Differences Analysis:

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