共用方式為


Byrefs

F# 有兩個主要功能區域,可處理低階程式設計的空間:

  • byref/inref/outref 型別,也就是受控指標。 它們在使用上有所限制,因此您無法在執行階段編譯不正確的程式。
  • byref 的結構,是一種具有與 byref<'T> 類似語意及相同編譯時間限制的結構。 其中一個範例是 Span<T>

語法

// Byref types as parameters
let f (x: byref<'T>) = ()
let g (x: inref<'T>) = ()
let h (x: outref<'T>) = ()

// Calling a function with a byref parameter
let mutable x = 3
f &x

// Declaring a byref-like struct
open System.Runtime.CompilerServices

[<Struct; IsByRefLike>]
type S(count1: int, count2: int) =
    member x.Count1 = count1
    member x.Count2 = count2

Byref、Inref 和 Outref

byref 有三種形式:

  • inref<'T>,用於讀取基礎值的受控指標。
  • outref<'T>,用於寫入基礎值的受控指標。
  • byref<'T>,用於讀取和寫入基礎值的受控指標。

byref<'T> 可在預期 inref<'T> 的情況下被傳遞。 同樣地,byref<'T> 可在預期 outref<'T> 的情況下被傳遞。

使用 Byref

若要使用 inref<'T>,您必須使用 & 來取得指標值:

open System

let f (dt: inref<DateTime>) =
    printfn $"Now: %O{dt}"

let usage =
    let dt = DateTime.Now
    f &dt // Pass a pointer to 'dt'

若要使用 outref<'T>byref<'T> 寫入指標,您也必須將擷取到的值設為 mutable 的指標。

open System

let f (dt: byref<DateTime>) =
    printfn $"Now: %O{dt}"
    dt <- DateTime.Now

// Make 'dt' mutable
let mutable dt = DateTime.Now

// Now you can pass the pointer to 'dt'
f &dt

如果您只寫入指標而不進行讀取,請考慮使用 outref<'T> 而不是 byref<'T>

Inref 語意

請考慮下列程式碼:

let f (x: inref<SomeStruct>) = x.SomeField

語意上,這表示下列各項:

  • x 指標的預留位置只能使用它來讀取值。
  • SomeStruct 內的 struct 巢狀欄位取得的任何指標,都是指定型別 inref<_>

下列也成立:

  • 沒有任何隱含指出,其他執行緒或別名沒有對 x 的寫入權限。
  • 沒有任何隱含指出因為 xinref,所以 SomeStruct 不可變。

然而,針對不可變 的 F# 實值型別,系統會將 this 指標推斷為 inref

所有這些規則都表示 inref 指標的預留位置可能不會修改所指向之記憶體的立即內容。

Outref 語意

outref<'T> 的用途是指出指標應該只能被寫入。 在其名稱的前提下,outref<'T> 意外地允許讀取基礎值。 這是為了達到相容性。

語意上,outref<'T>byref<'T> 除了一項差異之外並無不同:具有 outref<'T> 參數的方法會隱含建構為元組傳回型別,就像使用 [<Out>] 參數來呼叫方法一樣。

type C =
    static member M1(x, y: _ outref) =
        y <- x
        true

match C.M1 1 with
| true, 1 -> printfn "Expected" // Fine with outref, error with byref
| _ -> printfn "Never matched"

使用 C# 相互操作

除了 ref 傳回之外,C# 還支援 in refout ref 關鍵字。 下列表格顯示 F# 如何解譯 C# 發出的內容:

C# 建構 F# 推斷
ref 傳回值 outref<'T>
ref readonly 傳回值 inref<'T>
in ref 參數 inref<'T>
out ref 參數 outref<'T>

下列表格顯示 F# 發出的內容:

F# 建構 發出的建構
inref<'T> 引數 在引數上的 [In] 屬性
inref<'T> 傳回 在值上的 modreq 屬性
在抽象位置或實作中的 inref<'T> 在引數或傳回上的 modreq
outref<'T> 引數 在引數上的 [Out] 屬性

型別推斷和多載規則

在下列情況下,F# 編譯器會推斷 inref<'T> 型別:

  1. 具有 IsReadOnly 屬性的 .NET 參數或傳回型別。
  2. 結構類型上不具可變動欄位的 this 指標。
  3. 衍生自另一個 inref<_> 指標的記憶體位置之位址。

取得 inref 的隱含位址時,具有 SomeType 型別引數的多載會優先於具有型別 inref<SomeType> 引數的多載。 例如:

type C() =
    static member M(x: System.DateTime) = x.AddDays(1.0)
    static member M(x: inref<System.DateTime>) = x.AddDays(2.0)
    static member M2(x: System.DateTime, y: int) = x.AddDays(1.0)
    static member M2(x: inref<System.DateTime>, y: int) = x.AddDays(2.0)

let res = System.DateTime.Now
let v =  C.M(res)
let v2 =  C.M2(res, 4)

在兩種情況下,系統都會解析採用 System.DateTime 的多載,而不是採用 inref<System.DateTime> 的多載。

類 Byref 結構

除了 byref/inref/outref 三者之外,您還可以自行定義遵守與 byref 類似的語意之結構。 請使用 IsByRefLikeAttribute 屬性進行:

open System
open System.Runtime.CompilerServices

[<IsByRefLike; Struct>]
type S(count1: Span<int>, count2: Span<int>) =
    member x.Count1 = count1
    member x.Count2 = count2

IsByRefLike 並不表示 Struct。 兩者都必須存在於類型上。

F# 中「類似 byref」的結構是與堆疊繫結的實值型別。 永遠不會配置在受控堆積上。 類似 byref 的結構適用於高效能程式設計,因為它會施行一組關於存留期和非擷取的強式檢查。 規則如下:

  • 可作為函式參數、方法參數、區域變數、方法傳回。
  • 不能是類別或一般結構的靜態或執行個體成員。
  • 無法供任何結束建構 (async 方法或 Lambda 運算式) 擷取。
  • 它們不能作為泛型參數使用。

最後一點對於 F# 管線樣式的程式設計而言非常重要,因為 |> 是一種會對其輸入型別進行參數化的泛型函式。 未來可能會放寬對 |> 的這項限制,因為它內嵌,而且不會在其主體中對非內嵌泛型函式進行任何呼叫。

雖然這些規則會強式限制使用方式,但它們會以安全的方式履行高效能運算的承諾。

Byref 傳回

來自 F# 函式或成員的傳回可供產生和取用。 取用 byref 傳回方法時,該值為隱含取值。 例如:

let squareAndPrint (data : byref<int>) =
    let squared = data*data    // data is implicitly dereferenced
    printfn $"%d{squared}"

若要傳回 Byref 值,包含值的變數的存留期必須超過目前的範圍。 此外,若要傳回 Byref 值,請使用 &value (值為存留期超過目前範圍的變數)。

let mutable sum = 0
let safeSum (bytes: Span<byte>) =
    for i in 0 .. bytes.Length - 1 do
        sum <- sum + int bytes[i]
    &sum  // sum lives longer than the scope of this function.

為了避免隱含取值,例如透過多個鏈結呼叫傳遞參考,請使用 &x (其中 x 為值)。

您也可以直接指派給傳回 byref。 請考量下列 (高度命令式) 程式:

type C() =
    let mutable nums = [| 1; 3; 7; 15; 31; 63; 127; 255; 511; 1023 |]

    override _.ToString() = String.Join(' ', nums)

    member _.FindLargestSmallerThan(target: int) =
        let mutable ctr = nums.Length - 1

        while ctr > 0 && nums[ctr] >= target do ctr <- ctr - 1

        if ctr > 0 then &nums[ctr] else &nums[0]

[<EntryPoint>]
let main argv =
    let c = C()
    printfn $"Original sequence: %O{c}"

    let v = &c.FindLargestSmallerThan 16

    v <- v*2 // Directly assign to the byref return

    printfn $"New sequence:      %O{c}"

    0 // return an integer exit code

此為輸出:

Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence:      1 3 7 30 31 63 127 255 511 1023

Byref 的範圍界定

let 繫結值不能使其參考超出其定義範圍。 例如,下列是不允許的參數:

let test2 () =
    let x = 12
    &x // Error: 'x' exceeds its defined scope!

let test () =
    let x =
        let y = 1
        &y // Error: `y` exceeds its defined scope!
    ()

這可防止您根據是否使用最佳化進行編譯而取得不同的結果。