C# 与 F# 中的默认排序

Default ordering in C# vs. F#

考虑在 C#F# 中分别对字符串进行简单排序的两个代码片段:

C#:

var strings = new[] { "Tea and Coffee", "Telephone", "TV" };
var orderedStrings = strings.OrderBy(s => s).ToArray();

F#:

let strings = [| "Tea and Coffee"; "Telephone"; "TV" |]
let orderedStrings =
    strings
    |> Seq.sortBy (fun s -> s)
    |> Seq.toArray

这两段代码return不同的结果:

在我的特定情况下,我需要关联这两种语言之间的排序逻辑(一种是生产代码,一种是测试断言的一部分)。这提出了几个问题:

编辑

为了回应几条探索性评论,运行以下片段揭示了更多关于此排序差异的确切性质:

F#:

let strings = [| "UV"; "Uv"; "uV"; "uv"; "Tv"; "TV"; "tv"; "tV" |]
let orderedStrings =
    strings
    |> Seq.sortBy (fun s -> s)
    |> Seq.toArray

C#:

var strings = new[] { "UV", "Uv", "uv", "uV", "TV", "tV", "Tv", "tv" };
var orderedStrings = strings.OrderBy(s => s).ToArray();

给出:

由于字符的基本顺序不同,字符串的字典顺序不同:

不同的库对默认的字符串比较操作选择不同。 F# 严格默认区分大小写,而 LINQ to Objects 不区分大小写。

两者都List.sortWith and Array.sortWith allow the comparison to be specified. As does an overload of Enumerable.OrderBy

但是 Seq 模块似乎没有等效项(4.6 中没有添加)。

具体问题:

Is there an underlying reason for the differences in ordering logic?

两种顺序均有效。在英语情况下,不敏感似乎更自然,因为那是我们习惯的。但这并不能使它更正确。

What is the recommended way to overcome this "problem" in my situation?

明确比较的种类。

Is this phenomenon specific to strings, or does it apply to other .NET types too?

char也会受到影响。以及有不止一种可能排序的任何其他类型(例如 People 类型:您可以根据具体要求按姓名或出生日期排序)。

感谢@Richard 和 为我指出了进一步理解这个问题的方向

我的问题似乎根源于没有完全理解 F# 中 comparison 约束的后果。这是Seq.sortBy

的签名
Seq.sortBy : ('T -> 'Key) -> seq<'T> -> seq<'T> (requires comparison)

我的假设是,如果类型 'T 实现了 IComparable,那么这将用于排序。我应该先咨询这个问题:F# comparison vs C# IComparable,其中包含一些有用的参考资料,但需要进一步仔细阅读才能完全理解发生了什么。

因此,尝试回答我自己的问题:

Is there an underlying reason for the differences in ordering logic?

是的。 C# 版本似乎使用了 IComparable 的字符串实现,而 F# 版本没有。

What is the recommended way to overcome this "problem" in my situation?

虽然我无法评论这是否是 "recommended",但下面的 F# 函数 order 将使用 IComparable 的实现,如果相关类型上有的话:

let strings = [| "UV"; "Uv"; "uV"; "uv"; "Tv"; "TV"; "tv"; "tV" |]
let order<'a when 'a : comparison> (sequence: seq<'a>) = 
    sequence 
    |> Seq.toArray
    |> Array.sortWith (fun t1 t2 ->
        match box t1 with
        | :? System.IComparable as c1 -> c1.CompareTo(t2)
        | _ ->
            match box t2 with
            | :? System.IComparable as c2 -> c2.CompareTo(t1)
            | _ -> compare t1 t2)
let orderedValues = strings |> order

Is this phenomenon specific to strings, or does it apply to other .NET types too?

comparison 约束和 IComparable 接口之间的关系显然存在一些微妙之处。为了安全起见,我将遵循@Richard 的建议,并始终明确比较的种类——可能使用上面的函数 "prioritize" 在排序中使用 IComparable

这与 C# 与 F# 无关,甚至与 IComparable 无关,而只是由于库中的不同排序实现。

TL;DR;版本是排序字符串可以给出不同的结果:

"tv" < "TV"  // false
"tv".CompareTo("TV")  // -1 => implies "tv" *is* smaller than "TV"

或者更清楚:

"a" < "A"  // false
"a".CompareTo("A")  // -1 => implies "a" is smaller than "A"

这是因为CompareTo使用当前文化(see MSDN)

我们可以通过一些不同的示例来了解这在实践中的效果。

如果我们使用标准的 F# 排序,我们会得到大写优先的结果:

let strings = [ "UV"; "Uv"; "uV"; "uv"; "Tv"; "TV"; "tv"; "tV" ]

strings |> List.sort 
// ["TV"; "Tv"; "UV"; "Uv"; "tV"; "tv"; "uV"; "uv"]

即使我们转换为 IComparable,我们也会得到相同的结果:

strings |> Seq.cast<IComparable> |> Seq.sort |> Seq.toList
// ["TV"; "Tv"; "UV"; "Uv"; "tV"; "tv"; "uV"; "uv"]

另一方面,如果我们在 F# 中使用 Linq,我们会得到与 C# 代码相同的结果:

open System.Linq
strings.OrderBy(fun s -> s).ToArray()
// [|"tv"; "tV"; "Tv"; "TV"; "uv"; "uV"; "Uv"; "UV"|]

根据MSDNOrderBy 方法 "compares keys by using the default comparer Default."

F#库默认不使用Comparer,但我们可以使用sortWith:

open System.Collections.Generic
let comparer = Comparer<string>.Default

现在当我们进行这种排序时,我们得到与 LINQ 相同的结果 OrderBy:

strings |> List.sortWith (fun x y -> comparer.Compare(x,y))
// ["tv"; "tV"; "Tv"; "TV"; "uv"; "uV"; "Uv"; "UV"]

或者,我们可以使用内置的 CompareTo 函数,它给出相同的结果:

strings |> List.sortWith (fun x y -> x.CompareTo(y))
// ["tv"; "tV"; "Tv"; "TV"; "uv"; "uV"; "Uv"; "UV"] 

故事的寓意:如果您关心排序,请始终指定要使用的具体比较!

参见 language spec 的第 8.15.6 节。

字符串、数组和本机整数具有特殊的比较语义,如果已实现,其他所有内容都会转到 IComparable(对产生相同结果的各种优化取模)。

特别是,F# 字符串默认使用 ordinal 比较,而大多数 .NET 默认使用文化感知比较。

这显然是 F# 和其他 .NET 语言之间令人困惑的不兼容性,但它确实有一些好处:

  • OCAML 兼容
  • String和char比较一致
    • C#Comparer<string>.Default.Compare("a", "A") // -1
    • C#Comparer<char>.Default.Compare('a', 'A') // 32
    • F#compare "a" "A" // 1
    • F#compare 'a' 'A' // 32

编辑:

请注意,"F# uses case-sensitive string comparison" 的说法具有误导性(尽管并非不正确)。 F# 使用 ordinal 比较,这比仅区分大小写更严格。

// case-sensitive comparison
StringComparer.InvariantCulture.Compare("[", "A") // -1
StringComparer.InvariantCulture.Compare("[", "a") // -1

// ordinal comparison
// (recall, '[' lands between upper- and lower-case chars in the ASCII table)
compare "[" "A"  // 26
compare "[" "a"  // -6