다음을 통해 공유


바이레프스(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에 대한 쓰기 권한을 갖고 있지 않다는 시사점은 없습니다.
  • 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#와의 상호 운용성

C#은 ref 반환 외에도 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] 특성

형식 유추 및 오버로드 규칙

inref<'T> 형식은 다음 경우에 F# 컴파일러에 의해 유추됩니다.

  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-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# 파이프라인 스타일 프로그래밍에 매우 중요합니다. 향후 |>에 대한 이 제한은 인라인 상태이며 본문에서 인라인되지 않은 제네릭 함수를 호출하지 않기 때문에 완화될 수 있습니다.

이러한 규칙은 사용을 강력하게 제한하지만 안전한 방식으로 고성능 컴퓨팅의 약속을 이행합니다.

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!
    ()

이렇게 하면 최적화를 사용하여 컴파일하는지 여부에 따라 다른 결과를 얻을 수 없습니다.