F#调试器因`sprintf "%A"`栈溢出崩溃,重写ToString无效求解决
解决F#判别联合类型调试时栈溢出问题
问题背景
我定义了如下F#判别联合类型:
type Value = | Float of float | Int of int | String of string | Function of (ContextStack -> Value -> Value) | Action of (unit -> Value) | Constructor of string * int * Value | Type of string * Context | Object of Value * List<Value> | Ref of Context * (Context -> Value) * (Context -> Value -> Context) | Unrecognizable of Node | Closure of Value * ContextStack * bool | Application of Value * Value
其中Context本质是System.Collections.Generic.Dictionary<string, Value>,ContextStack为Context list。由于存在循环引用(比如Value实例引用包含自身的Context字典),调试器尝试用sprintf "%A"生成对象字符串表示时,会触发无限递归导致栈溢出。
我尝试重写Value的ToString方法,但无效。更新后发现:调试器对Value对象本身会使用ToString,但当处理包含Value的字典时,会对字典内的Value调用sprintf "%A"而非ToString,依然触发错误。
环境信息:
- 操作系统:Windows 11
- IDE:Rider 2024.3
- .NET目标框架:netcoreapp 3.1(无法更换)
解决方案
1. 给Value和Context添加DebuggerDisplay特性
直接告诉调试器使用自定义的显示文本,避免自动递归遍历所有字段:
// 先给Context类型添加DebuggerDisplay,只显示条目数量 [<DebuggerDisplay("Context[{Count}]")>] type Context = System.Collections.Generic.Dictionary<string, Value> // 给Value类型添加DebuggerDisplay,自定义各分支的显示内容 [<DebuggerDisplay("{DebuggerDisplay}")>] type Value = | Float of float | Int of int | String of string | Function of (ContextStack -> Value -> Value) | Action of (unit -> Value) | Constructor of string * int * Value | Type of string * Context | Object of Value * List<Value> | Ref of Context * (Context -> Value) * (Context -> Value -> Context) | Unrecognizable of Node | Closure of Value * ContextStack * bool | Application of Value * Value with // 保留ToString作为 fallback override this.ToString() = "Value" // 自定义调试器显示文本,避免递归循环 member this.DebuggerDisplay = match this with | Float f -> $"Float({f})" | Int i -> $"Int({i})" | String s -> $"String({s})" | Function _ -> "Function(...)" | Action _ -> "Action(...)" | Constructor(name, arity, v) -> $"Constructor({name}, {arity}, {v.DebuggerDisplay})" | Type(name, ctx) -> $"Type({name}, {ctx.DebuggerDisplay})" | Object(ctor, args) -> $"Object({ctor.DebuggerDisplay}, [{args.Length} args])" | Ref(ctx, _, _) -> $"Ref({ctx.DebuggerDisplay}, ...)" | Unrecognizable node -> $"Unrecognizable({node.GetType().Name})" | Closure(v, stack, isTail) -> $"Closure({v.DebuggerDisplay}, Stack[{stack.Length}], {isTail})" | Application(func, arg) -> $"Application({func.DebuggerDisplay}, {arg.DebuggerDisplay})"
这个方法会让调试器在显示对象时直接使用DebuggerDisplay属性指定的文本,不会递归遍历可能引发循环的字段(比如Context只显示条目数,不展开所有Value)。
2. 自定义格式化器处理sprintf "%A"
如果需要在代码中使用sprintf "%A"而不触发栈溢出,可以实现自定义格式化器,让%A格式调用我们的逻辑:
type ValueFormatter() = interface IFormatProvider with member this.GetFormat(formatType: Type) = if formatType = typeof<ICustomFormatter> then this :> obj else null interface ICustomFormatter with member this.Format(format: string, arg: obj, formatProvider: IFormatProvider) = match arg with | :? Value as v -> // 处理%A格式,返回自定义显示文本 match format with | "A" | null -> v.DebuggerDisplay | _ -> v.ToString() | :? Context as ctx -> match format with | "A" | null -> $"Context[{ctx.Count}]" | _ -> ctx.ToString() | _ -> // 其他类型使用默认格式化 if arg <> null then string arg else ""
使用时,替换默认的sprintf调用:
let fmt = ValueFormatter() let displayText = String.Format(fmt, "{0:A}", myValue)
3. 调整Rider调试器设置
在Rider中限制调试器的对象展开深度,避免自动递归遍历循环引用:
- 打开
File > Settings > Build, Execution, Deployment > Debugger > Data Views - 在
General选项卡中,设置Maximum expansion depth为较小值(比如2或3) - 勾选
Do not evaluate properties automatically,避免调试器自动计算所有属性
内容的提问来源于stack exchange,提问作者TimurTimergalin
相关产品推荐
相关产品推荐

