GDKB

Strings

Beginner20 Minutes readUpdated July 21, 20263 related concepts

A string is a sequence of characters treated as a single piece of text — a display name, a save file path, dialogue for an NPC. In most languages a game studio uses, strings are immutable: every operation that looks like it "changes" a string actually produces a brand-new one, which is the single fact that explains most string-related performance and correctness surprises.

On this page

Strings

Summary

A string is a sequence of characters treated as a single piece of text — a display name, a save file path, dialogue for an NPC. In most languages a game studio uses, strings are immutable: every operation that looks like it "changes" a string actually produces a brand-new one, which is the single fact that explains most string-related performance and correctness surprises.

Mental Model

A string is like a printed sentence on a card — a fixed row of characters, the same contiguous-memory idea as an array ([[CON-0012]]), because a string is effectively an array of characters under the hood. "Editing" a printed card isn't possible; if you want a different sentence, you print a brand-new card and throw the old one away. name = name + "!"; is exactly that: it doesn't append ! onto the existing card, it prints an entirely new card with the combined text and points name at the new one, leaving the old card to be discarded (garbage collected, in managed languages).

That mental model — every "change" is actually a brand-new card — is why building a long piece of text one small append at a time in a loop is much more expensive than it looks: each += prints a whole new card, longer than the last, and throws the previous one away.

Core Explanation

  • Immutability. A string value, once created, never changes. str.ToUpper(), str.Substring(2), str + other all return new strings; none of them modify str itself. This makes strings inherently thread-safe to read and eliminates a whole class of aliasing bugs a mutable buffer would have — but it means repeated concatenation allocates repeatedly.
  • Character array underneath. Indexing a string (str[0]) reads a single character the same way indexing an array reads an element, because that's essentially what a string is — a length-prefixed (or null-terminated, in raw C) block of characters.
  • Comparison is by value, not identity. "abc" == "abc" is true in C# even though they may be two distinct objects in memory — string equality compares character-by-character content by default, unlike the reference-identity default most other reference types use.
  • Encoding matters. C#'s string is UTF-16 internally; Unreal's FString is similar. Text containing non-ASCII characters (localization, player-entered names) behaves correctly because of this, but naive byte-length assumptions ("café".Length counting bytes instead of characters) will not.

Syntax / Examples

C# (Unity)

csharp
public class PlayerNameTag : MonoBehaviour
{
    public string PlayerName = "Hero";

    void Start()
    {
        string display = PlayerName.ToUpper();       // new string
        string greeting = $"Welcome, {display}!";     // interpolation

        Debug.Log(greeting);
        Debug.Log(PlayerName); // unchanged: "Hero"
    }
}

C++ (Unreal)

cpp
FString PlayerName = TEXT("Hero");

void APlayerNameTag::BeginPlay()
{
    Super::BeginPlay();

    FString Display = PlayerName.ToUpper();
    FString Greeting = FString::Printf(TEXT("Welcome, %s!"), *Display);

    UE_LOG(LogTemp, Log, TEXT("%s"), *Greeting);
    // PlayerName is unchanged: "Hero"
}

Unreal Blueprint

Format Text and Append nodes build new Text/String values from existing ones, mirroring the C++/C# examples' immutability — there is no "mutate in place" node, only nodes that consume inputs and produce a new output string.

Practical Example

Building a debug HUD line that summarizes several stats should avoid repeated concatenation in a loop that runs every frame.

csharp
using System.Text;

public class DebugHud : MonoBehaviour
{
    public void BuildSummary(string[] statNames, float[] statValues)
    {
        var builder = new StringBuilder();
        for (int i = 0; i < statNames.Length; i++)
        {
            builder.Append(statNames[i]);
            builder.Append(": ");
            builder.Append(statValues[i]);
            builder.Append('\n');
        }

        string summary = builder.ToString();
        Debug.Log(summary);
    }
}

StringBuilder maintains one mutable internal buffer and only produces a single final string at the end — exactly one "new card" instead of one per append, which is the fix for the immutability cost described above when a string is being assembled in a loop.

Common Mistakes

  • Concatenating strings in a loop with +=. Each iteration allocates a brand-new string containing the whole accumulated text so far — an O(n²) total cost for n appends. Use StringBuilder (C#) or TStringBuilder (Unreal) for anything beyond a handful of concatenations, especially inside Update().
  • Comparing strings with == when culture/case sensitivity matters. "Hero" == "hero" is false — case-sensitive, ordinal comparison by default in C#. Player-entered text (usernames, chat commands) usually needs an explicit StringComparison.OrdinalIgnoreCase comparison.
  • Assuming .Length counts what a player perceives as "characters." Combining characters, emoji, and some non-Latin scripts can be represented by more than one UTF-16 code unit, so .Length can diverge from the visual character count — relevant for input validation on player-entered names.
  • Allocating new strings every frame for UI text that rarely changes. Rebuilding a formatted string in Update() every frame when the underlying value only changes occasionally wastes allocations; only rebuild the string when the source value actually changes.

Best Practices

  • Use StringBuilder/TStringBuilder for any text built incrementally in a loop, and reserve plain +/+= for one-off, fixed-count concatenations.
  • Cache formatted strings that don't change every frame instead of reformatting them in Update().
  • Be explicit about StringComparison (C#) when comparing player-entered or externally-sourced text, rather than relying on the default ordinal comparison always being what's intended.
  • Prefer string interpolation ($"{a} {b}") over manual + concatenation for readability on one-off formatting — it compiles to efficient code and is far easier to review for typos.

Exercises

  1. Build the same 100-line summary string once with += in a loop and once with StringBuilder, and explain — in terms of the mental model above — why one allocates far more than the other even though both produce the same final text.
  2. Write a case-insensitive username comparison function and explain which StringComparison value you chose and why.
  3. Given string s = "café";, predict whether s.Length equals 4, and verify. Research (or note as a follow-up) what encoding detail explains the result.
  4. Refactor a hypothetical Update() method that rebuilds a "Score: X" label every frame into one that only rebuilds the string when the score actually changes.
  • Arrays — a string is conceptually a character array; many string behaviors (indexing, length, immutability trade-offs) mirror array behaviors directly.
  • Collections — building a string from many pieces is a specialized case of accumulating many elements, which is why StringBuilder exists as a purpose-built collection-like tool.
  • Exceptions — malformed or unexpected string input (parsing a number from player text, reading a corrupted save file) is a common source of exceptions that must be handled explicitly.

References

Related Concepts