2013-04-15 246 views
7

有少數情況下,當F#記錄的行爲是奇怪的對我說:奇怪的行爲

上含糊不清不警告

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string;} 

// F# compiler will use second type without any complains or warnings 
let p = {Id = 42; Name = "Foo";} 

警告的記錄解構,而不是記錄的建設

F#編譯器在記錄「解構」時發出警告,而不是在前面的記錄構造中發出警告:

// Using Person and AnotherPerson types and "p" from the previous example! 
// We'll get a warning here: "The field labels and expected type of this 
// record expression or pattern do not uniquely determine a corresponding record type" 
let {Id = id; Name = name} = p 

注意,存在與模式匹配沒有警告(我懷疑那是因爲模式是使用「記載建築表現形式」,而不是與「記載解構式」建):

match p with 
| {Id = _; Name = "Foo"} -> printfn "case 1" 
| {Id = 42; Name = _} -> printfn "case 2" 
| _ -> printfn "case 3" 

類型缺少字段的推理錯誤

F#編譯器會選擇第二種類型,並且會因爲缺少Age字段而發出錯誤!

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string; Age: int} 

// Error: "No assignment given for field 'Age' of type 'Person'" 
let p = {Id = 42; Name = "Foo";} 

醜陋的語法「中記載解構」

我問了好幾家的同事我的一個問題:「這段代碼是怎麼一回事?」

type Person = {Id: int; Name: string;} 
let p = {Id = 42; Name = "Foo";} 

// What will happend here? 
let {Id = id; Name = name} = p 

這對每個人總有意外驚喜的是「ID」和「名」實際上是「左值」,雖然它們放置在表達的「右手邊」。我明白,這更多的是關於個人喜好,但是對於大多數人來說似乎很奇怪,在一個特定情況下,輸出值被放在表達的右側。

我不認爲所有這些都是錯誤,我懷疑這些東西大部分都是功能。
我的問題是:這種晦澀的行爲背後有沒有理性?

回答

6

你的例子可以分爲兩類:記錄表達式記錄圖形。雖然記錄表達式需要聲明所有字段並返回一些表達式,但記錄模式有可選字段並且用於模式匹配。 The MSDN page on Records有兩個明確的部分,它可能值得一讀。

在這個例子中,

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string;} 

// F# compiler will use second type without any complains or warnings 
let p = {Id = 42; Name = "Foo";} 

行爲是從the MSDN page above規定的規則明確。

最近聲明的類型的標籤優先 那些先前聲明的類型

在模式匹配的情況下,你專注於創建你需要一些綁定。所以你可以寫

type Person = {Id: int; Name: string;} 
let {Id = id} = p 

爲了得到id綁定供以後使用。圖案let綁定匹配可能看起來有點不可思議,但它是非常相似的方式,你通常模式匹配函數參數:

type Person = {Id: int; Name: string;} 
let extractName {Name = name} = name 

我覺得你的方式警告匹配的例子是合理的,因爲編譯器無法猜測你的意圖。

不過,不建議使用重複字段的不同記錄。至少你應該使用合格的名稱,以避免混淆:

type AnotherPerson = {Id: int; Name: string} 
type Person = {Id: int; Name: string; Age: int} 

let p = {AnotherPerson.Id = 42; Name = "Foo"} 
+0

謝謝,@ pad。我瞭解目前採取最後定義的行爲是有據可查的。但對我而言,這似乎仍然很奇怪。在這種情況下,我更喜歡得到錯誤或警告。那麼我的部分稱爲「丟失字段的類型推斷錯誤」呢?你覺得合理嗎? –

5

我覺得大部分的評論都涉及到一個事實,即記錄名稱成爲在記錄被定義的命名空間直接可用 - 也就是說,當你定義具有屬性NameId的記錄Person,名稱NameId是全局可見的。這有優點也有缺點:

  • 的好處是,它使編程更加容易,因爲你可以只寫{Id=1; Name="bob"}
  • 壞事是這些名稱可以在範圍上和其他記錄名稱衝突所以如果你的名字不唯一(你的第一個例子),你會陷入困境。

您可以告訴編譯器,您總是要使用RequireQualifiedAccess屬性明確限定名稱。這意味着你將不能寫只是IdName,但你需要始終包含的類型名稱:

[<RequireQualifiedAccess>] 
type AnotherPerson = {Id: int; Name: string} 
[<RequireQualifiedAccess>] 
type Person = {Id: int; Name: string;} 

// You have to use `Person.Id` or `AnotherPerson.Id` to determine the record 
let p = {Person.Id = 42; Person.Name = "Foo" } 

這給你一個更嚴格的模式,但它使編程不太方便。默認情況下(更模糊的行爲)已經由@pad解釋 - 編譯器會簡單地選擇稍後在源代碼中定義的名稱。即使在通過查看錶達式中的其他字段來推斷類型的情況下也可以這樣做 - 僅僅因爲查看其他字段不會總是有效(例如,當您使用關鍵字with時),所以最好堅持使用簡單一致的策略。

至於模式匹配,當我第一次看到語法時,我非常困惑。我認爲這不是經常使用,但它可以是有用的。

意識到F#不使用結構類型(意味着您不能使用具有更多字段的記錄作爲以較少字段記錄的函數的參數)。這可能是一個有用的功能,但它不適合.NET類型系統。這基本上意味着你不能期望太花哨的東西 - 論據必須是一個衆所周知的命名記錄類型的記錄。

當你寫:

let {Id = id; Name = name} = p 

術語左值idname出現在模式而不是在表達的事實。在F#的語法定義告訴你是這樣的:

expr := let <pat> = <expr> 
     | { id = <expr>; ... } 
     | <lots of other expressions> 

pat := id 
     | { id = <pat>; ... } 
     | <lots of other patterns> 

所以,在let=左手邊是一個模式,而右手邊是一個表達式。 F#中的兩個共享類似結構 - (x, y)可用於構造和去構造一個元組。而記錄的情況也是如此...

+0

謝謝,托馬斯。它清楚這是一個功能,但不是一個錯誤;)我的觀點是:爲什麼我們有當前的行爲?例如,如果編譯器看到兩個匹配的記錄類型,我寧願看到這個(至少有警告)。那麼「丟失字段的類型推斷錯誤」部分呢?你覺得合理嗎? –

+0

@Sergey:爲什麼要在變量陰影不變的情況下輸入陰影保證警告/錯誤?事實上,後者是慣用的,並不令人驚訝...... – ildjarn

+0

@ildjarn:假設我們有大項目,有人會在其中添加類似的記錄。編譯器會在解決方案資源管理器中選擇與文件順序有關的最新的一個對我來說太危險了。爲什麼*在這種情況下不發出警告*?不向用戶顯示警告的好處是什麼? –