C# 与 F# 中的默认排序

Posted

技术标签:

【中文标题】C# 与 F# 中的默认排序【英文标题】:Default ordering in C# vs. F# 【发布时间】:2015-09-09 00:43:20 【问题描述】:

考虑分别对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

这两段代码返回不同的结果:

C#:茶和咖啡、电话、电视 F#:电视、茶和咖啡、电话

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

排序逻辑的差异是否存在根本原因? 在我的情况下,克服这个“问题”的推荐方法是什么? 这种现象是字符串特有的,还是也适用于其他 .NET 类型?

编辑

为了响应几个探测 cmets,运行下面的片段可以更多地了解这种排序差异的确切性质:

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();

给予:

C#:电视,电视,电视,电视,紫外线,紫外线,紫外线,紫外线 F#:电视、电视、紫外线、紫外线、电视、电视、紫外线、紫外线

字符串的字典顺序不同,因为字符的基本顺序不同:

C#:“aAbBcCdD...tTuUvV...” F#:“ABC..TUV..Zabc..tuv..”

【问题讨论】:

如果你这样做,似乎工作“很好”(fun s -> s.ToLower()) 这很奇怪......我真的希望这在 F# 中可以正常工作。如果你有以不同字母开头的单词,它们如何在 F# 中显示? 感谢@leppie - 这是“字符串大小写”的一个很好的解决方案。我的实际代码更通用 - 依赖于比较实现。如果我知道这是异常情况,将字符串作为特殊情况处理是可以接受的 - 但我怀疑发生的事情比我理解的要多...... 可以用 [| 《茶与咖啡》; “电话”; "Tv" |] |> Array.sort 必须是大写 根据this questionF#的sortBy方法没有使用Linq的OrderBy() 【参考方案1】:

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

List.sortWithArray.sortWith 都允许指定比较。 Enumerable.OrderBy 的重载也是如此。

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

具体问题:

排序逻辑上的差异是否存在根本原因?

两个命令都有效。在英语的情况下,不敏感似乎更自然,因为这是我们习惯的。但这并不能使它更正确。

在我的情况下,克服这个“问题”的推荐方法是什么?

明确说明比较的类型。

这种现象是字符串特有的,还是也适用于其他 .NET 类型?

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

【讨论】:

这些是有用的 cmets - 谢谢。我的理解(来自this page)是,如果一个类型实现了IComparable,那么它用于进行排序。如果我明确使用此接口为 C# 版本实现IComparer<string>,问题仍然存在。这表明我对 F# 如何进行比较(即使是 IComparable)的理解是错误的。如果有可能阐明 F# 如何选择进行比较,那将非常有趣.. @Lawrence [Seq.sortBy](msdn.microsoft.com/en-us/library/ee353610.aspx) 的文档说它使用了Operators.compare,但该页面上没有详细信息。是时候阅读语言规范或源代码了…… > 但是 Seq 模块似乎没有等效项(并且在 4.6 中没有添加)。Seq.sortWith 确实已添加到 F# 4.0 中。 请注意,F# 默认使用 ordinal 比较,这比仅 不区分大小写 更严格。 char 与所有大型 .NET 语言的序数比较进行比较,因此与 string 不同,F# 用户至少不会感到“惊讶”。 @latkin 我没有检查Seq 是否有未来版本的 F#。 WRT 序数:.NET 还支持序数不区分大小写的比较,因此序数与区分大小写有些正交,并迅速进入所需的大量背景细节。【参考方案2】:

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

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

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

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

所以,尝试回答我自己的问题:

排序逻辑上的差异是否存在根本原因?

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

在我的情况下,克服这个“问题”的推荐方法是什么?

虽然我无法评论这是否是“推荐的”,但如果相关类型上有一个 IComparable 的实现,下面的 F# 函数 order 将使用:

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

这种现象是字符串特有的,还是也适用于其他 .NET 类型?

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

【讨论】:

在您的代码中,您要排序两次:首先是Seq.sortBy,然后是Array.sortWith【参考方案3】:

这与 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"|]

根据MSDN, OrderBy 方法“使用默认比较器 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"] 

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

【讨论】:

【参考方案4】:

请参阅language spec 的第 8.15.6 节。

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

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

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

OCAML 兼容 字符串和字符比较是一致的 C#Comparer&lt;string&gt;.Default.Compare("a", "A") // -1 C#Comparer&lt;char&gt;.Default.Compare('a', 'A') // 32 F#compare "a" "A" // 1 F#compare 'a' 'A' // 32

编辑:

请注意,声明“F# 使用 区分大小写 字符串比较”是一种误导(尽管并非不正确)。 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

【讨论】:

谢谢@latkin - 这正是我一直在寻找的好答案 - 真的很有帮助!

以上是关于C# 与 F# 中的默认排序的主要内容,如果未能解决你的问题,请参考以下文章

为啥 F# (FSharpOption<T>) 中的默认参数是引用类型?

C# 集合及集合内排序 问题 速求

构造函数中的 C# 默认值与序列化的两个构造函数相同

C中的结构内存布局

C# DataMember 序列化器排序与预期相反

C#中的委托