Reference

Every member of Observable, ObservableList and ObservableDictionary, and what each one notifies.

Updated 2026-09-08

Everything lives in Udonite.Observables.

Observable<T>

new Observable<int>()        // default value, nobody notified
new Observable<int>(7)       // seeded, nobody notified
Member Notifies
T Value { get; set; } on set, only when the new value differs
Subscribe(Action<T> handler) no
Subscribe(Action<T> handler, bool notifyNow) notifyNow calls this handler only
Unsubscribe(Action<T> handler) no; removing one that was never subscribed does nothing
UnsubscribeAll() no
SetSilently(T value) no
Publish() yes, every subscriber, changed or not
bool CascadeStalled { get; } no
ToString() no; the value, or "" when there is none

Equality is object.Equals. Two equal strings are not a change; null is a value like any other, so null to null is silent and null to a value is not.

Subscribing twice subscribes once. The usual way to end up there is a Start that runs again, and being called twice per change is a bug every time.

A subscriber may live anywhere. score.Subscribe(display.Show) reaches a handler on another behaviour, score.Subscribe(OnChanged) one on this behaviour, and a class that is not a behaviour subscribes one of its own methods from inside itself. One observable can hold a mixture of all three.

The exception is handing over another object's method when that object is not a behaviour — score.Subscribe(tally.Add) where tally is an ordinary class. Udon has nowhere to put the receiver, and the compiler refuses it. Give the class a method that subscribes itself:

public class Tally
{
    public void Watch(Observable<int> score) { score.Subscribe(Add); }

    private void Add(int value) { ... }
}

There is no subscriber count, and Unsubscribe reports nothing. Removal matches by content, so a handler can be removed with a delegate built fresh at the call site rather than the exact value that was added, but Udon registers no way to inspect a list of subscribers. Code that needs to know what it subscribed should keep track of that itself.

CascadeStalled reports a publish that restarted sixteen times without settling. Reaching it takes a subscriber that writes back to the same observable, which the compiler refuses, so in a world it stays false.

ObservableList<T>

Subscribers take Action<ObservableList<T>> and are handed the list itself.

Member Notifies LastChange
T this[int] { get; set; } on set, only when different Replaced
Add(T) yes Added
AddRange(T[]) once for the whole range; nothing for an empty or null one None
Insert(int, T) yes Added
bool Remove(T) only if it was there Removed
RemoveAt(int) yes Removed
Clear() only if it held anything Cleared
AddSilently(T) no
Contains, IndexOf, ToArray, Count no

Inside a handler, LastChange, LastIndex and LastItem describe what happened, so a subscriber can spawn one object rather than rebuild everything:

private void OnNamesChanged(ObservableList<string> list)
{
    if (list.LastChange == CollectionChange.Added)
        Spawn(list.LastItem);
    else
        RedrawEverything(list);
}

They describe the notification in progress and are only meaningful inside a handler.

ObservableDictionary<TKey, TValue>

Subscribers take Action<ObservableDictionary<TKey, TValue>>.

Member Notifies LastChange
TValue this[TKey] { get; } no
Set(TKey, TValue) only when the value differs Added for a new key, Replaced for a known one
bool Remove(TKey) only if it was there Removed
Clear() only if it held anything Cleared
SetSilently(TKey, TValue) no
ContainsKey, TryGetValue, Count no

LastKey and LastValue name what changed.

There is no indexer setter, on purpose. A dictionary in Udon is a DataDictionary, and reading a key that is not there faults rather than returning a default, so a read-before-write hidden inside a setter would be a trap. Set keeps the read explicit: ask ContainsKey first, or use TryGetValue.

CollectionChange

None, Added, Removed, Replaced, Cleared.

None is both "nothing has happened yet" and "a bulk change with no single item": AddRange, or a Subscribe(handler, true) that is showing the collection rather than reporting a change.

If a handler throws

An exception in Udon halts the behaviour for the rest of the session, and there is no catching one subscriber's and carrying on to the next. A handler that throws takes down the rest of the notification and everything that behaviour would have done afterwards, so keep handlers to reading the value and updating something.

Something went wrong Reload