바이레프스(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>
을 전달할 수 있습니다.
byrefs 사용
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>
다르지 않습니다. 단, 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#와의 상호 운용성
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)
두 경우 모두 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
-like" 구조체는 스택 바인딩된 값 형식입니다. 관리되는 힙에는 할당되지 않습니다.
byref
과 유사한 구조체는 수명 및 비캡처에 대한 강력한 검사가 강제되므로, 고성능 프로그래밍에 유용합니다. 규칙은 다음과 같습니다.
- 함수 매개 변수, 메서드 매개 변수, 지역 변수, 메서드 반환으로 사용할 수 있습니다.
- 클래스 또는 일반 구조체의 정적 또는 인스턴스 멤버일 수 없습니다.
- 클로저 구문(
async
메서드 또는 람다 식)으로 캡처할 수 없습니다. - 제네릭 매개 변수로 사용할 수 없습니다.
- F# 9부터는 allows ref 구조체 안티 제약 조건을 사용하여 제네릭 매개 변수가 C#에 정의된 경우 이 제한이 완화됩니다. F#은 byref와 유사한 형식을 사용하여 형식 및 메서드에서 이러한 제네릭을 인스턴스화할 수 있습니다. 몇 가지 예로, 이는 BCL 대리자 형식(
Action<>
,Func<>
), 인터페이스(IEnumerable<>
,IComparable<>
) 및 사용자가 제공한 누적기 함수(String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)
)를 사용하는 제네릭 인수에 영향을 줍니다. - F#에서 byref와 유사한 형식을 지원하는 제네릭 코드를 작성하는 것은 불가능합니다.
- F# 9부터는 allows ref 구조체 안티 제약 조건을 사용하여 제네릭 매개 변수가 C#에 정의된 경우 이 제한이 완화됩니다. F#은 byref와 유사한 형식을 사용하여 형식 및 메서드에서 이러한 제네릭을 인스턴스화할 수 있습니다. 몇 가지 예로, 이는 BCL 대리자 형식(
|>
입력 형식을 매개 변수화하는 제네릭 함수이므로 이 마지막 지점은 F# 파이프라인 스타일 프로그래밍에 매우 중요합니다. 향후 |>
에 대한 이 제한은 인라인 상태이며 본문에서 인라인되지 않은 제네릭 함수를 호출하지 않기 때문에 완화될 수 있습니다.
이러한 규칙은 사용을 강력하게 제한하지만 안전한 방식으로 고성능 컴퓨팅의 약속을 이행합니다.
Byref 값 반환
F# 함수 또는 멤버에서 Byref 반환을 생성 및 사용할 수 있습니다.
byref
반환 메서드를 사용하는 경우 값은 암시적으로 역참조됩니다. 예를 들어:
let squareAndPrint (data : byref<int>) =
let squared = data*data // data is implicitly dereferenced
printfn $"%d{squared}"
값 바이레프를 반환하려면 값이 포함된 변수가 현재 범위보다 오래 있어야 합니다.
또한 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
바이레프 참조 범위 지정
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