Byrefs
F# には、低レベルのプログラミングの領域を扱う 2 つの主要な機能領域があります。
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
には 3 つの形式があります。
inref<'T>
は、基になる値を読み取るためのマネージド ポインターです。outref<'T>
は、基になる値に書き込むためのマネージド ポインターです。byref<'T>
は、基になる値の読み取りと書き込みを行うためのマネージド ポインターです。
inref<'T>
が想定される場合は、byref<'T>
を渡すことができます。 同様に、outref<'T>
が想定される場合は、byref<'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
ポインターを読み取る代わりに書き込みだけを行う場合は、byref<'T>
の代わりに outref<'T>
を使用することを検討してください。
inref セマンティクス
次のコードがあるとします。
let f (x: inref<SomeStruct>) = x.SomeField
意味的には、これは次のことを意味します。
x
ポインターの所有者は、それを使用して値を読み取ることのみできます。SomeStruct
内の入れ子になったstruct
フィールドに対して取得されたポインターには、inref<_>
型が指定されます。
次のことも当てはまります。
- 他のスレッドまたは別名に
x
への書き込みアクセス権がないということではありません。 x
がinref
であるという理由でSomeStruct
が不変であることにはなりません。
ただし、不変の F# 値型の場合、this
ポインターは inref
として推論されます。
これらのすべての規則が組み合わされると、inref
ポインターの所有者が、ポイントされているメモリの直接の内容を変更できないことを意味します。
outref セマンティクス
outref<'T>
の目的は、ポインターの書き込みだけが必要であることを示すことです。 意外にも、outref<'T>
では、その名前にもかかわらず基になる値の読み取りが許可されます。 これは、互換性のみを目的としています。
意味的には、outref<'T>
は byref<'T>
と何ら変わりません。ただし、[<Out>]
パラメーターを含むメソッドを呼び出す場合と同様に、outref<'T>
パラメーターを含むメソッドがタプル戻り値の型に暗黙的に構築されているという 1 点においてのみ異なります。
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# との相互運用
C# では、ref
戻り値に加えて、in ref
と out 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] 属性 |
型の推定とオーバーロードの規則
inref<'T>
型は、次の場合に F# コンパイラによって推定されます。
IsReadOnly
属性を持つ .NET パラメーターまたは戻り値の型。- 変更可能なフィールドを持たない構造体型の
this
ポインター。 - 別の
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)
どちらの場合も、inref<System.DateTime>
を受け取るオーバーロードではなく System.DateTime
を受け取るオーバーロードが解決されます。
byref に似た構造体
byref
/inref
/outref
の 3 つに加えて、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
メソッドまたはラムダ式) によってキャプチャすることはできません。 - これらは、ジェネリック パラメーターとして使用することはできません。
この最後の点は F# パイプライン スタイルのプログラミングに不可欠です。|>
はその入力の型をパラメーター化するジェネリック関数であるからです。 |>
に対するこの制限は、将来緩和される可能性があります。これはインラインであり、その本体で非インライン ジェネリック関数を呼び出すことはないからです。
これらの規則によって使用法が厳密に制限されますが、そうするのはハイパフォーマンス コンピューティングの約束事を安全な方法で実現するためです。
byref の戻り値
F# 関数またはメンバーから byref の戻り値を生成して使用することができます。 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!
()
これにより、最適化を使用してコンパイルするかどうかに応じて異なる結果が生成されるのを防ぐことができます。
.NET