在 F# 中重写 ToString 时避免堆栈溢出
Avoiding stack overflow when overriding ToString in F#
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()
DiscrUnion.ToString() 的打印方式类似于 "FSI_0003+DiscrUnion",但是对于 DiscrUnionWithToString.ToString() 我得到了实际属性:文字"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()
甚至不要尝试实例化 PocoTypeWithToString。 WhosebugException.
我了解对于 POCO 类型,尝试使用“%A”格式化指令会导致调用 ToString,因此当 ToString 本身包含此类指令时,它将失败。但是 ToString 覆盖的正确方法是什么?我应该只注意 C# 类型(可区分的联合和记录似乎工作正常),还是还有其他需要注意的事情?
简单的答案 - 不要在任何地方对 ToString
使用这样的笼统实施。
格式字符串 %A
启动了一个相当毛茸茸的基于反射的打印机,如果它没有以特殊方式处理,可能会回退到 ToString
。请参阅 anyToStringForPrintf
here.
的代码
更简洁的解决方案是在记录对象时使用单个 sprintf %A
,而不是让所有 DU 实现样板文件 ToString
,但您说这不是一个选项.
对于常规 .NET class(与特定于 F# 的记录或联合相反),不要使用 this
- 而是使用一些有意义的标识符或输出所有成员,或者做任何你喜欢的事情。只是不要开始 ToStrings
.
的无限循环
之所以出现WhosebugException,是因为打印机使用GetValueInfoOfObject
格式化。如您所见,如果对象是 F# 对象,它有如何处理它们的特殊情况(元组、函数、联合、异常、记录)。
但是,如果不是其中一种情况,它将成为 ObjectValue(obj)
。稍后,在 reprL
中,我们有一些特殊情况来处理 ObjectValue
,例如字符串、数组、map/set、ienumerable,然后在最后如果失败,它只会使它是 Leaf
.
类型的基本布局 (let basicL = LayoutOps.objL obj
)
很久以后,Leaf
被格式化为 leafformatter
。 leafformatter
可以处理基元,但是当它处理像您的 POCO 这样的复杂对象时,它会处理 let text = obj.ToString()
,这会导致无限循环和 Whosebug 异常。
解决方案是不在 POCO 上使用 %A
。
好消息是 F# 的下一版本 可能 有一个默认 records/unions 的 ToString
实现,实际上是 override this.ToString() = sprintf "%A" this
。它的实现在此处部分完成:https://github.com/Microsoft/visualfsharp/pull/1589。它可能会解决您一开始遇到的问题。
DiscrUnionWithToString.ToString() I get the actual properties: Text "text"
当我遇到那个问题时,我想到了这个
type DiscrUnionWithToString =
| Text of string
override text.ToString() =
match text with
| Text text -> text
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()
DiscrUnion.ToString() 的打印方式类似于 "FSI_0003+DiscrUnion",但是对于 DiscrUnionWithToString.ToString() 我得到了实际属性:文字"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()
甚至不要尝试实例化 PocoTypeWithToString。 WhosebugException.
我了解对于 POCO 类型,尝试使用“%A”格式化指令会导致调用 ToString,因此当 ToString 本身包含此类指令时,它将失败。但是 ToString 覆盖的正确方法是什么?我应该只注意 C# 类型(可区分的联合和记录似乎工作正常),还是还有其他需要注意的事情?
简单的答案 - 不要在任何地方对 ToString
使用这样的笼统实施。
格式字符串 %A
启动了一个相当毛茸茸的基于反射的打印机,如果它没有以特殊方式处理,可能会回退到 ToString
。请参阅 anyToStringForPrintf
here.
更简洁的解决方案是在记录对象时使用单个 sprintf %A
,而不是让所有 DU 实现样板文件 ToString
,但您说这不是一个选项.
对于常规 .NET class(与特定于 F# 的记录或联合相反),不要使用 this
- 而是使用一些有意义的标识符或输出所有成员,或者做任何你喜欢的事情。只是不要开始 ToStrings
.
之所以出现WhosebugException,是因为打印机使用GetValueInfoOfObject
格式化。如您所见,如果对象是 F# 对象,它有如何处理它们的特殊情况(元组、函数、联合、异常、记录)。
但是,如果不是其中一种情况,它将成为 ObjectValue(obj)
。稍后,在 reprL
中,我们有一些特殊情况来处理 ObjectValue
,例如字符串、数组、map/set、ienumerable,然后在最后如果失败,它只会使它是 Leaf
.
let basicL = LayoutOps.objL obj
)
很久以后,Leaf
被格式化为 leafformatter
。 leafformatter
可以处理基元,但是当它处理像您的 POCO 这样的复杂对象时,它会处理 let text = obj.ToString()
,这会导致无限循环和 Whosebug 异常。
解决方案是不在 POCO 上使用 %A
。
好消息是 F# 的下一版本 可能 有一个默认 records/unions 的 ToString
实现,实际上是 override this.ToString() = sprintf "%A" this
。它的实现在此处部分完成:https://github.com/Microsoft/visualfsharp/pull/1589。它可能会解决您一开始遇到的问题。
DiscrUnionWithToString.ToString() I get the actual properties: Text "text"
当我遇到那个问题时,我想到了这个
type DiscrUnionWithToString =
| Text of string
override text.ToString() =
match text with
| Text text -> text