byrefの概要
F#には低レベルプログラミングの領域を扱うための主要な機能が2つあります。
- byref / inref / outref (= マネージドポインタ型)
実行時に無効な操作となるようなものをコンパイルしないようにするために、使用上の制限があります。 - IsByRefLike属性
この属性を使うことによって、byrefと同じような機能とコンパイル時制限を課すことができます。Span<T> などがその代表的な例です。
構文
//《 byref 》
// byref は呼び出し元で値が初期化されていることを求めます.
// また, 関数内部で Read / Write のどちらも実行することを示唆します.
let f (x: byref<_>) =
(* do something *)
//《 inref 》
// inref は呼び出し元で値が初期化されていることを求めます.
// また, inref で渡された値に対しては Read 以外の処理は実行できません.
let f (x: inref<_>) =
(* do something *)
//《 outref 》
// outref は呼び出し元で値の初期化をしていることを求めません.
// 関数内で引数の値を初期化することを示唆します.
// また, 関数内部で Read / Write のどちらの処理も実行することができます.
let f (x: outref<_>) =
(* do something *)
// 必ずopenする必要があります.
open System.Runtime.CompilerServices
[<Struct; IsByRefLike>]
type S(count1: int, count2: int) =
member x.Count1 = count1
member x.Count2 = count2
概要
これらの機能は主に構造体を利用する際に使われます。これは構造体がデフォルトで値渡しとなっているため、余計なコストを回避するための手段として有効であるためです。また、IsByRefLike属性を利用することで、基本的にスタック上で値の操作が実行されるようになり、(時と場合にもよりますが) 処理を高速に済ますことが可能となります。ただし、byref / outrefは内部で値の書き換えを許可してしまえる関係上、参照透過性を保つことが難しくなってしまいます。そのため、利用する範囲を可能な限り狭くしつつ、基本的には inref を利用するように心がけましょう。inref ではどうしても実現できない処理がある場合に限り outref を利用し、outref でも実現が困難な場合のみ byref を利用するのがベターです。
サンプルでみるbyref
inref
まずは inref のサンプルコードを見ていきましょう。// -------------------------------------
// inref
open System
let f (x:inref<DateTime>) =
printfn "x= %s" (x.ToString ())
// x <- DateTime.Now // xに対する再代入処理はできません.
// トップレベルでinrefをパラメータにもつ関数を呼び出すことはできない
// let now = DateTime.Now
// f &now
// inrefのパラメータに値を渡す時は,
// &を利用してアドレスを指定する必要があります.
let usage =
let now = DateTime.Now
f &now
usage
ここで注目すべき点は2箇所あります。まず1点目はトップレベルで inref をパラメータにもつ関数をコールすることができないということです。これは先述した通り、スタック上で処理を実行するた目の制約の一つです。そのため、必ず関数内などで利用する必要があります。
そして2点目は引数に値を指定する際は、"&" を付与する必要があるということです。C/C++を学んだことがある方は見覚えのある形式かもしれません。これは値のアドレスを取得するためのものです。つまり、inrefパラメータには値のアドレスを引数に渡していることがここからわかります。
outref
次に outref のサンプルコードを見ていきましょう。// -------------------------------------
// outref
open System
let string (d:DateTime) =
d.ToString("yyyy/MM/dd HH:mm:ss.ffffff")
let f (x:outref<DateTime>) =
x <- DateTime.Now // xに対する再代入処理が可能!
// outrefのパラメータに値を渡す時は,
// &を利用してアドレスを指定する必要があります.
let usage =
// let now = DateTime.Now // 通常の宣言だと,
// f &now // ココでエラー
let mutable now = DateTime.Now // mutableでの宣言が必須となることに注意!
now
|> string
|> printfn "now= %s"
f &now
now
|> string
|> printfn "now= %s"
usage
// outref の場合, inrefとは違いトップレベルでの関数呼び出しも可能.
let mutable now = DateTime.Now
now
|> string
|> printfn "now= %s"
f &now
now
|> string
|> printfn "now= %s"
サンプルコードを見てもわかるとおり、inref とは違い outref を引数にとる関数はトップレベルで呼び出すことができます。また、パラメータに対して再代入できるところが inref と大きく異なる点です。利用方法は inref と同じく、引数に値を指定する際には "&" を付与する必要があるので注意しましょう。また、outref で渡されてくる引数は有効であるという保証がないため、書き込みだけでなく読み取りも行いたい場合は outref ではなく byref を利用するようにしましょう。
byref
最後に byref のサンプルコードを見ていきましょう。// -------------------------------------
// byref
open System
let f (x:byref<DateTime>) =
printfn "x= %s" (x.ToString ()) // 安全に読み取ることが可能!
x <- DateTime.Now // xに対する再代入処理も可能!
printfn "x= %s" (x.ToString ())
// byrefのパラメータに値を渡す時も,
// &を利用してアドレスを指定する必要があります.
let usage =
// let now = DateTime.Now // 通常の宣言だと,
// f &now // ココでエラー
let mutable now = DateTime.Now // mutableでの宣言が必須となることに注意!
f &now
usage
// byref の場合, outrefと同じくトップレベルでの関数呼び出しも可能.
let mutable now = DateTime.Now
f &now
byrefは、パラメータの読み取りと書き込みの両方をしたい場合に利用します。さらに言えば、読み取りのみをしたい場合には "inref" を利用し、書き込みのみをしたい場合には "outref" を利用するようにします。一見すると「byref は inref/outref の両方の性質を持っているため、すべてにおいて byref を利用すればよいのではないか?」と思ってしまいがちですが、これは誤りです。なぜならば、inref は「読み取りのみするので引数への変更はしません」という意味を内包しており、outref は「引数が不正な値でも書き込みしかしないので問題ありません」という意味を内包しているためです。これがすべて byref になってしまったとたん、本当であれば読み取りしかしないはずなのに呼び出し元で「値が変更されているかもしれない」という可能性を考えながらプログラムを読み書きしなければならなくなり、書き込みしかしないはずなのに呼び出しもとで「有効値のみ渡さなければならない」という認識の元でプログラムを読み書きしなければならなくなります。
プログラムを書いていく上では、必ず意図と合致する機能を選択し、コーディングを進めるようにしましょう。