在F#中使用StringBuilder是正确的吗?

时间:2013-09-03 15:04:34

标签: f# immutability stringbuilder

StringBuiler是一个可变对象,F#鼓励尽可能多地使用不变性。所以应该使用转换而不是变异。在F#中构建字符串时,这是否适用于StringBuilder?有一个F#immutable替代它吗?如果是这样,这种替代方案是否有效?

A snippet

2 个答案:

答案 0 :(得分:40)

我认为在F#中使用StringBuilder完全没问题 - sb.Append返回StringBuilder的当前实例的事实意味着它可以很容易地与fold一起使用功能。即使这仍然是必要的(对象是变异的),当您不公开对StringBuilder的引用时,它与函数样式相当吻合。

但同样,您可以构建一个字符串列表并使用String.concat连接它们 - 这几乎与使用StringBuilder一样有效(它更慢,但不是很多 - 并且它明显更快而不是使用+

连接字符串

因此,列表为您提供了类似的性能,但它们是不可变的(并且兼容并发等) - 如果您在算法上构建字符串,它们将非常适合,因为您只需将字符串附加到列表的前面 - 这是对列表的非常有效的操作(然后反转字符串)。此外,使用列表表达式为您提供了非常方便的语法:

// Concatenating strings using + (2.3 seconds)
let s1 = [ for i in 0 .. 25000 -> "Hello " ] |> Seq.reduce (+)
s1.Length

// Creating immutable list and using String.concat (5 ms)
let s2 = [ for i in 0 .. 25000 -> "Hello " ] |> String.concat ""
s2.Length

// Creating a lazy sequence and concatenating using StringBuilder & fold (5 ms)
let s3 = 
  seq { for i in 0 .. 25000 -> "Hello " }
  |> Seq.fold(fun (sb:System.Text.StringBuilder) s -> 
      sb.Append(s)) (new System.Text.StringBuilder())
  |> fun x -> x.ToString()
s3.Length

// Imperative solution using StringBuilder and for loop (1 ms)
let s4 = 
  ( let sb = new System.Text.StringBuilder()
    for i in 0 .. 25000 do sb.Append("Hello ") |> ignore
    sb.ToString() )
s4.Length

在F#Interactive中使用#time在我的,相当快的工作机器上测量时间 - 很可能它在发布版本中会更快,但我认为它们具有相当的代表性。

答案 1 :(得分:8)

如果您需要高性能sting连接,那么字符串构建器可能是正确的方法,但是,有一些方法可以使字符串构建器更具功能性。一般来说,如果在功能程序中需要可变性,那么执行此操作的适当方法是为其创建功能包装器。在F#中,这通常表示为计算表达式。有一个字符串构建器计算表达式here的示例。

示例用法:

//Create a function which builds a string from an list of bytes
let bytes2hex (bytes : byte []) =
    string {
        for byte in bytes -> sprintf "%02x" byte
    } |> build

//builds a string from four strings
string {
        yield "one"
        yield "two"
        yield "three"
        yield "four"
    } |> build

编辑: 我对上面的计算表达式进行了新的实现,然后运行了Tomas的四个解决方案的发布版本以及我之前链接的计算表达式和计算表达式。

s1 elapsed Time: 128150 ms  //concatenation
s2 elapsed Time: 459 ms     //immutable list + String.concat
s3 elapsed Time: 354 ms     //lazy sequence and concatenating using StringBuilder & fold 
s4 elapsed Time: 39 ms      //imperative
s5 elapsed Time: 235 ms     //my computation expression
s6 elapsed Time: 334 ms     //the linked computation expression

请注意,s3的使用时间是命令的9倍,而s5只需要6倍的时间。

这是我对字符串构建器计算表达式的实现:

open System.Text

type StringBuilderUnion =
| Builder of StringBuilder
| StringItem of string

let build = function | Builder(x) -> string x | StringItem(x) -> string x

type StringBuilderCE () =
    member __.Yield (txt : string) = StringItem(txt)
    member __.Yield (c : char) = StringItem(c.ToString())
    member __.Combine(f,g) = Builder(match f,g with
                                     | Builder(F),   Builder(G)   ->F.Append(G.ToString())
                                     | Builder(F),   StringItem(G)->F.Append(G)
                                     | StringItem(F),Builder(G)   ->G.Insert(0, F)
                                     | StringItem(F),StringItem(G)->StringBuilder(F).Append(G))
    member __.Delay f = f()
    member __.Zero () = StringItem("")
    member __.For (xs : 'a seq, f : 'a -> StringBuilderUnion) =
                    let sb = StringBuilder()
                    for item in xs do
                        match f item with
                        | StringItem(s)-> sb.Append(s)|>ignore
                        | Builder(b)-> sb.Append(b.ToString())|>ignore
                    Builder(sb)

let builder1 = new StringBuilderCE ()

定时器功能(注意每个测试运行100次):

let duration f = 
    System.GC.Collect()
    let timer = new System.Diagnostics.Stopwatch()
    timer.Start()
    for _ in 1..100 do
        f() |> ignore
    printfn "elapsed Time: %i ms" timer.ElapsedMilliseconds