在F#中覆盖ToString时避免堆栈溢出

Vag*_*lov 4 stack-overflow f# overriding tostring

F#有一个非常强大的格式化指令"%A",因为触发格式化程序来扩展类型并列出单个成员.在我们的应用程序的某些地方,使用ToString方法记录数据(有一些技术原因),然后对于有区别的联合类型,它只是一个记录的类型名称.太糟糕了,所以我们开始为某些类型重写ToString方法.

举个例子:

open System

type DiscrUnion =
    | Text of string

let t1 = DiscrUnion.Text "text"
sprintf "%A" t1
sprintf "%s" <| t1.ToString()


type DiscrUnionWithToString =
    | Text of string
    override this.ToString() = sprintf "%A" this

let t2 = DiscrUnionWithToString.Text "text"
sprintf "%A" t2
sprintf "%s" <| t2.ToString()
Run Code Online (Sandbox Code Playgroud)

DiscrUnion.ToString()的打印方式类似于"FSI_0003 + DiscrUnion",但是对于DiscrUnionWithToString.ToString(),我得到了实际的属性:Text"text".

到现在为止还挺好.但是,对于CLR类型,这种覆盖会导致灾难性的结果:堆栈溢出!这是一个例子:

type PocoType() =
    member val Text : string = null with get, set

let t3 = PocoType()
t3.Text <- "text"
sprintf "%A" t3
sprintf "%s" <| t3.ToString()

type PocoTypeWithToString() =
    member val Text : string = null with get, set
    override this.ToString() = sprintf "%A" this

let t4 = PocoTypeWithToString()
t4.Text <- "text"
sprintf "%A" t4
sprintf "%s" <| t4.ToString()
Run Code Online (Sandbox Code Playgroud)

甚至不要尝试实例化PocoTypeWithToString.StackOverflowException.

我理解,对于POCO类型,尝试使用"%A"格式化指令会导致ToString调用,因此当ToString本身包含此类指令时,它将失败.但ToString覆盖的正确方法是什么?我应该只注意C#类型(有区别的工会和记录似乎工作正常),还是有其他事情需要注意?

Rin*_*gil 6

发生StackOverflowException的原因是打印机使用GetValueInfoOfObject格式化.如您所见,如果对象是F#对象,则它具有如何处理它们的特殊情况(元组,函数,联合,异常,记录).

但是,如果它不是这些情况之一,它将成为一个ObjectValue(obj).稍后,在reprL我们有一些特殊情况下处理ObjectValue诸如字符串,数组,映射/集合,ienumerable之类的s,然后在最后如果失败,它将使它成为类型的基本布局(let basicL = LayoutOps.objL obj)Leaf.

很久以后,Leaf使用格式化leafformatter.leafformatter可以处理原语,但是当它处理一个复杂的对象(如POCO)时,它会let text = obj.ToString()导致无限循环和StackOverflow异常.

解决方案是不要%A在POCO上使用.

好消息是,F#的下一个版本可能ToString有效地实现了记录/联合的默认实现override this.ToString() = sprintf "%A" this.它的实现部分完成:https://github.com/Microsoft/visualfsharp/pull/1589.它可以解决您必须开始的问题.