egametang/ET · error · NotSupportedException

检测到循环引用,类型: {runtimeType.FullName}

Error message

检测到循环引用,类型: {runtimeType.FullName}

What it means

Thrown by EmitterContext.Enter when the same object instance is entered twice along the current emission path. The emitter tracks visited references to detect cycles in the object graph; a re-entry means the graph loops back on itself, which would otherwise recurse infinitely while generating C# literals.

Source

Thrown at Packages/cn.etetet.config/Scripts/Model/Share/CSharpObjectCodeEmitter.cs:900

            return false;
        }

        private string Indent(int level)
        {
            return new string(' ', level * 4);
        }

        [EnableClass]
        private sealed class EmitterContext
        {
            private readonly HashSet<object> visited = new(new ObjectReferenceComparer());

            public void Enter(object value, Type runtimeType)
            {
                if (!this.visited.Add(value))
                {
                    throw new NotSupportedException($"检测到循环引用,类型: {runtimeType.FullName}");
                }
            }

            public void Exit(object value)
            {
                this.visited.Remove(value);
            }
        }

        [EnableClass]
        private sealed class ObjectReferenceComparer : IEqualityComparer<object>
        {
            bool IEqualityComparer<object>.Equals(object x, object y)
            {
                return ReferenceEquals(x, y);
            }

            int IEqualityComparer<object>.GetHashCode(object obj)

View on GitHub (pinned to 5cab01f7a8)

Solutions

  1. Break the cycle before emitting: set parent/back-references to null or replace them with id references.
  2. Project the cyclic graph into an acyclic DTO (ids instead of object pointers) for export.
  3. Mark back-reference fields as non-serialized so the emitter skips them.

Example fix

// before
public class TreeNode
{
    public List<TreeNode> Children = new();
    public TreeNode Parent; // cycle: child -> parent -> child
}
// after
public class TreeNode
{
    public List<TreeNode> Children = new();
    [NonSerialized] public TreeNode Parent; // skip during export
}
Defensive patterns

Strategy: validation

Validate before calling

// detect cycles in the graph before emitting (BFS reference check)
static bool HasCycle(object root)
{
    var visited = new HashSet<object>(new ObjectReferenceComparer());
    var stack = new Stack<object>();
    stack.Push(root);
    while (stack.Count > 0)
    {
        var cur = stack.Pop();
        if (cur == null) continue;
        if (!visited.Add(cur)) return true;
        foreach (var f in cur.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public))
            if (f.GetValue(cur) is object child && !f.FieldType.IsValueType) stack.Push(child);
    }
    return false;
}

Prevention

When it happens

Trigger: An object graph with a reference cycle: A references B and B references A; a parent/child collection where a child points back to its parent; a self-referential field (node.Next = node).

Common situations: Runtime data structures that legitimately cycle (linked lists, trees with parent pointers, bidirectional relationships) being passed to an exporter that only supports trees/DAGs.

Related errors


AI-assisted analysis of egametang/ET@5cab01f7a8 (2026-08-13). Data as JSON: /api/errors/4bf4a9d124eb241d. Report an issue: GitHub.