GDKB

Delegates

Intermediate25 Minutes readUpdated July 21, 20263 related concepts

A delegate is a type that holds a reference to a method instead of a value — it lets you pass a function around like any other piece of data: store it in a variable, pass it as an argument, or call it later without the caller needing to know which specific method will run. Delegates are what make it possible to say "call whatever function was configured here" rather than hard-coding one specific function call.

On this page

Delegates

Summary

A delegate is a type that holds a reference to a method instead of a value — it lets you pass a function around like any other piece of data: store it in a variable, pass it as an argument, or call it later without the caller needing to know which specific method will run. Delegates are what make it possible to say "call whatever function was configured here" rather than hard-coding one specific function call.

Why This Matters

Without delegates, code that needs to run "some behavior decided elsewhere" has to know the exact method name up front, or resort to large switch statements that grow every time a new behavior is added. A damage system that needs to notify "whoever cares" when health hits zero would otherwise have to import and directly call every possible listener by name, coupling the damage system to UI code, audio code, and achievement code all at once. Delegates let the damage system hold a reference to "call this later" without knowing or caring what it points to.

Mental Model

A delegate is a phone number, not the person behind it. Storing a phone number in your contacts doesn't require knowing who will answer, what they'll say, or even that they'll still be reachable at that number tomorrow — it's a reference you can call later. A method reference stored in a delegate works the same way: the delegate variable holds "how to reach this behavior," and invoking the delegate is like dialing the number, without the caller needing to know which method actually answers.

Core Explanation

  • A delegate type defines a method signature, not an implementation. delegate void DamageHandler(int amount) describes "any method taking an int and returning void" — any method matching that shape can be assigned to a variable of this delegate type.
  • Assignment binds a specific method at runtime. DamageHandler handler = TakeDamage; stores a reference to the TakeDamage method itself, not the result of calling it — invoking handler(10) later runs TakeDamage(10).
  • Delegates can hold multiple methods (multicast). handler += PlayHurtSound; appends a second method to the same delegate — invoking it now calls every attached method in order, which is the mechanism events (see [[CON-0024]]) build on top of.
  • Action and Func are built-in generic delegate types. Rather than declaring a new named delegate for every signature, Action<int> (void, one int parameter) and Func<int, bool> (one int parameter, returns bool) cover the vast majority of cases without a custom declaration.

Syntax / Theory

C# (Unity)

csharp
public delegate void DamageHandler(int amount);

public class Health : MonoBehaviour
{
    public DamageHandler OnDamaged; // Multicast — can hold 0+ methods.

    public void TakeDamage(int amount)
    {
        currentHealth -= amount;
        OnDamaged?.Invoke(amount); // Null-conditional: safe if nothing subscribed.
    }
}
csharp
// Built-in generic delegates avoid declaring a custom delegate type.
Action<int> logDamage = amount => Debug.Log($"Took {amount} damage");
Func<int, bool> isLethal = amount => amount >= currentHealth;

C++ (Unreal — delegates)

cpp
DECLARE_DELEGATE_OneParam(FDamageHandler, int32);

class AHealthComponent
{
public:
    FDamageHandler OnDamaged;

    void TakeDamage(int32 Amount)
    {
        CurrentHealth -= Amount;
        OnDamaged.ExecuteIfBound(Amount);
    }
};

Unreal Blueprint

Blueprint exposes delegate-like behavior through Event Dispatchers — bindable slots that other Blueprints can attach a custom event to, matching the "store a reference, invoke it later" model without writing C++.

Practical Example

An input system needs to trigger different actions depending on configuration, without hard-coding which action runs.

csharp
public class InputBinding
{
    public string Key;
    public Action OnPressed;
}

var bindings = new List<InputBinding>
{
    new InputBinding { Key = "Space", OnPressed = Jump },
    new InputBinding { Key = "E",     OnPressed = Interact },
};

void Update()
{
    foreach (var binding in bindings)
        if (Input.GetKeyDown(binding.Key))
            binding.OnPressed?.Invoke();
}

Adding a new bindable action means adding one entry to bindings — the Update loop never needs to know the names Jump or Interact, or grow a new if branch for each one.

Common Mistakes

  • Forgetting the null check before invoking. OnDamaged(amount) throws a NullReferenceException if nothing has subscribed yet; OnDamaged?.Invoke(amount) is the safe form and should be the default.
  • Never unsubscribing a delegate. +=-ing a method without a matching -= when the subscriber is destroyed keeps a live reference to the destroyed object, which either throws when eventually invoked or, worse, silently keeps the object alive longer than intended (a memory leak).
  • Using = instead of += when a second subscriber is intended. OnDamaged = PlayHurtSound; replaces any previously assigned method instead of adding to it — the earlier subscriber silently stops being called.
  • Reaching for a delegate where a simple direct method call would do. Delegates add a layer of indirection that's worth it when the caller genuinely doesn't know the behavior in advance — using one to call a single, always-known method just makes the code harder to follow.

Game Development Example

A quest system needs to react to many different kinds of player actions (kill an enemy, pick up an item, reach a location) without the core quest tracker knowing about every specific game system in advance.

csharp
public class QuestObjective
{
    public Func<bool> IsComplete;
    public Action OnCompleted;

    public void CheckCompletion()
    {
        if (IsComplete())
            OnCompleted?.Invoke();
    }
}

var killObjective = new QuestObjective
{
    IsComplete = () => enemiesKilled >= 5,
    OnCompleted = () => questLog.MarkComplete("Clear the Camp"),
};

Best Practices

  • Prefer the built-in Action/Func generic delegates over declaring a custom delegate type unless the signature is reused widely enough to benefit from a named type.
  • Always pair += with a corresponding -= in the object's cleanup path (OnDestroy, OnDisable) to avoid leaking references.
  • Use ?.Invoke() for every multicast delegate call — never assume at least one subscriber exists.
  • Reserve delegates for genuine "caller doesn't know the behavior in advance" situations; a direct method call is simpler and should be preferred when the target is always known.

Exercises

  1. Declare an Action<int> field on a ScoreManager class called OnScoreChanged, invoke it whenever the score changes, and subscribe a method that logs the new score to the console.
  2. Take the InputBinding example and add a second binding whose OnPressed is a Func<bool>-style condition check combined with an action — explain why Action alone isn't enough if the binding should only fire under a condition.
  3. You're choosing between a delegate-based notification system and having each interested class directly call a shared EventBus singleton. Argue for one approach for a mid-sized single-player game, and name one situation where the other approach would be justified instead.
  4. Research how a lambda expression (amount => Debug.Log(amount)) captures variables from its enclosing scope (a "closure"), and explain why a delegate holding a lambda that captures a local MonoBehaviour reference can accidentally keep that object alive.
  • Events — events are built directly on top of delegates, adding restrictions (only the declaring class can invoke) that make multicast delegates safe to expose publicly.
  • Functions — a delegate is fundamentally "a function treated as a value"; understanding functions as units of behavior is the prerequisite for understanding a reference to one.
  • Interfaces — both delegates and interfaces let calling code depend on a shape rather than a concrete implementation; a delegate is the single-method version of that idea.

Further Reading

Related Concepts