Unity Mastery Handbook
Topic chapterT04P1 · Foundations: Editor, C#, Maths, Physics, 2D/3D

C# Beyond Basics: OOP, Generics, Collections, LINQ, Delegates & Memory

Go past the basics: build class families with inheritance and interfaces, write reusable generic code, pick the right collection, query data with LINQ, connect code with delegates and events, and see what your code does to memory. Every example is compiled and run, and the chapter ends with a harbour simulation you build yourself.

Study time
12–16 hours
Reading time
191 min read · 12,643 words
Prerequisites
T03 · C# Fundamentals: Types, Logic, Methods & Classes
Courses merged
6 course files
Your progress
—
After this chapter you can…
  • Build a class family with inheritance, using virtual, override, abstract and sealed, and explain polymorphism
  • Define interfaces, implement several on one class, and choose between an interface, an abstract class and composition
  • Write generic methods and classes with constraints, and read the generic APIs Unity is full of
  • Choose between List, Dictionary, HashSet, Queue and Stack by how you'll use the data and what each operation costs
  • Pass behaviour around with delegates, lambdas, Func and Action, and avoid the closure traps
  • Publish and subscribe to events, and unsubscribe so nothing leaks
  • Query collections with LINQ, explain deferred execution, and write your own iterators with yield return
  • Explain the stack, the heap, boxing and the garbage collector, and keep allocations out of code that runs every frame

In T03 · C# Fundamentals: Types, Logic, Methods & Classes you learned to write code that works. This chapter is about writing code that stays easy to change: classes that share behaviour, code that works for any type, data stored in the right shape, and parts of a program that talk to each other without being tied together. These are the tools every Unity system is built from, and the ones Unity's own API uses on every page.

As before, most examples are small console programs, compiled and run, with their exact output underneath. They use Unity's version of C# (C# 9), so everything here works in Unity 6. Near the end, four short Unity scripts show the same ideas in a scene, and the guided build is a harbour simulation that uses every tool in the chapter at once.

The big picture#

Once a program grows past a few hundred lines, the hard part stops being "how do I make the computer do this?" and becomes "how do I organise this so I can still change it next month?" Each section of this chapter answers one version of that question:

ToolThe problem it solvesWhere you'll meet it in Unity
InheritanceSeveral classes share most of their code but differ in a few detailsEvery script you write inherits from MonoBehaviour
InterfacesUnrelated classes need to be used the same wayGetComponent<IDamageable>(), Unity's event system interfaces
GenericsThe same code should work for many types without copying itGetComponent<T>(), List<T>, ObjectPool<T>
CollectionsData has to be found, added and removed quicklyInventories, lookup tables, spawn queues, undo stacks
Delegates and eventsOne part of the game must react to another without depending on itHealth changed, enemy died, button clicked, UnityEvent
LINQ and iteratorsQuestions about a collection take many lines of loopsTools, setup code, turn-based logic; iterators power coroutines
MemoryCode works but stutters, because it creates garbageAnything in Update, which runs every frame

A thread runs through all of them: depend on as little as possible. A class that knows only an interface, rather than ten concrete classes, keeps working when you add an eleventh. A lighthouse that raises an event, rather than calling the harbour directly, keeps working when you add a logbook. You'll see that idea again in every section.

Inheritance: building on a class#

Imagine three shoreline creatures for a small game: a crab, a jellyfish and a gull. Each has a name and health, takes damage and does something on its turn. Written as three separate classes, they'd repeat the same fields and the same damage code three times, and a fix to one would have to be copied to the other two. Inheritance lets you write the shared part once, in a base class, and have each creature derive from it, adding or changing only what's different.

Shore/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Shore
{
    // The base class holds what every creature has and does.
    abstract class Creature
    {
        public string Name { get; }
        public int Health { get; protected set; }

        protected Creature(string name, int health)
        {
            Name = name;
            Health = health;
        }

        // virtual: a default that a derived class may replace.
        public virtual void TakeHit(int damage)
        {
            Health = Math.Max(0, Health - damage);
            Console.WriteLine($"  {Name} takes {damage}, health {Health}");
        }

        // abstract: no default at all; every concrete creature must supply one.
        public abstract string Act();

        public override string ToString() => $"{GetType().Name} '{Name}' ({Health} hp)";
    }

    class Crab : Creature
    {
        readonly int shell;

        public Crab(string name, int shell) : base(name, 12)
        {
            this.shell = shell;
        }

        public override void TakeHit(int damage)
        {
            int through = Math.Max(1, damage - shell);   // the shell absorbs some
            Console.WriteLine($"  The shell blocks {damage - through}.");
            base.TakeHit(through);                        // then the normal rules
        }

        public override string Act() => $"{Name} snaps its claws.";
    }

    class Jellyfish : Creature
    {
        public Jellyfish(string name) : base(name, 6) { }

        public override string Act() => $"{Name} drifts and stings.";
    }

    sealed class Gull : Creature
    {
        public Gull(string name) : base(name, 5) { }

        public override void TakeHit(int damage)
        {
            base.TakeHit(damage);
            if (Health > 0 && Health < 3)
            {
                Console.WriteLine($"  {Name} flaps away to safety!");
            }
        }

        public override string Act() => $"{Name} steals a chip.";
    }

    class Program
    {
        static void Main()
        {
            // A list of the base type can hold every kind of creature.
            var shore = new List<Creature>
            {
                new Crab("Pincher", shell: 3),
                new Jellyfish("Glim"),
                new Gull("Squawk"),
            };

            foreach (Creature creature in shore)
            {
                Console.WriteLine(creature);
                Console.WriteLine("  " + creature.Act());
                creature.TakeHit(4);   // same call, each type reacts its own way
            }

            // Asking what something really is, with a type pattern
            foreach (Creature creature in shore)
            {
                if (creature is Crab crab)
                {
                    Console.WriteLine($"{crab.Name} is a crab with {crab.Health} hp left");
                }
            }
        }
    }
}
OutputText
Crab 'Pincher' (12 hp)
  Pincher snaps its claws.
  The shell blocks 3.
  Pincher takes 1, health 11
Jellyfish 'Glim' (6 hp)
  Glim drifts and stings.
  Glim takes 4, health 2
Gull 'Squawk' (5 hp)
  Squawk steals a chip.
  Squawk takes 4, health 1
  Squawk flaps away to safety!
Pincher is a crab with 11 hp left

Base classes and derived classes#

class Crab : Creature reads "Crab derives from Creature" (or "inherits from", or "is a subclass of"). A Crab automatically has everything public and protected in Creature: Name, Health, TakeHit, ToString. It adds a private shell field of its own. A C# class can derive from one base class only; you'll see in the next section how interfaces get around that.

The test for whether inheritance fits is the phrase "is a": a crab is a creature, so Crab : Creature makes sense. A crab has a shell, so the shell is a field, not a base class. If "is a" sounds wrong out loud, inheritance is the wrong tool.

Constructors and base(…)#

Constructors aren't inherited. Each derived class writes its own, and must say which base constructor runs first with : base(…). In Crab, : base(name, 12) after the constructor's parameters means "to make a crab, first build the Creature part with this name and 12 health, then run my own body". The base part is always built before the derived part, so by the time Crab's constructor body runs, Name and Health are already set.

Creature's constructor is protected, so only derived classes can call it. That's a common pattern for base classes: nobody should build "just a creature".

protected: for the family only#

In T03 · C# Fundamentals: Types, Logic, Methods & Classes you met public and private. protected sits between them: visible inside the class and inside any class that derives from it, invisible to everyone else. Health { get; protected set; } lets anyone read the health, but only Creature and its subclasses can change it. Use protected sparingly. Every protected member is something all future subclasses may depend on, which makes it harder to change later; prefer private fields and a protected or public method that changes them safely.

virtual, override and base#

A method marked virtual in the base class may be replaced in a derived class by a method marked override with the same signature. Creature's TakeHit is virtual: the jellyfish doesn't override it, so it uses the default; the crab and the gull do.

Inside an override, base.TakeHit(…) calls the base class's version. That's how the crab reuses the normal damage rules after its shell takes its share, and how the gull adds its escape after the normal rules ran. Calling base is optional: leave it out and the override replaces the behaviour entirely. Whether to call it first, last or not at all is a design choice that changes behaviour, so make it on purpose.

abstract and sealed#

An abstract class can't be created with new; it exists only to be derived from (trying gives error CS0144). An abstract method has no body, and every non-abstract derived class must override it (or get error CS0534). Creature's Act is abstract because there's no sensible default: every creature must say what it does.

sealed is the opposite: a sealed class can't be derived from. Gull is sealed, which says "this is a finished design". It also lets the runtime make calls slightly faster, but the main reason is clarity.

KeywordOn a classOn a method or property
virtual(not used)Has a default; derived classes may override it
abstractCan't be created; may have abstract membersNo body; derived classes must override it
override(not used)Replaces a virtual or abstract member
sealedCan't be derived fromsealed override: no further overriding
protected(only on nested classes)Visible to the class and its subclasses

Polymorphism: one call, many behaviours#

The loop in Main holds each creature in a variable of type Creature and calls creature.TakeHit(4). The code in the loop doesn't know or care which kind of creature it has. At run time, C# looks at the actual object and calls the right override: the crab's shell version, the jellyfish's default, the gull's escape version. That's polymorphism, and it's why the list can hold all three types at once.

The payoff is that you can add a fourth creature, say a seal, by writing one new class. The loop, and any other code that works with Creature, doesn't change at all. Exercise 1 asks you to do exactly that.

Checking and converting types: is, as and casts#

Sometimes you do need to know the real type. You saw is patterns in T03 · C# Fundamentals: Types, Logic, Methods & Classes; here's where they earn their keep:

  • if (creature is Crab crab) tests the type and, if it matches, gives you a variable of that type. This is the form to use almost always.
  • creature as Crab converts if it can and gives null if it can't. Older code uses it followed by a null check; the is pattern does both in one step.
  • (Crab)creature is a cast: it converts, and throws an InvalidCastException if the object isn't really a crab. Use it only when being wrong would be a bug you want to hear about.

Converting from derived to base (Creature c = new Crab(…)) is always safe and needs no cast. Converting from base to derived needs one of the three forms above; plain assignment gives error CS0266. If you find yourself writing many is checks in a row (if it's a crab do this, if it's a gull do that), that's usually a sign the behaviour belongs in a virtual method instead.

Every class derives from object#

A class that names no base class still has one: System.Object, usually written object. That's where ToString, Equals and GetHashCode come from, and why Creature writes public override string ToString(). Overriding ToString is the easiest win in this chapter: it makes every log message and debugger view more useful.

Hiding a method by accident#

If a derived class declares a method with the same name and signature as a base method but forgets override, it doesn't override; it hides. The compiler warns (CS0114 if the base method is virtual, CS0108 if not). With hiding, which method runs depends on the variable's type, not the object's: through a Creature variable you'd get the base version, which is almost never what you wanted. Treat these warnings as errors. If hiding really is your intent, the keyword new on the method says so and silences the warning, but you'll rarely need it.

System.Object UnityEngine.Object name, Instantiate, Destroy GameObject Component ScriptableObject transform, GetComponent Transform Behaviour enabled MonoBehaviour Camera your scripts
Part of Unity's class hierarchy. Each class inherits everything above it. Your scripts sit below MonoBehaviour, which is why a new script already has transform, enabled, name and GetComponent.

When not to inherit#

Inheritance is the tightest link two classes can have: the derived class depends on every detail of the base, and a change to the base reaches every subclass. Deep chains (Creature → SeaCreature → ShellCreature → ArmouredCrab → GiantArmouredCrab) become hard to change, and real game objects rarely fit one tree: is a flying fish a bird or a fish? Keep hierarchies shallow, one or two levels below the base, and prefer the alternatives in the next section when the "is a" test feels forced.

Interfaces and composition#

Inheritance shares code between classes that belong to one family. Often, though, you want unrelated things to be used the same way. A barrel, a lamp and an enemy can all be damaged, but they share nothing else and shouldn't share a base class. An interface is the tool for this: a named list of members that a class promises to provide, with no fields and (usually) no code.

Contracts/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Contracts
{
    // An interface is a promise: "anything that implements me has these members".
    interface IDamageable
    {
        bool IsBroken { get; }
        void TakeDamage(int amount);
    }

    interface IRepairable
    {
        void Repair();
    }

    class Barrel : IDamageable
    {
        int strength = 3;
        public bool IsBroken => strength <= 0;

        public void TakeDamage(int amount)
        {
            strength -= amount;
            Console.WriteLine(IsBroken ? "  The barrel bursts." : "  The barrel cracks.");
        }
    }

    // A class can implement any number of interfaces.
    class Lamp : IDamageable, IRepairable
    {
        public bool IsBroken { get; private set; }

        public void TakeDamage(int amount)
        {
            IsBroken = true;
            Console.WriteLine("  The lamp goes dark.");
        }

        public void Repair()
        {
            IsBroken = false;
            Console.WriteLine("  The lamp is lit again.");
        }
    }

    class Rock { }   // implements nothing: you can't damage a rock

    // IComparable<T> is a .NET interface: it lets List.Sort put catches in order.
    class Catch : IComparable<Catch>
    {
        public string Species { get; }
        public int Grams { get; }

        public Catch(string species, int grams)
        {
            Species = species;
            Grams = grams;
        }

        public int CompareTo(Catch other) => Grams.CompareTo(other.Grams);

        public override string ToString() => $"{Species} {Grams} g";
    }

    class Program
    {
        static void Main()
        {
            var things = new List<object> { new Barrel(), new Lamp(), new Rock() };

            foreach (object thing in things)
            {
                Console.WriteLine($"Hitting the {thing.GetType().Name}:");
                if (thing is IDamageable target)
                {
                    target.TakeDamage(2);
                    if (target.IsBroken && target is IRepairable fixable)
                    {
                        fixable.Repair();
                    }
                }
                else
                {
                    Console.WriteLine("  Nothing happens.");
                }
            }

            var basket = new List<Catch>
            {
                new Catch("mackerel", 340),
                new Catch("sprat", 25),
                new Catch("bass", 1200),
            };
            basket.Sort();   // uses CompareTo
            Console.WriteLine(string.Join(", ", basket));
        }
    }
}
OutputText
Hitting the Barrel:
  The barrel cracks.
Hitting the Lamp:
  The lamp goes dark.
  The lamp is lit again.
Hitting the Rock:
  Nothing happens.
sprat 25 g, mackerel 340 g, bass 1200 g

Declaring and implementing an interface#

An interface declares members without bodies: void TakeDamage(int amount);. A class implements it by listing it after a colon, like a base class, and providing a public member for each item: class Lamp : IDamageable, IRepairable. If one is missing, the class doesn't compile (error CS0535), so the promise is checked for you. Interface names start with a capital I by convention, and usually describe an ability: IDamageable, IInteractable, ISaveable.

The interesting part is the code that uses them. The loop knows nothing about barrels or lamps. It asks one question, "can this be damaged?" (thing is IDamageable target), and if so, uses only what the interface promised. Tomorrow you can add a damageable crate or ship, and the loop won't change. Code that depends on an interface instead of on concrete classes is said to be decoupled from them.

Interfaces .NET already defines#

The .NET library is full of interfaces, and implementing one lets your classes plug into its features:

InterfacePromisesWhat you get
IComparable<T>CompareTo(T other)List.Sort(), Max, OrderBy without a custom comparer
IEquatable<T>Equals(T other)Fast, correct equality for dictionary keys and Contains
IEnumerable<T>GetEnumerator()foreach and all of LINQ (see below)
IDisposableDispose()The using statement, which cleans up for you
IReadOnlyList<T>Count and an indexerHand out a list that callers can read but not change

CompareTo returns a negative number if this object comes first, zero if they're equal, and a positive number if the other comes first. Catch passes the job to int.CompareTo, which already follows that rule, so basket.Sort() orders the catches by weight.

Interface or abstract class?#

Abstract classInterface
How many per classOneAny number
Can hold fields and shared codeYesNo fields; in Unity code, treat it as members only
Relationship"is a" (a crab is a creature)"can do" (a lamp can be repaired)
Use whenRelated classes share real code and stateUnrelated classes need the same ability

They combine well. The harbour build at the end of the chapter has an abstract Vessel base class for shared data, plus an IInspectable interface that only some vessels implement.

Composition: "has a" instead of "is a"#

The third option is often the best: build an object out of smaller objects instead of inheriting behaviour. A boat class could inherit from SailingThing, then you'd want a motor boat, then a boat with a sail and a motor, and the tree tangles. With composition the boat has an engine field and has a sail field, either of which can be missing or swapped. Each part is small, tested on its own, and reused anywhere.

Unity is built on this idea. A GameObject isn't a subclass of "Player" or "Door": it's an empty container that has components, and its behaviour is whatever its components add up to. A door is a Transform, a MeshRenderer, a BoxCollider, an AudioSource and your small Hinge script. A locked door is the same plus a Lock component, not a LockedDoor subclass. When you design your own scripts, follow Unity's lead: many small components that each do one job, connected by references, interfaces and events.

Inheritance Door LockedDoor CreakyLockedDoor …and CreakyDoor, AutoDoor, AutoLockedDoor… Composition Door (GameObject) Transform Hinge Lock AudioSource add or remove parts freely
Inheritance needs a new class for every combination of features. Composition, which is how Unity works, builds each object from small parts.

Generics: code that works for any type#

You've been using generics since T03 · C# Fundamentals: Types, Logic, Methods & Classes: List<string> is the generic class List<T> with T filled in as string. The T is a type parameter, a placeholder for a type that whoever uses the class chooses. One List<T> class gives you lists of ints, strings, creatures and anything else, each of them type-safe: a List<int> refuses a string at compile time.

Before generics existed, collections stored everything as object. You had to cast each item back to its real type (risking an InvalidCastException), and each int or float you stored was boxed into a heap object (see Memory). Generics remove both problems. You can write your own generic methods and classes the same way.

Generics/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Generics
{
    // A generic class: one pool type that works for any class with a parameterless constructor.
    class Pool<T> where T : class, new()
    {
        readonly Stack<T> spare = new Stack<T>();

        public int Created { get; private set; }
        public int Spare => spare.Count;

        public T Get()
        {
            if (spare.Count > 0)
            {
                return spare.Pop();
            }
            Created++;
            return new T();   // allowed because of the new() constraint
        }

        public void Release(T item) => spare.Push(item);
    }

    class Spark { }
    class Splash { }

    class Program
    {
        // A generic method with a constraint: T must know how to compare itself.
        static T Largest<T>(IReadOnlyList<T> items) where T : IComparable<T>
        {
            T best = items[0];
            for (int i = 1; i < items.Count; i++)
            {
                if (items[i].CompareTo(best) > 0)
                {
                    best = items[i];
                }
            }
            return best;
        }

        static void Swap<T>(ref T a, ref T b)
        {
            T temp = a;
            a = b;
            b = temp;
        }

        static T FirstOrFallback<T>(List<T> items) => items.Count > 0 ? items[0] : default;

        static void Main()
        {
            Console.WriteLine(Largest(new[] { 4, 19, 7 }));               // T is int
            Console.WriteLine(Largest(new[] { "kelp", "anchor", "tide" })); // T is string

            string left = "port", right = "starboard";
            Swap(ref left, ref right);
            Console.WriteLine($"{left} / {right}");

            Console.WriteLine(FirstOrFallback(new List<int>()));          // default(int) is 0
            Console.WriteLine(FirstOrFallback(new List<string>()) == null); // default(string) is null

            var sparks = new Pool<Spark>();
            var splashes = new Pool<Splash>();
            Spark a = sparks.Get();
            Spark b = sparks.Get();
            sparks.Release(a);
            Spark c = sparks.Get();                    // reuses a instead of creating
            Console.WriteLine($"Sparks created: {sparks.Created}, reused: {ReferenceEquals(a, c)}");
            splashes.Release(new Splash());
            Console.WriteLine($"Splashes waiting: {splashes.Spare}");
        }
    }
}
OutputText
19
tide
starboard / port
0
True
Sparks created: 2, reused: True
Splashes waiting: 1

Generic methods#

static void Swap<T>(ref T a, ref T b) declares a type parameter after the method name. Inside, T is used like any type. When you call Swap(ref left, ref right) with two strings, the compiler infers that T is string; you could also write Swap<string>(…) explicitly, which you'll need when nothing in the arguments tells the compiler what T is. Unity's GetComponent<Rigidbody>() is a generic method of exactly this kind: it has no arguments, so you name the type in angle brackets.

Generic classes#

class Pool<T> declares the type parameter after the class name, and every member can use it. Pool<Spark> and Pool<Splash> are two separate types made from one piece of code. A pool keeps objects you've finished with and hands them out again instead of creating new ones, which matters in games because creating objects has a cost (see Memory). Unity ships a full version of this idea as UnityEngine.Pool.ObjectPool<T>.

Constraints: what T is allowed to be#

Inside Largest<T>, the code calls items[i].CompareTo(best). For an unknown type that wouldn't compile: not every type has CompareTo. The constraint where T : IComparable<T> promises that it does, and the compiler then allows the call (and rejects Largest on a type that can't compare itself).

ConstraintMeansLets you
where T : classT is a reference typeCompare with null; store null
where T : structT is a value type (not nullable)Rely on value copying, no nulls
where T : new()T has a public parameterless constructorWrite new T()
where T : CreatureT is Creature or derives from itUse Creature's members on a T
where T : IComparable<T>T implements that interfaceCall the interface's members
where T : unmanagedT is a value type with no references insideLow-level code; used by Unity's job system

Combine them with commas, as Pool does: where T : class, new(). Unity's own APIs use constraints too: GetComponent<T> works for any type (so it accepts interfaces), and AddComponent<T> requires T : Component, because only components can be added. Breaking a constraint is a compile error: Pool<int> fails with CS0452 (int isn't a reference type), and a class without a parameterless constructor fails with CS0310.

default(T)#

Generic code sometimes needs "the empty value" of a type it doesn't know. default gives it: 0 for numbers, false for bool, null for reference types, and a zeroed struct for structs. FirstOrFallback returns it when the list is empty. LINQ's FirstOrDefault, which you'll meet below, works the same way.

Collections: choosing the right container#

In T03 · C# Fundamentals: Types, Logic, Methods & Classes you stored groups of values in arrays and List<T>. Lists are the right default, but some jobs fit other shapes far better. "Does the player have the brass key?" is slow to answer with a list of 500 items and instant with the right collection. All of these live in System.Collections.Generic.

Collections/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Collections
{
    class Program
    {
        static void Main()
        {
            // Dictionary: look a value up by its key.
            var stock = new Dictionary<string, int>
            {
                ["rope"] = 3,
                ["lantern oil"] = 1,
            };
            stock["rope"] += 2;      // the key exists: update it
            stock["hook"] = 10;      // the key is new: add it
            Console.WriteLine($"Rope: {stock["rope"]}, hooks: {stock["hook"]}");

            if (stock.TryGetValue("tar", out int tar))
            {
                Console.WriteLine($"Tar: {tar}");
            }
            else
            {
                Console.WriteLine("No tar in stock");
            }
            stock.Remove("lantern oil");
            Console.WriteLine($"Kinds of item: {stock.Count}, has oil: {stock.ContainsKey("lantern oil")}");

            // HashSet: each value at most once, and fast "is it in there?"
            var visited = new HashSet<string> { "Gull Rock", "Saltmarsh" };
            bool isNew = visited.Add("Gull Rock");
            Console.WriteLine($"Gull Rock new? {isNew}. Islands visited: {visited.Count}");

            // Queue: first in, first out
            var dock = new Queue<string>();
            dock.Enqueue("Heron");
            dock.Enqueue("Bluefin");
            dock.Enqueue("Marigold");
            Console.WriteLine($"Docking first: {dock.Dequeue()}, next up: {dock.Peek()}");

            // Stack: last in, first out
            var undo = new Stack<string>();
            undo.Push("place buoy");
            undo.Push("move pier");
            undo.Push("paint hull");
            Console.WriteLine($"Undo: {undo.Pop()}, then {undo.Pop()}");

            // List extras: removing by condition, and finding
            var crew = new List<string> { "Ada", "Bram", "Cora", "Dov", "Edda" };
            int removed = crew.RemoveAll(name => name.Length == 3);
            Console.WriteLine($"Removed {removed}: {string.Join(", ", crew)}");
            Console.WriteLine($"First with an 'r': {crew.Find(name => name.Contains("r"))}");
        }
    }
}
OutputText
Rope: 5, hooks: 10
No tar in stock
Kinds of item: 2, has oil: False
Gull Rock new? False. Islands visited: 2
Docking first: Heron, next up: Bluefin
Undo: paint hull, then move pier
Removed 2: Bram, Cora, Edda
First with an 'r': Bram

Dictionary: find by key#

A Dictionary<TKey, TValue> stores key–value pairs, and finding a value by its key is fast however big the dictionary gets. It works by turning each key into a number (its hash code) that says roughly where to look, so it checks one or two entries instead of all of them. Use it whenever you'd otherwise search a list for "the item whose name is…": stock by item name, prefabs by ID, players by connection number.

  • Indexer write (stock["hook"] = 10) adds the key if it's new and replaces the value if not. Add instead throws if the key already exists, which is useful when a duplicate would be a bug.
  • Indexer read (stock["tar"]) throws a KeyNotFoundException if the key is missing. When a key might be missing, use TryGetValue: it returns false instead of throwing, and does the lookup only once (checking ContainsKey and then reading does it twice).
  • Keys are unique, compared with Equals and GetHashCode. Strings and numbers work out of the box. For your own class used as a key, either use the object itself (each instance is a different key) or implement IEquatable<T> and override GetHashCode so equal values give equal hash codes. Never change an object in a way that changes its hash code while it's a key: the dictionary will look in the wrong place and not find it.
  • Don't rely on the order. Looping over a dictionary gives you every pair, but the order isn't guaranteed. Sort the keys (or use SortedDictionary) if you need an order.

HashSet, Queue and Stack#

A HashSet<T> is a dictionary with keys only: it holds each value at most once and answers "is this in the set?" instantly. Add returns false if the value was already there, which makes "first time visiting this island?" a one-line check. Sets can also be combined: UnionWith, IntersectWith and ExceptWith do set maths in place.

A Queue<T> is first in, first out, like people queuing at a counter: Enqueue adds at the back, Dequeue takes from the front, Peek looks at the front without removing it. Use it for anything processed in arrival order: messages, spawn waves, ships waiting to dock.

A Stack<T> is last in, first out, like a pile of plates: Push puts on top, Pop takes from the top. Use it for undo history, backtracking through a maze, or nested menus where "back" returns to the previous screen. Both throw an InvalidOperationException if you Dequeue or Pop when empty; check Count first or use TryDequeue and TryPop.

More from List<T>#

Lists have methods that take a test as an argument, written as a small inline function called a lambda (name => name.Length == 3, explained properly in the next section). RemoveAll removes every item that passes the test and returns how many it removed, which is the safe way to remove while "looping" that T03 · C# Fundamentals: Types, Logic, Methods & Classes promised. Find returns the first match (or the default, null here, if none), FindAll returns all matches, and Exists says whether any match. Sort takes an optional comparison: (a, b) => a.Length.CompareTo(b.Length) passed to crew.Sort sorts by name length.

Under the hood a List is an array with spare room at the end. Adding at the end is fast; when the spare room runs out, the list copies everything into an array twice the size. Inserting or removing near the front shifts every later item along, so it gets slower as the list grows. If you know roughly how many items you'll store, pass it to the constructor (new List<Enemy>(64)) to skip the copying.

Choosing a collection#

CollectionGood atSlow or impossibleTypical use
T[] arrayFixed size, fastest indexingChanging sizeGrid cells, fixed slots, buffers
List<T>Index, add or remove at the endSearch by value; insert at the frontThe default for "some things"
Dictionary<K,V>Find, add or remove by keyOrder; finding by valueLookup tables, inventories
HashSet<T>"Is it in there?", no duplicatesOrder; indexingVisited, unlocked, tagged sets
Queue<T>Add at back, take from frontAnything in the middleWaiting lines, message queues
Stack<T>Add and take at the topAnything below the topUndo, backtracking
SortedDictionary<K,V>Keys kept in orderSlower than DictionaryLeaderboards by score, timelines
LinkedList<T>Insert or remove next to a known nodeIndexing; memory useRarely the best choice in games

"Fast" and "slow" here are about how the cost grows with the number of items. Finding a key in a dictionary costs about the same with 10 entries or 10,000. Searching a list for a value checks the items one by one, so 10,000 items take about 1,000 times as long as 10. Programmers write these as O(1) ("constant") and O(n) ("grows with n"). For a dozen items none of this matters; for thousands, checked every frame, it decides your frame rate.

Delegates and lambdas: passing behaviour around#

So far, variables have held data: numbers, strings, objects. A delegate is a variable that holds a method. You can store it, pass it to another method, and call it later. That lets you write code where part of the behaviour is decided by whoever calls it: "count the depths that pass this test", "when the lever moves, run this".

Delegates/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Delegates
{
    // A delegate type describes a method's shape: here, "takes an int, returns an int".
    delegate int Bonus(int score);

    class Program
    {
        static int Double(int score) => score * 2;

        static void Main()
        {
            // A delegate variable holds a method you can call later.
            Bonus bonus = Double;                  // a method group
            Console.WriteLine(bonus(21));

            bonus = score => score + 5;            // a lambda
            Console.WriteLine(bonus(21));

            // Func and Action are ready-made delegate types, so you rarely declare your own.
            Func<int, int, int> add = (a, b) => a + b;
            Action<string> shout = text => Console.WriteLine(text.ToUpper() + "!");
            Console.WriteLine(add(2, 3));
            shout("land ho");

            // Passing behaviour into a method
            int[] depths = { 12, 40, 7, 25 };
            Console.WriteLine(CountWhere(depths, d => d > 20));
            Console.WriteLine(CountWhere(depths, d => d % 2 == 1));

            // Multicast: one delegate, several methods, called in order
            Action alarm = () => Console.WriteLine("Ring the bell");
            alarm += () => Console.WriteLine("Light the beacon");
            alarm();

            // A closure: the lambda captures the variable 'caught', not a copy of it
            int caught = 0;
            Action haul = () => caught++;
            haul();
            haul();
            Console.WriteLine($"Caught: {caught}");

            // The classic trap: every lambda made in this for loop shares one i
            var later = new List<Func<int>>();
            for (int i = 0; i < 3; i++)
            {
                later.Add(() => i);
            }
            Console.WriteLine(CallAll(later));

            // The fix: copy the loop variable into a fresh local each pass
            later.Clear();
            for (int i = 0; i < 3; i++)
            {
                int copy = i;
                later.Add(() => copy);
            }
            Console.WriteLine(CallAll(later));
        }

        static string CallAll(List<Func<int>> functions)
        {
            var results = new List<int>();
            foreach (Func<int> f in functions)
            {
                results.Add(f());
            }
            return string.Join(" ", results);
        }

        static int CountWhere(int[] values, Func<int, bool> test)
        {
            int count = 0;
            foreach (int v in values)
            {
                if (test(v)) count++;
            }
            return count;
        }
    }
}
OutputText
42
26
5
LAND HO!
2
2
Ring the bell
Light the beacon
Caught: 2
3 3 3
0 1 2

Delegate types, Func and Action#

delegate int Bonus(int score); declares a delegate type: "a method that takes an int and returns an int". Any method with that shape can be stored in a Bonus variable, and calling the variable calls the method. Writing just the method's name without brackets (Bonus bonus = Double;) is called a method group; it refers to the method instead of calling it.

You'll rarely declare delegate types yourself, because .NET provides generic ones for every shape:

  • Action takes nothing and returns nothing. Action<string> takes a string; Action<int, bool> takes an int and a bool. Actions never return a value.
  • Func<TResult> returns a TResult. The last type is always the return type: Func<int, bool> takes an int and returns a bool, and Func<int, int, int> takes two ints and returns an int.
  • Predicate<T> is an older name for Func<T, bool>; List.Find and RemoveAll use it.

Lambdas#

A lambda is a method written inline, without a name: parameters, then => (read "goes to"), then the body. d => d > 20 takes d and returns whether it's over 20. Two parameters need brackets: (a, b) => a + b. None need empty brackets: () => Console.WriteLine("Ring"). A longer body uses braces and an explicit return. The compiler works out the parameter types from where the lambda is used: in CountWhere(depths, d => d > 20), it knows d is an int because the parameter is a Func<int, bool>.

Passing a lambda is how you hand a decision to someone else's code. CountWhere knows how to count; the caller says what to count. The List methods above, all of LINQ, Unity's WaitUntil, and UI button callbacks all work this way.

Multicast delegates#

A delegate can hold several methods. += adds one, -= removes one, and calling the delegate calls them all in the order they were added. If the delegate returns a value, you only get the last method's result, so multicast is used almost entirely with Action-style delegates. Events, in the next section, are built on this.

Closures and the loop trap#

A lambda can use local variables from the method around it. When it does, it captures the variable itself, not a copy of its value at that moment, and the lambda plus its captured variables is called a closure. That's why haul can increase caught: both refer to the same variable, which lives on as long as the lambda does.

The same rule causes the classic trap. A for loop has one variable i, changed each pass. Three lambdas created in the loop all capture that one variable, and by the time they run the loop has finished with i at 3, so they all return 3. The fix is to copy i into a new local inside the loop body; each pass creates a fresh copy, and each lambda captures its own. (A foreach loop variable is already fresh on every pass, so foreach doesn't have this problem.) In Unity this bites when you hook up several buttons in a for loop and every button reports the last index.

Events: announcing without knowing who listens#

Delegates let a method call code it doesn't know about. Events make that into a pattern: one object, the publisher, announces that something happened; any number of other objects, the subscribers, choose to be told. The publisher doesn't know who they are or how many there are. This is the main tool for keeping parts of a game independent: the health component announces "I changed", and the health bar, the sound effect and the achievement tracker each react, without the health component referencing any of them.

Events/Program.cs.NET consoleC#
using System;

namespace Events
{
    // The publisher: it announces things and doesn't know who's listening.
    class Lighthouse
    {
        public event Action<int> ShipSighted;    // the int is the distance in metres
        public event Action StormWarning;

        public void Scan(int distance)
        {
            Console.WriteLine($"[Lighthouse] something at {distance} m");
            ShipSighted?.Invoke(distance);       // ?. because nobody may be listening
        }

        public void Storm() => StormWarning?.Invoke();
    }

    // Subscribers: each one reacts in its own way.
    class Harbour
    {
        readonly string name;
        public Harbour(string name) { this.name = name; }

        public void OnShipSighted(int distance)
        {
            if (distance < 500)
            {
                Console.WriteLine($"  {name}: clear a berth");
            }
        }
    }

    class Logbook
    {
        int entries;
        public void Record(int distance) => Console.WriteLine($"  Log #{++entries}: ship at {distance} m");
    }

    class Program
    {
        static void Main()
        {
            var tower = new Lighthouse();
            var harbour = new Harbour("Port Wren");
            var log = new Logbook();

            tower.ShipSighted += harbour.OnShipSighted;   // subscribe
            tower.ShipSighted += log.Record;
            tower.StormWarning += () => Console.WriteLine("  Everyone: reef the sails");

            tower.Scan(800);
            tower.Scan(300);
            tower.Storm();

            tower.ShipSighted -= log.Record;              // unsubscribe
            tower.Scan(200);

            // tower.ShipSighted(100);   // error CS0070: only Lighthouse can raise its event
            // tower.ShipSighted = null; // error CS0070: outsiders can only += and -=
        }
    }
}
OutputText
[Lighthouse] something at 800 m
  Log #1: ship at 800 m
[Lighthouse] something at 300 m
  Port Wren: clear a berth
  Log #2: ship at 300 m
  Everyone: reef the sails
[Lighthouse] something at 200 m
  Port Wren: clear a berth
Lighthouse event ShipSighted ShipSighted?.Invoke(300) calls each one Harbour OnShipSighted(int distance) Logbook Record(int distance) Subscribers join with += and leave with -=
The publisher keeps a list of methods to call, nothing more. Adding a third subscriber, or removing one, doesn't change the Lighthouse class at all.

What the event keyword adds#

Without the word event, ShipSighted would be a public delegate field, and any code could call it (pretending a ship was sighted) or overwrite it with = (silently removing every other subscriber). event restricts outside code to exactly two operations, += and -=. Only the class that declares the event can raise it; anything else is error CS0070. That's the whole difference, and it's why every public "something happened" delegate should be an event.

Raising an event safely#

An event with no subscribers is null, and calling it then throws a NullReferenceException. ShipSighted?.Invoke(distance) calls it only if someone is listening. (This ?. is fine here because the event is a plain C# delegate; it's on Unity objects that ?. misbehaves, as T03 · C# Fundamentals: Types, Logic, Methods & Classes warned.) Subscribers run one after another, in the order they subscribed, on the same thread, before Invoke returns. If one throws an exception, the ones after it don't run, so keep handlers short and safe.

Designing events#

  • Name events for what happened, usually in the past tense: Died, HealthChanged, ShipSighted, LevelCompleted. Name handler methods On plus the event: OnShipSighted.
  • Pass what subscribers need as parameters: Action<int, int> for old and new health saves every subscriber from asking. .NET's own convention is EventHandler<TArgs> with a sender and an arguments object; game code more often uses Action<…>, which is shorter. Pick one style per project.
  • Raise events after the state has changed, so a subscriber that reads the publisher sees the new values.
  • Static events (such as a static event Action<Enemy> named AnyEnemyDied) let anyone hear about every instance without a reference to it. They're convenient, but subscribers stay subscribed until they unsubscribe, which leads to the next point.

LINQ: asking questions of collections#

Questions about collections ("which catches weigh over 400 g, heaviest first?", "how much did each boat land?") take a loop, an if, a temporary list and a sort when written by hand. LINQ (Language Integrated Query) answers them in one readable chain. It's a set of extension methods (see below) on IEnumerable<T>, which means it works on arrays, lists, dictionaries, sets and anything else you can foreach over. Add using System.Linq; to get them; without it, calls like .Where fail with error CS1061.

Linq/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;
using System.Linq;

namespace Linq
{
    class Catch
    {
        public string Boat { get; }
        public string Species { get; }
        public int Grams { get; }

        public Catch(string boat, string species, int grams)
        {
            Boat = boat;
            Species = species;
            Grams = grams;
        }
    }

    class Program
    {
        static void Main()
        {
            var day = new List<Catch>
            {
                new Catch("Heron", "mackerel", 340),
                new Catch("Bluefin", "bass", 1200),
                new Catch("Heron", "sprat", 25),
                new Catch("Marigold", "mackerel", 410),
                new Catch("Bluefin", "pollock", 860),
                new Catch("Heron", "bass", 950),
            };

            // Filter, sort, and pick out one value from each item
            IEnumerable<string> heavy = day
                .Where(c => c.Grams >= 400)
                .OrderByDescending(c => c.Grams)
                .Select(c => $"{c.Species} ({c.Grams} g)");
            Console.WriteLine("Heavy: " + string.Join(", ", heavy));

            // Questions with a yes/no or single answer
            Console.WriteLine($"Any sprat? {day.Any(c => c.Species == "sprat")}");
            Console.WriteLine($"All over 20 g? {day.All(c => c.Grams > 20)}");
            Console.WriteLine($"Mackerel caught: {day.Count(c => c.Species == "mackerel")}");
            Console.WriteLine($"Total: {day.Sum(c => c.Grams)} g, heaviest: {day.Max(c => c.Grams)} g");
            Console.WriteLine($"Average: {day.Average(c => c.Grams):F1} g");

            Catch firstBass = day.First(c => c.Species == "bass");
            Catch eel = day.FirstOrDefault(c => c.Species == "eel");
            Console.WriteLine($"First bass: {firstBass.Boat}. Any eel? {eel != null}");

            // Group by boat, then summarise each group
            var perBoat = day
                .GroupBy(c => c.Boat)
                .Select(g => new { Boat = g.Key, Total = g.Sum(c => c.Grams), Count = g.Count() })
                .OrderByDescending(b => b.Total);
            foreach (var boat in perBoat)
            {
                Console.WriteLine($"  {boat.Boat,-9}{boat.Total,5} g  (catches: {boat.Count})");
            }

            // Distinct species in alphabetical order
            Console.WriteLine(string.Join(" ", day.Select(c => c.Species).Distinct().OrderBy(s => s)));

            // The same query in query syntax
            var heronCatches = from c in day
                               where c.Boat == "Heron"
                               orderby c.Grams
                               select c.Species;
            Console.WriteLine("Heron: " + string.Join(", ", heronCatches));
        }
    }
}
OutputText
Heavy: bass (1200 g), bass (950 g), pollock (860 g), mackerel (410 g)
Any sprat? True
All over 20 g? True
Mackerel caught: 2
Total: 3785 g, heaviest: 1200 g
Average: 630.8 g
First bass: Bluefin. Any eel? False
  Bluefin   2060 g  (catches: 2)
  Heron     1315 g  (catches: 3)
  Marigold   410 g  (catches: 1)
bass mackerel pollock sprat
Heron: sprat, mackerel, bass

The operators you'll use most#

OperatorWhat it doesReturns
Where(c => test)Keeps items that pass the testA sequence
Select(c => value)Turns each item into something elseA sequence
OrderBy, OrderByDescendingSorts by a key; add ThenBy for tiesA sequence
GroupBy(c => key)Groups items that share a keyA sequence of groups
Distinct, Take(n), Skip(n)Removes repeats; keeps the first n; skips nA sequence
First, FirstOrDefaultThe first item (that passes a test)One item
Any, All, CountIs there one? Do all pass? How many?bool or int
Sum, Max, Min, AverageTotals and extremes of a valueA number
ToList, ToArray, ToDictionaryRuns the query and stores the resultsA collection

Each operator returns something you can call the next one on, so you read a query top to bottom like a recipe: take the day's catches, keep the heavy ones, sort them, describe each. GroupBy gives you groups, each with a Key and the items that share it, which you usually summarise with Select. The new { Boat = …, Total = … } in that query creates an anonymous type, a quick unnamed class with read-only properties, handy inside one method and not usable outside it.

First throws an InvalidOperationException ("Sequence contains no elements") when nothing matches; FirstOrDefault returns the default instead (null for a class). Use First when a match must exist and its absence is a bug, and FirstOrDefault plus a null check when "none" is a normal answer. Max and Average on an empty sequence of numbers also throw.

The last query uses query syntax, which reads like SQL (a database language) and compiles to the same method calls. Most C# and Unity code uses the method syntax; you'll see both.

LINQ in Unity: where it belongs#

LINQ is clear and safe, but it isn't free. Every query creates small objects behind the scenes (the iterator objects that run the chain, and often delegates for the lambdas), and sorting and grouping build temporary collections. In code that runs once (loading a level, building a menu, editor tools, a turn-based game's turn) that cost is nothing. In Update, on hundreds of objects, every frame, those allocations add up and feed the garbage collector, which can cause stutters. The usual rule in Unity: LINQ for setup, tools and occasional work; plain loops in per-frame code. When you're not sure, measure with the Profiler (T45 · Performance & Profiling: CPU, GPU, Memory & GC) rather than guessing.

Iterators and deferred execution#

A LINQ query doesn't run when you write it. It runs when something asks for its results, and only as far as needed. This is called deferred execution, and it comes from a C# feature you can use yourself: iterator methods, written with yield return. They're worth understanding for one more reason: Unity's coroutines (T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable) are iterator methods.

Lazy/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;
using System.Linq;

namespace Lazy
{
    class Program
    {
        // An iterator method: yield return hands back one value, then pauses here.
        static IEnumerable<int> Soundings()
        {
            Console.WriteLine("  (lowering the line)");
            yield return 12;
            Console.WriteLine("  (moving on)");
            yield return 30;
            Console.WriteLine("  (moving on)");
            yield return 8;
            Console.WriteLine("  (line back on deck)");
        }

        static void Main()
        {
            Console.WriteLine("Calling Soundings()");
            IEnumerable<int> depths = Soundings();      // nothing runs yet
            Console.WriteLine("Starting the loop");
            foreach (int d in depths)
            {
                Console.WriteLine($"Depth {d}");
            }

            Console.WriteLine("Building a query");
            IEnumerable<int> deep = Soundings().Where(d => d > 10);   // still nothing runs
            Console.WriteLine("Taking the first deep one");
            Console.WriteLine($"First deep: {deep.First()}");          // runs only as far as needed

            Console.WriteLine("Counting twice");
            Console.WriteLine($"Count: {deep.Count()}");                // runs the whole thing
            Console.WriteLine($"Count: {deep.Count()}");                // and again, from the start

            Console.WriteLine("Saving the results once");
            List<int> saved = deep.ToList();                            // runs once, stores the values
            Console.WriteLine($"Saved: {string.Join(", ", saved)}");
        }
    }
}
OutputText
Calling Soundings()
Starting the loop
  (lowering the line)
Depth 12
  (moving on)
Depth 30
  (moving on)
Depth 8
  (line back on deck)
Building a query
Taking the first deep one
  (lowering the line)
First deep: 12
Counting twice
  (lowering the line)
  (moving on)
  (moving on)
  (line back on deck)
Count: 2
  (lowering the line)
  (moving on)
  (moving on)
  (line back on deck)
Count: 2
Saving the results once
  (lowering the line)
  (moving on)
  (moving on)
  (line back on deck)
Saved: 12, 30

How yield return works#

A method that returns IEnumerable<T> and contains yield return isn't run like a normal method. Calling Soundings() runs none of its body; it returns an object that knows how to run the body step by step. Each time a foreach asks for the next item, the method runs from where it last stopped until it reaches the next yield return, hands back that value, and pauses again, with all its local variables kept. When the method reaches its end (or a yield break), the sequence is over. The output shows this clearly: the "moving on" lines appear between the depths, not all at the start.

The compiler does this by turning your method into a hidden class with a MoveNext method and a field for each local variable: a small state machine. You never see it, but it's why an iterator can pause, and why Unity can resume a coroutine on the next frame: Unity calls MoveNext once per frame instead of in a tight loop.

Deferred execution and its traps#

LINQ operators like Where and Select are iterators too. Building deep ran nothing. First() pulled items one at a time and stopped at the first match, so the line was lowered once and the rest of Soundings never ran. That's efficient: a query over a million items that only needs the first match does only the work required.

Soundings() yield return 12 … .Where(d > 10) tests each item .First() wants one item 1. next? 2. next? 3. here's 12 4. 12 passes Requests flow right to left; values flow left to right, one at a time.
A LINQ chain is pulled from the end. Nothing runs until the last step asks for an item, and work stops as soon as it has what it needs.

The trap is the other side of the same coin: a query is a recipe, not a result. Every time you use it, it runs again from the start. deep.Count() twice lowered the line twice. If the source is slow, or changes in between, or the query creates objects, running it repeatedly costs time and can even give different answers. The rule: if you'll use a query's results more than once, run it once with ToList() or ToArray() and use the stored list. IDEs flag the risk as "possible multiple enumeration".

Small tools you'll see everywhere#

A few more features turn up constantly in Unity code and documentation. None is large, but each saves you from writing a clumsy workaround.

Toolkit/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Toolkit
{
    // Extension methods live in a static class; "this" on the first parameter
    // makes them callable as if they were members of that type.
    static class SeaExtensions
    {
        public static bool IsDeep(this int metres) => metres > 20;

        public static string Shout(this string text) => text.ToUpper() + "!";

        public static T PickLast<T>(this List<T> list) => list[list.Count - 1];
    }

    // [Flags]: each value is one bit, so several can be combined in one variable.
    [Flags]
    enum Cargo
    {
        None = 0,
        Fish = 1 << 0,
        Salt = 1 << 1,
        Timber = 1 << 2,
        Lamps = 1 << 3,
    }

    class Program
    {
        // A tuple returns two values without writing a class for them.
        static (int lowest, int highest) Range(int[] values)
        {
            int lo = values[0], hi = values[0];
            foreach (int v in values)
            {
                if (v < lo) lo = v;
                if (v > hi) hi = v;
            }
            return (lo, hi);
        }

        static void Main()
        {
            Console.WriteLine(35.IsDeep());
            Console.WriteLine("man overboard".Shout());
            Console.WriteLine(new List<string> { "Ada", "Bram", "Cora" }.PickLast());

            var (low, high) = Range(new[] { 14, 3, 27, 9 });   // deconstruction
            Console.WriteLine($"Tide range: {low} to {high}");

            // Nullable value types: an int that can also be "no value"
            int? bestTime = null;
            Console.WriteLine($"Best time set? {bestTime.HasValue}, shown as {bestTime ?? -1}");
            bestTime = 42;
            Console.WriteLine($"Best time: {bestTime.Value}");

            Cargo hold = Cargo.Fish | Cargo.Lamps;          // combine
            hold |= Cargo.Salt;                             // add one
            hold &= ~Cargo.Fish;                            // remove one
            Console.WriteLine($"Hold: {hold}");
            Console.WriteLine($"Carrying lamps? {hold.HasFlag(Cargo.Lamps)}");
            Console.WriteLine($"Carrying timber? {(hold & Cargo.Timber) != 0}");
        }
    }
}
OutputText
True
MAN OVERBOARD!
Cora
Tide range: 3 to 27
Best time set? False, shown as -1
Best time: 42
Hold: Salt, Lamps
Carrying lamps? True
Carrying timber? False

Extension methods#

An extension method is a static method in a static class whose first parameter has this in front of it. You can then call it as if it were a member of that type: 35.IsDeep() instead of SeaExtensions.IsDeep(35). It doesn't really change the type and can't see its private members; it's a nicer way to call a helper. All of LINQ is extension methods on IEnumerable<T>, which is why a using System.Linq; line makes .Where appear on every list. In Unity you'll write extensions for things like transform.ResetLocal() or vector.WithY(0). Keep them small and obviously named; an extension that does something surprising is hard to find, because it isn't in the class it appears to belong to.

Tuples and deconstruction#

A tuple groups a few values without declaring a class: (int lowest, int highest) is a type, and (lo, hi) creates one. var (low, high) = Range(…) deconstructs it into two variables. Tuples are value types and ideal for returning two or three values from a private method, which is cleaner than out parameters. If the same group of values travels around a lot, or needs behaviour, give it a proper struct or class with a name instead.

Nullable value types#

A value type like int can't be null; it always holds a number. Sometimes you need "no value yet" (no best lap time, no selected slot), and using a magic number like -1 for that invites bugs. int? (short for Nullable<int>) is an int that can also be null. HasValue says whether it holds one, Value gets it (and throws if there isn't one), and ?? gives a fallback. Unity uses this for optional values such as an optional colour override.

Flags enums#

A normal enum holds one value at a time. A [Flags] enum gives each value its own bit (1, 2, 4, 8…, written 1 << 0, 1 << 1…), so one variable can hold any combination, exactly like the layer masks T03 · C# Fundamentals: Types, Logic, Methods & Classes introduced. | combines flags, &= ~flag removes one, and (value & flag) != 0 or HasFlag tests for one. The [Flags] attribute also makes ToString print "Salt, Lamps" instead of a number. Unity's Inspector shows a flags enum field as a multi-select dropdown. Always include a None = 0 value.

Memory: the stack, the heap and the garbage collector#

C# manages memory for you: you create objects with new and never free them by hand. That's a huge relief, but it isn't magic, and in a game it has a visible cost. Knowing where your data lives and what creates work for the garbage collector is the difference between a game that runs smoothly and one that hitches every few seconds.

Memory/Program.cs.NET consoleC#
using System;
using System.Text;

namespace Memory
{
    struct Buoy          // a value type: the variable holds the data itself
    {
        public int X;
    }

    class Boat           // a reference type: the variable holds an arrow to the data
    {
        public int X;
    }

    // IDisposable: for objects that hold something that must be released promptly.
    class Logfile : IDisposable
    {
        readonly string name;

        public Logfile(string name)
        {
            this.name = name;
            Console.WriteLine($"  open {name}");
        }

        public void Write(string line) => Console.WriteLine($"  {name}: {line}");

        public void Dispose() => Console.WriteLine($"  close {name}");
    }

    class Program
    {
        static void Main()
        {
            // Copying a struct copies the data; copying a class copies the arrow.
            Buoy b1 = new Buoy { X = 1 };
            Buoy b2 = b1;
            b2.X = 99;
            Boat s1 = new Boat { X = 1 };
            Boat s2 = s1;
            s2.X = 99;
            Console.WriteLine($"Buoys: {b1.X} {b2.X}. Boats: {s1.X} {s2.X}");

            // Boxing: putting a value type in an object variable makes a heap copy.
            int tide = 5;
            object boxed = tide;            // box
            tide = 6;
            int unboxed = (int)boxed;       // unbox: the box still holds 5
            Console.WriteLine($"tide {tide}, box {unboxed}");

            object a = 7, b = 7;            // two separate boxes
            Console.WriteLine($"a == b: {a == b}, a.Equals(b): {a.Equals(b)}");

            // using: Dispose runs at the end of the block, even if an exception is thrown
            Console.WriteLine("Writing the log:");
            try
            {
                using (var log = new Logfile("voyage.txt"))
                {
                    log.Write("left port");
                    throw new InvalidOperationException("storm");
                }
            }
            catch (InvalidOperationException e)
            {
                Console.WriteLine($"  caught: {e.Message}");
            }

            // StringBuilder: one growing buffer instead of a new string every time
            var line = new StringBuilder();
            for (int knot = 1; knot <= 5; knot++)
            {
                line.Append(knot).Append(knot < 5 ? "-" : "");
            }
            Console.WriteLine($"Knots: {line}");
        }
    }
}
OutputText
Buoys: 1 99. Boats: 99 99
tide 6, box 5
a == b: False, a.Equals(b): True
Writing the log:
  open voyage.txt
  voyage.txt: left port
  close voyage.txt
  caught: storm
Knots: 1-2-3-4-5

The stack and the heap#

A running program keeps data in two main places. The stack is a small, fast area that holds each method call's local variables. When a method is called, room for its locals is reserved on top of the stack; when it returns, that room is released instantly. Nothing has to be cleaned up later. The managed heap is a large area for objects that must outlive the method that created them. Every new of a class, every array, every string goes there, and the variable on the stack holds only a reference: the arrow you met in T03 · C# Fundamentals: Types, Logic, Methods & Classes.

Stack (Main's locals) b1 Buoy { X = 1 } b2 Buoy { X = 99 } tide 6 s1 s2 boxed Managed heap Boat { X = 99 } box: int 5 Objects stay here until nothing refers to them; then the garbage collector reclaims the space.
Structs live inside their variables, so each copy is separate. Class instances and boxes live on the heap; variables only point at them, so two variables can share one object.

"Value types on the stack, reference types on the heap" is a useful first picture but not the whole truth. A value type lives wherever its variable lives: a local int is on the stack, but an int field inside a class instance is on the heap, inside that object. What really matters is the rule from T03 · C# Fundamentals: Types, Logic, Methods & Classes: copying a value type copies the data, and copying a reference copies the arrow. The output's first line shows it again: changing b2 left b1 alone, while changing s2 changed the one boat that s1 also points at.

The garbage collector#

Heap objects aren't freed when a method ends, because something else might still refer to them. Instead, when the heap fills up, the garbage collector (GC) runs: it finds every object still reachable from your variables, static fields and other live objects, and reclaims the space of everything else, the garbage. You never have to free memory, and you can't use memory that's been freed.

The price is time. Collection does real work, and while it runs your game isn't running. Unity's documentation is specific about its collector:

  • Unity uses the Boehm–Demers–Weiser garbage collector for both Mono and IL2CPP.
  • It is not generational. Desktop .NET treats young, short-lived objects cheaply; Unity's collector doesn't, so lots of small temporary objects are more expensive than a .NET book might suggest.
  • Incremental collection is on by default (Player settings, Configuration section). It spreads the work over several frames to avoid one long pause, but it doesn't make collection cheaper overall. The Web platform doesn't support it.

A small allocation is cheap on its own. The trouble is allocation that happens every frame. Unity's documentation gives the arithmetic: 1 KB of temporary allocation per frame at 60 frames per second is 3.6 MB of garbage a minute, all of which the collector has to process. The goal for code that runs every frame is zero allocations, or as close as you can get. Setup code, menus and one-off events can allocate freely.

What allocates, and what to do instead#

Allocates on the heapIn per-frame code, prefer
new of a class, array or List each frameCreate it once in a field and reuse it (call Clear())
Building strings: +, interpolation, ToString()Update text only when the value changes; StringBuilder for long text
Boxing: a value type passed as object or an interfaceGeneric methods and collections, which keep values unboxed
Lambdas that capture variables, LINQ queriesPlain loops; delegates created once and stored
params methods (a hidden array per call)Overloads with fixed parameters
foreach over an IEnumerable<T> variableforeach over the concrete List<T> or array
Unity APIs that return arrays (GetComponents<T>())Overloads that fill a list you pass in, called once and cached
gameObject.tag == "Boss" (the tag getter makes a string)CompareTag("Boss")
Instantiating and destroying many objectsAn object pool (ObjectPool<T>)

The foreach line needs a word of explanation. A List<T> has a struct enumerator, so foreach over a variable of type List<T> allocates nothing. Over a variable of type IEnumerable<T>, the same enumerator has to be returned as an interface, which boxes it. Same list, same loop, different variable type.

Boxing#

Boxing happens when a value type is stored somewhere that expects a reference: an object variable, an interface variable, or a non-generic collection. C# quietly creates a small heap object (the box) and copies the value into it. Unboxing with a cast copies it back out. The sample shows the two surprises. First, the box is a copy: changing tide later doesn't change it. Second, == on two object variables compares references, and two separate boxes of 7 are different objects, so a == b is False while a.Equals(b), which compares the values, is True.

Unity's documentation calls boxing one of the most common sources of unintended allocations, and notes that compilers don't warn about it. Methods that take object parameters box every number you pass them; string.Format("{0}", score) is a common example. Generics are the main cure: a List<int> stores ints directly, and a generic method with T doesn't box.

Structs and copying#

Structs avoid the heap, which makes them ideal for small values made in large numbers: Vector3, Color, Quaternion, grid coordinates. The trade-off is that every assignment and every argument copies the whole struct, so a large struct is slow to pass around. Keep structs small (a few fields) and treat them as values. Two features help: a readonly struct promises its fields never change, and an in parameter (void Draw(in Grid grid)) passes a struct by read-only reference instead of copying it. If a type needs an identity that several parts of the game share, it should be a class.

IDisposable and using#

The garbage collector frees memory, but it runs whenever it likes. Some objects hold things that must be released now: an open file, a network connection, a block of native memory. Those classes implement IDisposable, and you call Dispose() when you're done. The using statement does it for you: using (var log = new Logfile(…)) { … } calls Dispose when the block ends, even if an exception is thrown, as the "close voyage.txt" line before "caught: storm" shows. C# 8 added a shorter form, using var log = new Logfile(…);, which disposes at the end of the enclosing block.

In Unity you'll meet IDisposable with native collections such as NativeArray (which must be disposed or they leak), some editor GUI helpers, and ObjectPool<T>.Get(out var pooled), which hands back an object that returns itself to the pool when disposed.

Object pooling#

The generic Pool<T> from earlier is a tiny version of a pattern every action game uses. Creating and destroying objects all the time (bullets, sparks, damage numbers) allocates, and in Unity Instantiate and Destroy are expensive in their own right. A pool keeps finished objects, deactivated, and hands them out again. Unity's UnityEngine.Pool namespace provides ObjectPool<T> plus pools for lists and dictionaries; you give it delegates for creating, getting, releasing and destroying an object, which is the generics and delegates of this chapter working together. Pooling GameObjects properly, including resetting their state, is covered in T12 · Game Programming Patterns in Unity and T45 · Performance & Profiling: CPU, GPU, Memory & GC.

From the console to Unity#

Every tool in this chapter appears in ordinary Unity scripts. The four short scripts below make a lever that opens a gate. They use an interface, an event, subscription in OnEnable and OnDisable, and an interface lookup with TryGetComponent. You don't need input handling yet: you'll trigger the lever from the Inspector.

IInteractable.csUnity sandboxC#
namespace Handbook.T04
{
    // Anything the player can use: a lever, a door, a chest...
    public interface IInteractable
    {
        string Prompt { get; }
        void Interact();
    }
}
Lever.csUnity sandboxC#
using System;
using UnityEngine;

namespace Handbook.T04
{
    public class Lever : MonoBehaviour, IInteractable
    {
        [SerializeField] Transform handle;
        [SerializeField] bool isOn;

        public event Action<bool> Toggled;   // listeners react; the lever doesn't know who they are

        public string Prompt => isOn ? "Push the lever up" : "Pull the lever down";

        [ContextMenu("Interact")]   // adds "Interact" to this component's ⋮ menu
        public void Interact()
        {
            isOn = !isOn;
            if (handle != null)
            {
                handle.localRotation = Quaternion.Euler(isOn ? -40f : 40f, 0f, 0f);
            }
            Toggled?.Invoke(isOn);
        }
    }
}
Gate.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T04
{
    public class Gate : MonoBehaviour
    {
        [SerializeField] Lever lever;
        [SerializeField] Vector3 openOffset = new Vector3(0f, 3f, 0f);

        Vector3 closedPosition;

        void Awake() => closedPosition = transform.localPosition;

        // Subscribe when enabled, unsubscribe when disabled: they always pair up.
        void OnEnable()
        {
            if (lever != null) lever.Toggled += OnLeverToggled;
        }

        void OnDisable()
        {
            if (lever != null) lever.Toggled -= OnLeverToggled;
        }

        void OnLeverToggled(bool isOn)
        {
            transform.localPosition = isOn ? closedPosition + openOffset : closedPosition;
        }
    }
}
InteractProbe.csUnity sandboxC#
using UnityEngine;

namespace Handbook.T04
{
    // Stands in for the player until you have input (T07): uses whatever target it's given.
    public class InteractProbe : MonoBehaviour
    {
        [SerializeField] GameObject target;

        [ContextMenu("Use Target")]
        void UseTarget()
        {
            if (target != null && target.TryGetComponent(out IInteractable thing))
            {
                Debug.Log($"{name} uses {target.name}: {thing.Prompt}");
                thing.Interact();
            }
            else
            {
                Debug.Log($"{name}: {(target != null ? target.name : "no target")} can't be used");
            }
        }
    }
}

To try them in the sandbox or any 3D project:

  1. Create a Cube named Lever (scale 0.3, 1, 0.3) with a child Cube named Handle (scale 0.6, 0.6, 0.6, position 0, 0.6, 0). Add the Lever script and drag Handle into its Handle slot.
  2. Create a Cube named Gate (scale 4, 3, 0.3) a few metres away. Add the Gate script and drag the Lever object into its Lever slot.
  3. Create an empty GameObject named Probe, add InteractProbe, and drag the Lever object into Target.
  4. Press Play. On the Probe's InteractProbe component, open the ⋮ menu and choose Use Target. The Console logs the prompt, the handle tips over and the gate rises. Choose it again to close the gate. Then drag the Gate into the Probe's Target and try again: there's no IInteractable on it, so nothing happens.

Look at what each script knows. The probe knows only IInteractable; it would work just as well on a chest or a door. The lever knows nothing about the gate; it raises Toggled, and any number of gates, lights or sounds could subscribe. The gate knows the lever, because it chose to listen to it. Add a second gate and the lever's code doesn't change. That's the "depend on as little as possible" idea from the start of the chapter, in a scene.

Guided build: Harbour Ledger#

You'll build a small simulation of a harbour master's day. Vessels of three kinds arrive on a schedule, some are searched and turned away, the rest queue for two berths, stay a while, pay a fee and leave. At the end of the day a report summarises the takings. It's a console program, so you can run it in seconds and check every step against the output below, and it uses every tool in this chapter:

  • Inheritance: an abstract Vessel with three derived kinds that override the fee and the length of stay.
  • An interface: IInspectable, implemented by only two of the three kinds.
  • Generics: a Registry<T> that stores any kind of vessel by name.
  • Collections: a Queue for the waiting line, a List of moorings, and dictionaries for cargo, the schedule and the takings.
  • Events: the port announces docking, departures and refusals, and knows nothing about who listens.
  • LINQ: the end-of-day report.

Create a console project named Harbour (set LangVersion to 9.0 as in T03 · C# Fundamentals: Types, Logic, Methods & Classes) and add the four files below.

Step 1 · The vessels#

Harbour/Vessels.cs.NET consoleC#
using System.Collections.Generic;

namespace Harbour
{
    // Anything the harbour master searches before it may join the queue.
    interface IInspectable
    {
        bool PassesInspection(out string reason);
    }

    abstract class Vessel
    {
        public string Name { get; }
        public int Length { get; }   // metres

        protected Vessel(string name, int length)
        {
            Name = name;
            Length = length;
        }

        public abstract string Kind { get; }
        public abstract int StayTicks { get; }

        // Two coins per metre; some kinds of vessel change the rule.
        public virtual int Fee => Length * 2;

        public override string ToString() => $"{Name} ({Kind})";
    }

    class Trader : Vessel, IInspectable
    {
        readonly Dictionary<string, int> hold = new Dictionary<string, int>();

        public Trader(string name, int length) : base(name, length) { }

        public override string Kind => "trader";
        public override int StayTicks => 2;
        public override int Fee => base.Fee + 3 * hold.Count;   // plus 3 per kind of goods

        // Returning this lets calls chain: new Trader(...).Load(...).Load(...)
        public Trader Load(string goods, int crates)
        {
            hold[goods] = hold.TryGetValue(goods, out int already) ? already + crates : crates;
            return this;
        }

        public bool PassesInspection(out string reason)
        {
            reason = hold.ContainsKey("black powder") ? "black powder in the hold" : "";
            return reason == "";
        }
    }

    class Fisher : Vessel
    {
        public Fisher(string name, int length) : base(name, length) { }

        public override string Kind => "fishing boat";
        public override int StayTicks => 1;
        public override int Fee => base.Fee / 2;   // fishing boats pay half
    }

    class Ferry : Vessel, IInspectable
    {
        public int Passengers { get; }

        public Ferry(string name, int length, int passengers) : base(name, length)
        {
            Passengers = passengers;
        }

        public override string Kind => "ferry";
        public override int StayTicks => 1;
        public override int Fee => base.Fee + Passengers;   // plus a coin per passenger

        public bool PassesInspection(out string reason)
        {
            int limit = Length * 2;
            reason = Passengers > limit ? $"{Passengers} passengers, limit {limit}" : "";
            return reason == "";
        }
    }
}
  • Vessel is abstract: there's no such thing as a vessel of no kind. Kind and StayTicks are abstract properties, so every vessel must state them; Fee is virtual with a default of two coins per metre.
  • Each kind overrides Fee differently, and two of them call base.Fee to build on the default rather than repeat it. A fishing boat 12 m long pays 12; a ferry 24 m long with 40 passengers pays 88.
  • Only Trader and Ferry implement IInspectable. Fishing boats aren't searched, and nothing in the port has to know that: it simply asks each vessel whether it's inspectable.
  • Load returns this, so loading reads as one chain. The cargo is a dictionary from goods to crate count, using the TryGetValue pattern to add to an existing count.

Step 2 · A generic registry#

Harbour/Registry.cs.NET consoleC#
using System.Collections.Generic;

namespace Harbour
{
    // One registry class for any kind of vessel: Registry<Ferry>, Registry<Vessel>...
    class Registry<T> where T : Vessel
    {
        readonly Dictionary<string, T> byName = new Dictionary<string, T>();

        public int Count => byName.Count;
        public IEnumerable<T> All => byName.Values;

        public bool Register(T vessel)
        {
            if (byName.ContainsKey(vessel.Name))
            {
                return false;   // names must be unique
            }
            byName.Add(vessel.Name, vessel);
            return true;
        }

        public bool TryFind(string name, out T vessel) => byName.TryGetValue(name, out vessel);
    }
}

The constraint where T : Vessel means the registry can use vessel.Name on a T, and rejects Registry<string> at compile time. A Registry<Ferry> would hold only ferries and give them back as ferries, with no casts; the program uses Registry<Vessel> to hold all kinds. All exposes the values as IEnumerable<T>, so callers can loop and query but can't add behind the registry's back. TryFind follows the same pattern as TryGetValue.

Step 3 · The port and its events#

Harbour/Port.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Harbour
{
    class Port
    {
        // A vessel at a berth, and how many more ticks it stays.
        class Mooring
        {
            public Vessel Vessel;
            public int TicksLeft;
        }

        readonly int berths;
        readonly Queue<Vessel> waiting = new Queue<Vessel>();
        readonly List<Mooring> moored = new List<Mooring>();

        public event Action<Vessel> Docked;
        public event Action<Vessel, int> Departed;          // the int is the fee paid
        public event Action<Vessel, string> TurnedAway;     // the string is the reason

        public Port(int berths)
        {
            this.berths = berths;
        }

        public int Waiting => waiting.Count;

        public void Arrive(Vessel vessel)
        {
            if (vessel is IInspectable searched && !searched.PassesInspection(out string reason))
            {
                TurnedAway?.Invoke(vessel, reason);
                return;
            }
            waiting.Enqueue(vessel);
        }

        public void Tick()
        {
            // Every moored vessel stays one tick less; those at zero pay and leave.
            foreach (Mooring m in moored)
            {
                m.TicksLeft--;
                if (m.TicksLeft == 0)
                {
                    Departed?.Invoke(m.Vessel, m.Vessel.Fee);
                }
            }
            moored.RemoveAll(m => m.TicksLeft == 0);

            // Free berths are filled from the front of the queue.
            while (moored.Count < berths && waiting.Count > 0)
            {
                Vessel next = waiting.Dequeue();
                moored.Add(new Mooring { Vessel = next, TicksLeft = next.StayTicks });
                Docked?.Invoke(next);
            }
        }
    }
}
  • Arrive uses a type pattern and an interface in one line: if the vessel can be inspected and fails, raise TurnedAway and stop. Everything else joins the Queue, so vessels dock in arrival order.
  • Mooring is a nested class, private to Port: nobody outside needs it. It's a class, not a struct, so m.TicksLeft-- inside the foreach changes the real mooring. (With a struct, the loop variable would be a copy, and the compiler would refuse to let you change it.)
  • Departures happen in the foreach, but removal waits until after it, with RemoveAll: you can't remove from a list while a foreach is walking it.
  • The port raises three events and never prints anything itself. Whether a departure is logged, added to the books or ignored is someone else's decision.

Step 4 · Run the day#

Harbour/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;
using System.Linq;

namespace Harbour
{
    class Program
    {
        static void Main()
        {
            var registry = new Registry<Vessel>();
            var port = new Port(berths: 2);
            var takings = new Dictionary<string, int>();   // kind of vessel -> coins
            var turnedAway = new List<string>();

            // Three subscribers. The port doesn't know any of them exists.
            port.Docked += vessel => Console.WriteLine($"  {vessel} docks");
            port.Departed += (vessel, fee) =>
            {
                Console.WriteLine($"  {vessel} leaves and pays {fee}");
                takings[vessel.Kind] = takings.TryGetValue(vessel.Kind, out int sum) ? sum + fee : fee;
            };
            port.TurnedAway += (vessel, reason) =>
            {
                Console.WriteLine($"  {vessel} is turned away: {reason}");
                turnedAway.Add(vessel.Name);
            };

            // Which vessels arrive on which tick
            var schedule = new Dictionary<int, Vessel[]>
            {
                [1] = new Vessel[] { new Trader("Saltwind", 30).Load("salt", 12).Load("rope", 4), new Fisher("Heron", 12) },
                [2] = new Vessel[] { new Ferry("Islander", 24, passengers: 40), new Fisher("Bluefin", 10) },
                [3] = new Vessel[] { new Trader("Corsair", 28).Load("black powder", 2) },
                [4] = new Vessel[] { new Trader("Marigold", 22).Load("lamps", 6), new Ferry("Daybreak", 18, passengers: 50) },
            };

            for (int tick = 1; tick <= 6; tick++)
            {
                Console.WriteLine($"Tick {tick}");
                if (schedule.TryGetValue(tick, out Vessel[] arrivals))
                {
                    foreach (Vessel vessel in arrivals)
                    {
                        registry.Register(vessel);
                        port.Arrive(vessel);
                    }
                }
                port.Tick();
            }

            // The report: LINQ over everything the subscribers collected
            Console.WriteLine();
            Console.WriteLine("Harbour report");
            foreach (var pair in takings.OrderByDescending(p => p.Value))
            {
                Console.WriteLine($"  {pair.Key,-14}{pair.Value,4} coins");
            }
            Console.WriteLine($"  {"total",-14}{takings.Values.Sum(),4} coins");

            Vessel best = registry.All
                .Where(v => !turnedAway.Contains(v.Name))
                .OrderByDescending(v => v.Fee)
                .First();
            Console.WriteLine($"Best customer: {best}, {best.Fee} coins");
            Console.WriteLine($"Turned away: {string.Join(", ", turnedAway)}");
            Console.WriteLine($"Registered: {registry.Count}, still waiting: {port.Waiting}");
        }
    }
}
OutputText
Tick 1
  Saltwind (trader) docks
  Heron (fishing boat) docks
Tick 2
  Heron (fishing boat) leaves and pays 12
  Islander (ferry) docks
Tick 3
  Corsair (trader) is turned away: black powder in the hold
  Saltwind (trader) leaves and pays 66
  Islander (ferry) leaves and pays 88
  Bluefin (fishing boat) docks
Tick 4
  Daybreak (ferry) is turned away: 50 passengers, limit 36
  Bluefin (fishing boat) leaves and pays 10
  Marigold (trader) docks
Tick 5
Tick 6
  Marigold (trader) leaves and pays 47

Harbour report
  trader         113 coins
  ferry           88 coins
  fishing boat    22 coins
  total          223 coins
Best customer: Islander (ferry), 88 coins
Turned away: Corsair, Daybreak
Registered: 7, still waiting: 0

Trace one tick to check you can follow it. On tick 2, Islander and Bluefin arrive. Islander is a ferry, so it's inspected (40 passengers, limit 48) and queues; Bluefin isn't inspectable and queues behind it. Then Tick runs: Heron's one tick is up, so it leaves and pays 12 (24 coins for 12 m, halved). That frees a berth, and Islander, at the front of the queue, docks. Bluefin waits until Saltwind and Islander leave on tick 3.

The subscribers are lambdas that capture takings and turnedAway, local variables of Main. That's a closure doing useful work: the port calls a lambda it knows nothing about, and the lambda updates the program's books. The report then queries those collections with LINQ: sort the takings, sum them, and find the best paying vessel that wasn't turned away.

Step 5 · What this has to do with Unity#

Swap the words and this is the skeleton of many Unity systems. The port is a spawner or wave manager with a queue; the vessels are enemy types with a shared base class and overridden stats; IInspectable is IDamageable or IInteractable; the events are EnemySpawned and EnemyDied, heard by the score display, the audio and the achievements; and the report is an end-of-level summary. The difference in Unity is when things run: instead of a for loop of ticks, Unity calls your components every frame. That's the subject of T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable.

Going further#

Design principles#

The ideas in this chapter, depending on interfaces, preferring composition, raising events instead of calling out, are the practical core of the SOLID principles and of most design patterns. T11 · Clean Code, SOLID & a C# Style Guide for Unity covers SOLID and a full C# style guide for Unity, and T12 · Game Programming Patterns in Unity covers the patterns games use most (observer, command, state, object pool, and more), all of which are built from the tools you now have.

Newer C# you'll read about#

Books and articles written for current .NET use features Unity's C# 9 lacks; T03 · C# Fundamentals: Types, Logic, Methods & Classes has the table. Two more in the area of this chapter: records (C# 9) are compact classes with value-style equality, but need a workaround in Unity and can't be serialized by it, so avoid them in Unity code; and Span<T>, a view over a slice of an array or other memory without copying it, is available in Unity 6 and is useful in low-level, allocation-free code.

async and await#

One big C# feature isn't in this chapter: async and await, for code that waits for something (a timer, a download, the next frame) without blocking. In Unity it comes with Unity's own Awaitable type and is easiest to learn alongside coroutines, so it's in T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable.

Common mistakes & debugging#

It won't compile#

ErrorWhat it meansFix
CS0534A class doesn't implement an abstract member it inheritedAdd the override, or make the class abstract too
CS0535A class doesn't implement a member of an interface it listsAdd a public member with the exact signature
CS0506Overriding a method that isn't virtual, abstract or overrideMark the base method virtual, or don't override
CS0114 (warning)A method hides a virtual base methodAdd override (you almost certainly meant it)
CS0144Creating an instance of an abstract class or an interfaceCreate a concrete derived class instead
CS0266Assigning a base-type value to a derived-type variableUse an is pattern or an explicit cast
CS0452, CS0310A type argument breaks a generic constraintUse a type that fits, or change the constraint
CS0070Raising or assigning an event from outside its classOnly += or -= outside; raise it inside
CS1061 on .Where, .First…The LINQ extension methods aren't visibleAdd using System.Linq;

As in T03 · C# Fundamentals: Types, Logic, Methods & Classes, the compiler reports some errors before others: it may show only the class-level errors (such as CS0534 and CS0535) first, then the errors inside method bodies once those are fixed. A list of errors that changes after a fix is normal.

It crashes#

  • KeyNotFoundException: you read a dictionary with dict[key] and the key isn't there. Use TryGetValue, or find out why the key was never added (a typo in a string key is the usual cause).
  • InvalidOperationException: "Sequence contains no elements": First, Max or Average on an empty sequence. Use FirstOrDefault and check for null, or check Any() first.
  • InvalidOperationException: "Collection was modified": you added to or removed from a list or dictionary inside a foreach over it. Collect the changes and apply them after the loop, or use RemoveAll.
  • InvalidCastException: a cast like (Crab)creature on something that isn't a crab. Use an is pattern.
  • NullReferenceException when raising an event: you called Changed(…) with no subscribers. Use Changed?.Invoke(…).
  • MissingReferenceException in Unity after something was destroyed: a destroyed object is still subscribed to an event. Unsubscribe in OnDisable.

It runs but gives the wrong answer#

  • Every callback reports the same index: lambdas created in a for loop share the loop variable. Copy it into a local inside the loop.
  • A query gives different results each time, or runs slowly: it's being enumerated more than once. Store the result with ToList().
  • An override never runs: the method was declared without override and is hiding the base one (check the CS0114 warning), or the base method isn't virtual.
  • Base class setup never runs in Unity: base and derived components both declare a private Awake or Start. Make it protected virtual and call base.Awake().
  • == says two equal numbers differ: they're boxed in object variables, and == compares the boxes. Use Equals, or better, keep them as their real type.
  • Changing a struct in a list or dictionary "doesn't work": the indexer returns a copy. Copy, change, assign back, as T03 · C# Fundamentals: Types, Logic, Methods & Classes showed for Vector3.
  • A dictionary can't find a key you just added: the key object changed after it was added, or your key class doesn't override Equals and GetHashCode consistently.
  • The game stutters every few seconds in Unity: probably garbage collection. Profile for GC Alloc in per-frame code: LINQ, string building, closures and new in Update are the usual suspects.

Exercises#

Do these in your playground project. Exercises 1, 5 and 8 extend the programs from this chapter, so copy those in first.

1 · A fourth creature

Add a Seal to the Shore program: 8 health, it "barks" when it acts, and it dodges the first hit it ever takes (printing "slips underwater. Missed!") but takes later hits normally. Add one to the list and hit it twice. Which lines outside the new class did you have to change?

Solution
Seal, added to Shore/Program.csC#
class Seal : Creature
{
    bool dodged;

    public Seal(string name) : base(name, 8) { }

    public override void TakeHit(int damage)
    {
        if (!dodged)
        {
            dodged = true;
            Console.WriteLine($"  {Name} slips underwater. Missed!");
            return;   // skip the normal damage rules this once
        }
        base.TakeHit(damage);
    }

    public override string Act() => $"{Name} barks.";
}

Only the list in Main changes (new Seal("Pebble")). The loop, Creature and the other creatures are untouched, which is polymorphism paying off. The dodge state is a private field because nobody else needs it. Hitting it twice prints the dodge, then "Pebble takes 4, health 4".

2 · Counting words

Count how often each word appears in "the tide turns and the tide returns and the gulls cry" using a Dictionary<string, int>, without LINQ. Print the words most frequent first, and alphabetically among equal counts.

Solution
WordCount/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace WordCount
{
    class Program
    {
        static void Main()
        {
            string song = "the tide turns and the tide returns and the gulls cry";
            var counts = new Dictionary<string, int>();

            foreach (string word in song.Split(' '))
            {
                counts.TryGetValue(word, out int seen);   // seen is 0 if the word is new
                counts[word] = seen + 1;
            }

            // Sort the pairs: most frequent first, then alphabetically
            var pairs = new List<KeyValuePair<string, int>>(counts);
            pairs.Sort((a, b) => a.Value != b.Value
                ? b.Value.CompareTo(a.Value)
                : string.CompareOrdinal(a.Key, b.Key));

            foreach (var pair in pairs)
            {
                Console.WriteLine($"{pair.Key,-8}{pair.Value}");
            }
        }
    }
}
OutputText
the     3
and     2
tide    2
cry     1
gulls   1
returns 1
turns   1

TryGetValue sets seen to 0 when the word is missing, so one line handles new and existing words. A dictionary has no order, so the pairs are copied into a list and sorted with a comparison lambda: by count, descending (note b before a), then by word.

3 · A generic Clamp

Write Clamp<T>(T value, T min, T max) that works for ints, doubles, strings, chars and anything else that can compare itself, and throws an ArgumentException if min is greater than max. Test it with all four types.

Solution
Clamp/Program.cs.NET consoleC#
using System;

namespace Clamp
{
    class Program
    {
        // Works for any type that can compare itself with another of its kind.
        static T Clamp<T>(T value, T min, T max) where T : IComparable<T>
        {
            if (min.CompareTo(max) > 0)
            {
                throw new ArgumentException("min must not be greater than max");
            }
            if (value.CompareTo(min) < 0) return min;
            if (value.CompareTo(max) > 0) return max;
            return value;
        }

        static void Main()
        {
            Console.WriteLine(Clamp(150, 0, 100));
            Console.WriteLine(Clamp(-3, 0, 100));
            Console.WriteLine(Clamp(2.5, 1.0, 3.0).ToString("F1"));
            Console.WriteLine(Clamp("walrus", "apple", "pear"));
            Console.WriteLine(Clamp('m', 'a', 'f'));
        }
    }
}
OutputText
100
0
2.5
pear
f

The constraint where T : IComparable<T> is what makes CompareTo available. Without it the method doesn't compile, because the compiler can't know that an arbitrary T can be compared. Unity's Mathf.Clamp exists for floats and ints; this version covers everything else.

4 · Only when it changes

Write a Thermometer class with a Degrees property and an event Changed that passes the old and new values, raised only when the value really changes. Subscribe a reporter and an "ice on deck" warning for readings below zero, set some values (including the same one twice), then unsubscribe the reporter and set one more.

Solution
Thermometer/Program.cs.NET consoleC#
using System;

namespace Thermometer
{
    class Thermometer
    {
        int degrees;

        // Raised only when the reading really changes: (old value, new value)
        public event Action<int, int> Changed;

        public int Degrees
        {
            get => degrees;
            set
            {
                if (value == degrees) return;   // no change, no event
                int old = degrees;
                degrees = value;
                Changed?.Invoke(old, value);
            }
        }
    }

    class Program
    {
        static void Main()
        {
            var deck = new Thermometer();
            Action<int, int> report = (from, to) => Console.WriteLine($"{from} -> {to}");
            deck.Changed += report;
            deck.Changed += (from, to) =>
            {
                if (to < 0) Console.WriteLine("  Ice on deck!");
            };

            deck.Degrees = 4;
            deck.Degrees = 4;      // same value: nothing printed
            deck.Degrees = -2;
            deck.Changed -= report;
            deck.Degrees = -5;     // only the ice warning is still subscribed
        }
    }
}
OutputText
0 -> 4
4 -> -2
  Ice on deck!
  Ice on deck!

The event is raised after the field is updated, so a subscriber that read Degrees would see the new value. To unsubscribe a lambda you need the same delegate instance, which is why the reporter is stored in a variable first; the ice warning was added as an inline lambda and can't be removed later. This "changed" pattern is exactly how a health component should drive a health bar.

5 · Three questions in LINQ

Using the catch list from the LINQ sample, write one query for each: (a) the total weight of bass; (b) the boats that landed more than 1 kg in total, alphabetically; (c) the lightest catch of each species, with its boat.

Solution
CatchQueries/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;
using System.Linq;

namespace CatchQueries
{
    class Catch
    {
        public string Boat { get; }
        public string Species { get; }
        public int Grams { get; }

        public Catch(string boat, string species, int grams)
        {
            Boat = boat;
            Species = species;
            Grams = grams;
        }
    }

    class Program
    {
        static void Main()
        {
            var day = new List<Catch>
            {
                new Catch("Heron", "mackerel", 340),
                new Catch("Bluefin", "bass", 1200),
                new Catch("Heron", "sprat", 25),
                new Catch("Marigold", "mackerel", 410),
                new Catch("Bluefin", "pollock", 860),
                new Catch("Heron", "bass", 950),
            };

            // (a) Total weight of bass
            int bass = day.Where(c => c.Species == "bass").Sum(c => c.Grams);
            Console.WriteLine($"Bass: {bass} g");

            // (b) Boats that landed more than 1 kg in total, alphabetically
            var bigBoats = day
                .GroupBy(c => c.Boat)
                .Where(g => g.Sum(c => c.Grams) > 1000)
                .Select(g => g.Key)
                .OrderBy(name => name);
            Console.WriteLine($"Over 1 kg: {string.Join(", ", bigBoats)}");

            // (c) The lightest catch of each species
            var lightest = day
                .GroupBy(c => c.Species)
                .OrderBy(g => g.Key)
                .Select(g => g.OrderBy(c => c.Grams).First());
            foreach (Catch c in lightest)
            {
                Console.WriteLine($"  {c.Species,-9}{c.Grams,5} g ({c.Boat})");
            }
        }
    }
}
OutputText
Bass: 2150 g
Over 1 kg: Bluefin, Heron
  bass       950 g (Heron)
  mackerel   340 g (Heron)
  pollock    860 g (Bluefin)
  sprat       25 g (Heron)

(b) and (c) both group first, then work on each group: a group is itself a sequence, so you can call Sum, OrderBy and First on it. In (c), First is safe because a group always has at least one item.

6 · Your own iterators

Write an iterator Countdown(int from) that yields from, from − 1, … 0, and yields nothing at all for a negative argument. Then write a generic extension method EveryOther<T>() on IEnumerable<T> that yields the first item, skips the second, and so on. Neither may build a list internally.

Solution
Sequences/Program.cs.NET consoleC#
using System;
using System.Collections.Generic;

namespace Sequences
{
    static class SequenceExtensions
    {
        // Yields the first item, skips one, yields the next, and so on.
        public static IEnumerable<T> EveryOther<T>(this IEnumerable<T> source)
        {
            bool take = true;
            foreach (T item in source)
            {
                if (take)
                {
                    yield return item;
                }
                take = !take;
            }
        }
    }

    class Program
    {
        static IEnumerable<int> Countdown(int from)
        {
            if (from < 0)
            {
                yield break;   // ends the sequence early: nothing at all
            }
            for (int n = from; n >= 0; n--)
            {
                yield return n;
            }
        }

        static void Main()
        {
            Console.WriteLine(string.Join(" ", Countdown(5)));
            Console.WriteLine(string.Join(" ", Countdown(5).EveryOther()));
            Console.WriteLine(string.Join(" ", new[] { "Ada", "Bram", "Cora", "Dov" }.EveryOther()));
            Console.WriteLine($"Items from Countdown(-1): {new List<int>(Countdown(-1)).Count}");
        }
    }
}
OutputText
5 4 3 2 1 0
5 3 1
Ada Cora
Items from Countdown(-1): 0

EveryOther works on any sequence, including another iterator, and pulls items one at a time, so Countdown(5).EveryOther() never holds more than one number. That's how LINQ's own operators are written. yield break ends the sequence; a plain return isn't allowed in an iterator.

7 · Find the garbage

This Unity method runs every frame on a turret. List everything in it that allocates on the heap each frame, and rewrite it so that, once warmed up, it allocates nothing. Assume Enemy is a component, range is a float field and status is a UI text field.

Turret.cs (excerpt)C#
void Update()
{
    Enemy[] all = FindObjectsByType<Enemy>(FindObjectsSortMode.None);
    List<Enemy> near = all
        .Where(e => Vector3.Distance(e.transform.position, transform.position) < range)
        .ToList();
    status.text = "Targets: " + near.Count;
    foreach (Enemy e in near)
    {
        if (e.tag == "Boss") Debug.Log("Boss in range!");
    }
}
Solution

Allocations each frame: the array returned by FindObjectsByType (which is also slow, because it searches the whole scene); the LINQ iterator, the closure (the lambda captures this to read transform and range) and its delegate; the new list from ToList; the new string built for the status text; and the string from e.tag on every enemy.

Turret.cs, allocation-free once warmed upC#
readonly List<Enemy> near = new List<Enemy>();   // reused every frame
int shownCount = -1;

void Update()
{
    near.Clear();
    float rangeSqr = range * range;
    Vector3 here = transform.position;
    foreach (Enemy e in Enemy.Active)            // a list enemies add themselves to (see below)
    {
        if ((e.transform.position - here).sqrMagnitude < rangeSqr)
        {
            near.Add(e);
        }
    }

    if (near.Count != shownCount)                // rebuild the text only when it changes
    {
        shownCount = near.Count;
        status.text = "Targets: " + shownCount;
    }

    foreach (Enemy e in near)
    {
        if (e.CompareTag("Boss")) Debug.Log("Boss in range!");
    }
}

The list is created once and cleared, a plain loop replaces LINQ, the text is rebuilt only when the count changes, and CompareTag avoids the string. Comparing squared distances also skips a square root per enemy. Enemy.Active is a static List<Enemy> that each enemy adds itself to in OnEnable and removes itself from in OnDisable, which replaces the scene search; T05 · Scripting in Unity: Lifecycle, Serialization, Coroutines & Awaitable explains why those two methods are the place to do it. (A Debug.Log in a frame loop allocates too; it's fine for debugging, but remove it before profiling.)

8 · Ferries first

The harbour wants ferries to skip the queue: a waiting ferry always docks before any waiting trader or fishing boat, and within each group the order is still first come, first served. Change Port to do this without changing Vessel, its subclasses or Program's event handlers. Then add an arrival to the schedule that shows the rule working.

Solution
Changes to Port.csC#
// Replace the single queue with two
readonly Queue<Vessel> ferries = new Queue<Vessel>();
readonly Queue<Vessel> others = new Queue<Vessel>();

public int Waiting => ferries.Count + others.Count;

// In Arrive, instead of waiting.Enqueue(vessel):
(vessel is Ferry ? ferries : others).Enqueue(vessel);

// In Tick, the docking loop becomes:
while (moored.Count < berths && Waiting > 0)
{
    Vessel next = ferries.Count > 0 ? ferries.Dequeue() : others.Dequeue();
    moored.Add(new Mooring { Vessel = next, TicksLeft = next.StayTicks });
    Docked?.Invoke(next);
}

Two queues keep "first come, first served" inside each group, and the docking loop always looks at the ferry queue first. Nothing outside Port changes, because the waiting line was private. To see it work, add a ferry that arrives on tick 2 after Bluefin, such as new Ferry("Puffin", 16, 20). On tick 3, when two berths free up, Puffin docks before Bluefin; with the original port, Bluefin docks first. If priorities multiply (ferries, then mail boats, then everyone else), a SortedDictionary<int, Queue<Vessel>> keyed by priority generalises the idea.

Check yourself#

A base class method is public virtual void Move(). A derived class declares public void Move() without override. What happens when you call Move() through a variable of the base type holding a derived object?

  1. The derived version runs
  2. A compile error
  3. The base version runs, and the compiler warned you (CS0114)
  4. Both versions run

Without override the derived method hides the base one instead of overriding it, so the variable's type decides which runs. That's almost never what you want; add override.

Why does new Creature("x", 5) fail when Creature is abstract?

  1. Its constructor is protected
  2. Abstract classes can't be instantiated; only concrete derived classes can
  3. It needs override
  4. Abstract classes have no constructors

An abstract class exists to be derived from (error CS0144). It can still have constructors, which derived classes call with : base(…).

A barrel, an enemy and a door can all be damaged but have nothing else in common. What should they share?

  1. A base class Damageable
  2. A static Damage method
  3. Nothing; use is checks for each class
  4. An interface such as IDamageable

An interface describes an ability that unrelated classes can each implement, and a class can implement several. A base class would force them into one family they don't belong to.

What does the constraint where T : IComparable<T> make possible inside a generic method?

  1. Calling CompareTo on values of type T
  2. Creating new T()
  3. Assigning null to a T
  4. Nothing; constraints are only documentation

A constraint tells the compiler what every possible T can do, so it allows those members. new T() needs the new() constraint; null needs class.

You need to check thousands of times a frame whether a tile has been visited. Which collection fits best?

  1. List<Vector2Int> with Contains
  2. Queue<Vector2Int>
  3. HashSet<Vector2Int>
  4. An array searched with a loop

A hash set answers "is it in there?" in about the same time however large it gets. A list or array checks items one by one, which grows with the number of tiles.

What does stock["tar"] do if the dictionary has no key "tar"?

  1. Returns 0
  2. Throws a KeyNotFoundException
  3. Adds the key with value 0
  4. Returns null

Reading a missing key throws. TryGetValue is the safe way to read a key that may be missing. (Writing with the indexer, by contrast, adds the key.)

Three lambdas are created in for (int i = 0; i < 3; i++), each returning i. After the loop, what do they return?

  1. 0, 1, 2
  2. 0, 0, 0
  3. A compile error
  4. 3, 3, 3

They all capture the same loop variable, which is 3 when the loop ends. Copy i into a local inside the loop to give each lambda its own.

What does the event keyword add to a public delegate field?

  1. Outside code can only subscribe and unsubscribe; only the declaring class can raise it
  2. It runs subscribers on another thread
  3. It unsubscribes destroyed objects automatically
  4. It makes the delegate non-nullable

That restriction is the whole difference (raising it from outside is error CS0070). You still need ?.Invoke, and you still have to unsubscribe yourself.

var q = list.Where(x => x > 10); and then q.Count() is called twice. How many times is the filter run over the list?

  1. Zero: Where already ran
  2. Once: LINQ caches results
  3. Twice: the query runs each time it's enumerated
  4. Three times: once when created and once per Count

LINQ queries are deferred: creating one runs nothing, and each enumeration runs it again from the start. Call ToList() once if you need the results more than once.

Why is "zero allocations per frame" a stronger goal in Unity than in a typical desktop .NET app?

  1. Unity has no garbage collector
  2. Unity's garbage collector isn't generational, so frequent small temporary objects are relatively costly, and collection time shows up as frame hitches
  3. Allocation is forbidden in Update
  4. Structs can't be used in Unity

Unity uses the non-generational Boehm collector (incremental by default). Allocating in setup code is fine; allocating every frame feeds the collector continuously.

Cheat sheet#

Inheritance

  • class Crab : Creature
  • : base(name, 12) calls the base constructor
  • virtual may be overridden; abstract must be
  • override replaces; base.M() calls the original
  • sealed: no further deriving
  • protected: class and subclasses only
  • if (c is Crab crab) to test and convert

Interfaces and composition

  • interface IDamageable { … }
  • Members have no body: void TakeDamage(int a);
  • class Lamp : IDamageable, IRepairable
  • "is a" → inheritance; "can do" → interface
  • "has a" → a field (composition)
  • Unity: TryGetComponent(out IDamageable d)

Generics

  • T Largest<T>(…) where T : IComparable<T>
  • class Pool<T> where T : class, new()
  • Constraints: class, struct, new(), a base type, an interface
  • default: 0, false, null or a zeroed struct

Collections

  • List<T>: the default; RemoveAll, Find
  • Dictionary<K,V>: TryGetValue, d[k] = v
  • HashSet<T>: Add returns false if present
  • Queue: Enqueue / Dequeue (first in, first out)
  • Stack: Push / Pop (last in, first out)
  • Expose IReadOnlyList<T>

Delegates and events

  • Action<int>: takes an int, returns nothing
  • Func<int, bool>: last type is the return
  • Lambda: (a, b) => a + b
  • public event Action<int> Changed;
  • Raise: Changed?.Invoke(value);
  • Unity: += in OnEnable, -= in OnDisable

LINQ and iterators

  • using System.Linq;
  • Where, Select, OrderBy, GroupBy
  • First throws; FirstOrDefault doesn't
  • Deferred: runs when enumerated, every time
  • ToList() to run once and keep
  • yield return x; / yield break;

Memory

  • Classes, arrays, strings, boxes: heap
  • Unity GC: Boehm, not generational, incremental
  • Per frame: no new, LINQ, string building
  • CompareTag, cached lists, pools
  • using (var r = …) { } calls Dispose
  • Measure with the Profiler's GC Alloc

Small tools

  • Extension: static bool IsDeep(this int m)
  • Tuple: (int lo, int hi) Range(…)
  • var (lo, hi) = Range(xs);
  • int?, .HasValue, ?? fallback
  • [Flags] enum: a | b, (v & f) != 0

Glossary#

Inheritance
A class deriving from a base class and receiving its members, written class Derived : Base.
Base class
The class another class derives from. Also called a parent class or superclass.
Derived class
A class built on a base class. Also called a child class or subclass.
Virtual method
A method with a default body that derived classes may replace with override.
Abstract
A class that can't be instantiated, or a member with no body that derived classes must implement.
Polymorphism
One call doing the right thing for each object's actual type, through a variable of a base or interface type.
Interface
A named set of members that a class promises to implement; a class can implement any number of them.
Composition
Building an object from other objects it holds ("has a") instead of inheriting behaviour ("is a"). Unity's components work this way.
Decoupled
Code that depends on as little as possible, such as an interface or an event, so the other side can change freely.
Generic type or method
Code with a type parameter, such as List<T>, that works for any type filled in for T.
Constraint
A where clause that limits which types a type parameter accepts, and allows the members those types have.
Hash code
A number computed from a value that tells a dictionary or hash set roughly where to find it.
O(1), O(n)
Shorthand for how an operation's cost grows with the number of items: constant, or in proportion to the count.
Delegate
A type whose values are methods, so methods can be stored, passed and called later. Action and Func are ready-made delegates.
Lambda
An inline, unnamed method written with =>, such as x => x * 2.
Closure
A lambda together with the outer variables it captured; it shares those variables rather than copying them.
Event
A delegate member that outside code can only subscribe to and unsubscribe from; only its class can raise it.
Publisher and subscriber
The object that raises an event, and the objects that registered methods to be called when it does.
LINQ
Language Integrated Query: extension methods such as Where and Select for querying any sequence.
Deferred execution
A query running only when its results are enumerated, and again each time they are.
Iterator
A method using yield return that produces a sequence one item at a time, pausing between items.
Extension method
A static method with this on its first parameter, callable as if it were a member of that type.
Stack (memory)
The fast memory area holding each method call's local variables, released when the method returns.
Managed heap
The memory area for objects created with new, arrays and strings, reclaimed by the garbage collector.
Garbage collector
The runtime system that finds heap objects nothing refers to any more and reclaims their memory.
Boxing
Copying a value type into a heap object so it can be stored as object or an interface.
Object pool
A store of reusable objects handed out and returned, instead of creating and destroying them repeatedly.