다음을 통해 공유


낮은 수준의 구조체 개선 사항

메모

이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.

기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는관련 LDM(언어 디자인 모임) 노트에서 캡처됩니다.

사양문서에서 C# 언어 표준으로 기능 스펙릿을 채택하는 과정에 대해 더 알아볼 수 있습니다.

요약

이 제안은 struct 성능 향상을 위한 여러 가지 제안을 모은 것으로, ref 필드와 수명 기본값을 재정의할 수 있는 기능을 포함하고 있습니다. 다양한 제안을 고려하여 하위 수준의 struct 개선을 위한 포괄적인 기능 집합을 만드는 것이 목표인 디자인입니다.

참고: 이 사양의 이전 버전에서는 Span 안전 기능 사양에 도입된 "ref-safe-to-escape" 및 "safe-to-escape"라는 용어들을 사용했습니다. ECMA 표준 위원회는 이름을 "ref-safe-context" "안전 컨텍스트"각각 변경했습니다. 안전 컨텍스트의 값은 "declaration-block", "function-member" 및 "caller-context"를 일관되게 사용하도록 구체화되었습니다. 스펙렛은 이러한 용어에 대해 다른 구문을 사용했으며 "호출자 컨텍스트"의 동의어로 "반환 안전"을 사용했습니다. 이 스펙렛은 C# 7.3 표준의 용어를 사용하도록 업데이트되었습니다.

이 문서에 설명된 모든 기능이 C# 11에서 구현된 것은 아닙니다. C# 11에는 다음이 포함됩니다.

  1. ref 필드 및 scoped
  2. [UnscopedRef]

이러한 기능은 C#의 향후 버전에 대한 공개 제안으로 남아 있습니다.

  1. ref 필드에서 ref struct으로
  2. 일몰 시 제한되는 유형

동기

이전 버전의 C#에서는 ref 반환, ref struct, 함수 포인터 등 다양한 하위 수준 성능 기능을 언어에 추가했습니다. 이러한 기능을 통해 .NET 개발자는 형식 및 메모리 안전을 위해 C# 언어 규칙을 계속 활용하면서 성능이 뛰어난 코드를 작성할 수 있었습니다. 또한 Span<T>같은 .NET 라이브러리에서 기본 성능 유형을 만들 수 있습니다.

.NET 에코시스템에서 이러한 기능이 인기를 얻으면서, 내부 및 외부의 개발자들은 에코시스템 내 남아 있는 마찰 지점들에 대한 정보를 지속적으로 제공하고 있습니다. 작업을 완료하기 위해 unsafe 코드로 드롭하거나 런타임이 Span<T>같은 특수한 사례 형식으로 있어야 하는 위치입니다.

현재 Span<T> 런타임이 효과적으로 internal 필드로 처리하는 ByReference<T> 형식 ref 사용하여 수행됩니다. 이는 ref 필드의 이점을 제공하지만, 언어가 ref의 다른 용도에서처럼 이를 위한 안전 확인을 제공하지 않는다는 단점이 있습니다. 또한 dotnet/runtime만 이 형식을 internal사용할 수 있으므로 타사에서는 ref 필드를 기반으로 자체 기본 형식을 디자인할 수 없습니다. 이 작업의 동기 중 일부는 ByReference<T>를 제거하고 모든 코드 베이스에서 적절한 ref 필드를 사용하는 것입니다.

이 제안은 기존의 낮은 수준의 기능을 기반으로 구축하여 이러한 문제를 해결할 계획입니다. 특히 다음을 목표로 합니다.

  • ref struct 형식이 ref 필드를 선언하도록 허용합니다.
  • 런타임에서 C# 형식 시스템을 사용하여 Span<T>을 완전히 정의하고, 특별한 경우 형식인 ByReference<T>을 제거합니다.
  • struct 형식이 그들의 필드로 ref을 반환할 수 있도록 허용합니다.
  • 수명 기본값의 제한으로 인해 발생한 unsafe 사용을 런타임에서 제거할 수 있도록 허용
  • 관리되는 형식과 관리되지 않는 형식에 대해 안전한 fixed 버퍼를 struct에서 선언할 수 있도록 허용합니다.

상세 디자인

안전 규칙은 기존 용어를 사용하여 범위 안전 문서에 정의되어 있습니다. 이러한 규칙은 §9.7.2§16.4.12C# 7 표준에 통합되었습니다. 이 문서에서는 이 제안의 결과로 이 문서의 필수 변경 내용을 설명합니다. 승인된 기능으로 수락되면 이러한 변경 내용이 해당 문서에 통합됩니다.

이 디자인이 완료되면 Span<T> 정의는 다음과 같습니다.

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    // This constructor does not exist today but will be added as a part 
    // of changing Span<T> to have ref fields. It is a convenient, and
    // safe, way to create a length one span over a stack value that today 
    // requires unsafe code.
    public Span(ref T value)
    {
        _field = ref value;
        _length = 1;
    }
}

ref 필드와 범위 지정 기능을 제공

이 언어를 사용하면 개발자가 ref내에서 ref struct 필드를 선언할 수 있습니다. 예를 들어 큰 변경 가능한 struct 인스턴스를 캡슐화하거나 런타임 외에 라이브러리의 Span<T> 같은 고성능 형식을 정의하는 경우에 유용할 수 있습니다.

ref struct S 
{
    public ref int Value;
}

ref 필드는 ELEMENT_TYPE_BYREF 서명을 사용하여 메타데이터로 내보낼 것입니다. 이는 ref 지역 변수 또는 ref 인수를 생성하는 방식과 다르지 않습니다. 예를 들어 ref int _fieldELEMENT_TYPE_BYREF ELEMENT_TYPE_I4로 내보내집니다. 이를 위해 ECMA335를 이 항목을 허용할 수 있도록 업데이트해야 하지만, 이는 비교적 간단할 것입니다.

개발자는 ref struct 식을 사용하여 ref 필드가 있는 default를 초기화할 수 있습니다. 이 경우, 선언된 모든 ref 필드는 null값을 갖게 될 것입니다. 이러한 필드를 사용하려고 하면 NullReferenceException 예외가 발생합니다.

ref struct S 
{
    public ref int Value;
}

S local = default;
local.Value.ToString(); // throws NullReferenceException

C# 언어는 refnull이 될 수 없는 것처럼 보이지만, 실제로 런타임에서는 이것이 합법적이며 명확한 의미 체계를 가지고 있습니다. ref 필드를 형식에 도입하는 개발자는 이러한 가능성을 알고 있어야 하며 이 세부 정보를 코드 사용으로 유출하지 않도록 강력하게 방지해야 합니다. 대신 ref 필드는 런타임 도우미를 사용하여 null이 아님을 확인하고, 초기화되지 않은 struct이 잘못 사용되는 경우 예외를 발생시켜야 합니다.

ref struct S1 
{
    private ref int Value;

    public int GetValue()
    {
        if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
        {
            throw new InvalidOperationException(...);
        }

        return Value;
    }
}

ref 필드는 다음과 같은 방법으로 readonly 한정자와 결합할 수 있습니다.

  • readonly ref: 생성자 또는 init 메서드 외부에서 다시 할당할 수 없는 필드입니다. 이러한 컨텍스트 외부에 할당된 값일 수 있습니다.
  • ref readonly: 다시 할당할 수 있지만 언제든지 값을 할당할 수 없는 필드입니다. 이렇게 하면 in 매개 변수를 ref 필드에 다시 할당할 수 있습니다.
  • readonly ref readonly: ref readonlyreadonly ref조합입니다.
ref struct ReadOnlyExample
{
    ref readonly int Field1;
    readonly ref int Field2;
    readonly ref readonly int Field3;

    void Uses(int[] array)
    {
        Field1 = ref array[0];  // Okay
        Field1 = array[0];      // Error: can't assign ref readonly value (value is readonly)
        Field2 = ref array[0];  // Error: can't repoint readonly ref
        Field2 = array[0];      // Okay
        Field3 = ref array[0];  // Error: can't repoint readonly ref
        Field3 = array[0];      // Error: can't assign ref readonly value (value is readonly)
    }
}

readonly ref structref 필드를 readonly ref로 선언해야 합니다. readonly ref readonly를 밝힐 필요는 없습니다. 이렇게 하면 readonly struct 이러한 필드를 통해 간접 돌연변이를 가질 수 있지만 오늘날 참조 형식을 가리키는 readonly 필드와 다르지 않습니다(자세한 내용은).

readonly ref 플래그를 사용하여 다른 필드와 같은 방식으로 메타데이터에 initonly가 전송됩니다. ref readonly 필드에 System.Runtime.CompilerServices.IsReadOnlyAttribute속성이 부여됩니다. readonly ref readonly이 두 항목과 함께 방출됩니다.

이 기능을 사용하려면 런타임 지원 및 ECMA 사양 변경이 필요합니다. 따라서 해당 기능 플래그가 corelib로 설정된 경우에만 사용하도록 설정됩니다. 정확한 API를 추적하는 이슈는 여기에서 관리됩니다https://github.com/dotnet/runtime/issues/64165

ref 필드를 허용하는 데 필요한 안전한 컨텍스트 규칙의 변경 집합은 작고 대상으로 지정됩니다. 규칙은 이미 API에서 존재하고 소비되는 ref 필드를 고려합니다. 변경 내용은 만드는 방법과 다시 할당되는 방법의 두 가지 측면에만 집중해야 합니다.

먼저 필드에 대한 ref-safe-context 값을 설정하는 규칙을 다음과 같이 ref 필드에 대해 업데이트해야 합니다.

다음과 같은 형식의 식은 ref e.Fref-safe-context입니다.

  1. Fref 필드인 경우, ref-safe-contexte입니다.
  2. 그렇지 않은 경우, e가 참조 형식이라면, 호출자 컨텍스트의 ref-safe-context를 가집니다.
  3. 그렇지 않으면 의 ref-safe-contexte에서 가져옵니다.

규칙이 항상 ref내에 존재하는 ref struct 상태를 고려했으므로 규칙 변경을 나타내지는 않습니다. 실제로 refSpan<T> 상태는 항상 작동해 왔고, 소비 규칙이 이를 올바르게 반영합니다. 여기에서 변경된 내용은 개발자가 ref 필드에 직접 액세스할 수 있도록 하는 것이며, 그들이 암시적으로 Span<T>에 적용된 기존 규칙을 따라서 액세스할 수 있게 하는 것입니다.

이것은 그렇지만 ref 필드는 ref에서 ref struct으로 반환될 수 있지만, 일반 필드는 반환될 수 없다는 것을 의미합니다.

ref struct RS
{
    ref int _refField;
    int _field;

    // Okay: this falls into bullet one above. 
    public ref int Prop1 => ref _refField;

    // Error: This is bullet four above and the ref-safe-context of `this`
    // in a `struct` is function-member.
    public ref int Prop2 => ref _field;
}

언뜻 보기에 오류처럼 보일 수 있지만 이는 의도적인 디자인 포인트입니다. 다시 말하지만, 이 제안은 새로운 규칙을 생성하는 것이 아니라, 지금까지 Span<T> 상태로 행동해 온 기존 규칙을 인정하여, 개발자들이 자신의 ref 상태를 선언할 수 있도록 하는 것입니다.

다음으로, ref 필드가 존재할 때 ref 재할당 규칙을 조정해야 합니다. 재할당의 주요 시나리오는 ref struct 생성자가 ref 매개변수를 ref 필드에 저장하는 것입니다. 지원이 더 일반적이지만 이것이 핵심 시나리오입니다. 이를 지원하기 위해 다음과 같이 ref 필드를 고려하도록 ref 재할당 규칙이 조정됩니다.

Ref 재할당 규칙

= ref 연산자의 왼쪽 피연산자는 ref 지역 변수에 바인딩하거나, ref 매개변수(단, this제외), out 매개변수, 또는 ref 필드에 바인딩하는 식이어야 합니다.

ref 재할당의 경우, e1 = ref e2 형식에서 다음 두 가지 모두가 true여야 합니다.

  1. e2 ref-safe-context 최소한 ref-safe-contexte1
  2. e1안전 컨텍스트e2참고와 동일해야 합니다.

즉, 원하는 Span<T> 생성자는 추가 주석 없이 작동합니다.

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    public Span(ref T value)
    {
        // Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The 
        // safe-context of `this` is *return-only* and ref-safe-context of `value` is 
        // *caller-context* hence this is legal.
        _field = ref value;
        _length = 1;
    }
}

ref 재할당 규칙의 변경으로 ref 매개변수가 메서드를 벗어나 ref 값의 ref struct 필드에 포함될 수 있게 되었습니다. 호환성 고려 사항 섹션에서 설명한 대로ref 매개 변수가 ref 필드로 이스케이프되지 않은 기존 API에 대한 규칙을 변경할 수 있습니다. 매개 변수의 수명 규칙은 사용법이 아닌 선언만을 기반으로 합니다. 모든 refin 매개 변수는 호출자 컨텍스트에 있는 ref-safe-context을 가지고 있으므로 이제 ref 또는 ref 필드로 반환될 수 있습니다. 이스케이프 또는 이스케이프되지 않을 수 있는 ref 매개 변수가 있는 API를 지원하고 C# 10 호출 사이트 의미 체계를 복원하기 위해 언어는 제한된 수명 주석을 도입합니다.

scoped 한정자

키워드 scoped 값의 수명을 제한하는 데 사용됩니다. ref 또는 ref struct 값에 적용되며, 각각 ref-safe-context 또는 safe-context의 수명을 함수 멤버로 제한하는 영향을 미칩니다. 예를 들어:

매개 변수 또는 지역 ref-safe-context 안전한 맥락
Span<int> s 함수 멤버 호출자 컨텍스트
scoped Span<int> s 함수 멤버 함수 멤버
ref Span<int> s 호출자 컨텍스트 호출자 컨텍스트
scoped ref Span<int> s 함수 멤버 호출자 컨텍스트

이 관계에서 값의 ref-safe-context안전 컨텍스트보다 넓을 수 없습니다.

이렇게 하면 C# 11의 API에 주석을 추가하여 C# 10과 동일한 규칙을 가질 수 있습니다.

Span<int> CreateSpan(scoped ref int parameter)
{
    // Just as with C# 10, the implementation of this method isn't relevant to callers.
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 and legal in C# 11 due to scoped ref
    return CreateSpan(ref parameter);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

또한 scoped 주석은 이제 thisstruct 매개 변수를 scoped ref T로 정의할 수 있음을 의미합니다. 이전에는 다른 ref 매개 변수와 다른 ref-safe-context 규칙이 있는 ref 매개 변수로 규칙에서 특수한 경우여야 했습니다(안전한 컨텍스트 규칙에서 수신기를 포함하거나 제외하는 모든 참조 참조 참조). 이제 규칙 전체에서 일반 개념으로 표현되어 이를 더욱 간소화할 수 있습니다.

scoped 주석은 다음 위치에도 적용할 수 있습니다.

  • locals: 이 주석은 초기화 수명에 관계없이 안전 컨텍스트로 또는 로컬의 경우 ref로 수명을 설정하며, 이는 함수 멤버에 해당합니다.
Span<int> ScopedLocalExamples()
{
    // Error: `span` has a safe-context of *function-member*. That is true even though the 
    // initializer has a safe-context of *caller-context*. The annotation overrides the 
    // initializer
    scoped Span<int> span = default;
    return span;

    // Okay: the initializer has safe-context of *caller-context* hence so does `span2` 
    // and the return is legal.
    Span<int> span2 = default;
    return span2;

    // The declarations of `span3` and `span4` are functionally identical because the 
    // initializer has a safe-context of *function-member* meaning the `scoped` annotation
    // is effectively implied on `span3`
    Span<int> span3 = stackalloc int[42];
    scoped Span<int> span4 = stackalloc int[42];
}

지역 주민의 대한 다른 용도는 아래설명되어 있습니다.

scoped 주석은 반환, 필드, 배열 요소 등을 포함한 다른 위치에 적용할 수 없습니다. 하지만 scopedref, in, out에 적용될 때 영향을 미치지만, 그것은 오직 ref struct값에 적용될 때만 영향을 미칩니다. 비 scoped int을 반환하는 것이 항상 안전하기 때문에 ref struct 같은 선언은 아무런 영향을 미치지 않습니다. 컴파일러는 개발자 혼동을 방지하기 위해 이러한 사례에 대한 진단을 만듭니다.

out 매개 변수의 동작 변경

refin 매개 변수를 ref 필드로 반환할 수 있도록 하는 호환성 변경의 영향을 추가로 제한하기 위해, 언어는 파라미터에 대한 기본 out 값을 함수 멤버로 변경합니다. 효과적으로 out 매개 변수는 암시적으로 앞으로 scoped out. 이는 호환성의 관점에서 볼 때, 그것들은 ref에서 반환될 수 없음을 의미합니다.

ref int Sneaky(out int i) 
{
    i = 42;

    // Error: ref-safe-context of out is now function-member
    return ref i;
}

이렇게 하면 더 이상 참조로 캡처되는 매개 변수를 고려할 필요가 없으므로 ref struct 값을 반환하고 out 매개 변수를 갖는 API의 유연성이 향상됩니다. 이는 판독기 스타일 API의 일반적인 패턴이므로 중요합니다.

Span<byte> Read(Span<byte> buffer, out int read)
{
    // .. 
}

Span<byte> Use()
{
    var buffer = new byte[256];

    // If we keep current `out` ref-safe-context this is an error. The language must consider
    // the `read` parameter as returnable as a `ref` field
    //
    // If we change `out` ref-safe-context this is legal. The language does not consider the 
    // `read` parameter to be returnable hence this is safe
    int read;
    return Read(buffer, out read);
}

또한 언어는 더 이상 out 매개 변수에 전달된 인수를 반환할 수 있는 것으로 간주하지 않습니다. out 매개 변수에 대한 입력을 반환 가능한 것으로 처리하는 것은 개발자에게 매우 혼란스러웠습니다. 기본적으로 개발자가 out존중하지 않는 언어를 제외하고는 사용되지 않는 호출자가 전달한 값을 고려하도록 강요하여 out 의도를 전복시킵니다. ref struct 지원하는 전달 언어는 out 매개 변수에 전달된 원래 값을 읽지 않도록 해야 합니다.

C#은 명확한 할당 규칙을 통해 이를 달성합니다. 참조 보안 컨텍스트 규칙을 달성함과 동시에 기존 코드에서 out 매개변수 값을 할당하고 반환하는 것을 허용합니다.

Span<int> StrangeButLegal(out Span<int> span)
{
    span = default;
    return span;
}

이러한 변경으로 인해 out 매개 변수에 대한 인수가 메서드 호출에 안전한 컨텍스트 또는 ref-safe-context 값을 제공하지 않습니다. 이렇게 하면 ref 필드의 전반적인 호환성이 크게 줄어들고 개발자가 out대해 어떻게 생각하는지 간소화할 수 있습니다. out 매개 변수에 대한 인수는 반환에 기여하지 않고 단순히 출력입니다.

선언 식의 안전 컨텍스트 유추

선언 변수의 안전한 컨텍스트out 인수(M(x, out var y)) 또는 분해((var x, var y) = M())에서 다음 중 가장 좁은 컨텍스트입니다.

  • 호출자-컨텍스트
  • out 변수가 scoped으로 표시된 경우, 선언 블록(즉, 함수 멤버 또는 더 좁은)의 범위에 해당합니다.
  • out 변수의 형식이 ref struct인 경우 수신기를 포함하여 해당 호출의 모든 인수를 고려합니다.
    • 해당 매개 변수가 않고 반환 전용 이상의 안전한 컨텍스트 인수의 안전 컨텍스트
    • 매개 변수가 반환 전용 이상인 경우에 해당하는 인수의 ref-safe-contextref-safe-context입니다.

유추된 안전 컨텍스트선언 식의 예제를 참조하십시오.

암시적으로 scoped 매개변수

전체적으로 ref로 암시적으로 선언된 두 개의 scoped 위치가 있습니다.

  • this 인스턴스 메서드에 대한 struct
  • out 매개 변수

참조 안전 컨텍스트 규칙은 scoped refref기준으로 작성됩니다. ref safe 컨텍스트의 경우 in 매개 변수는 ref에 해당하며, outscoped ref에 해당합니다. inout 모두 규칙의 의미 체계에 중요한 경우에만 구체적으로 호출됩니다. 그렇지 않으면 각각 refscoped ref 간주됩니다.

매개 변수에 해당하는 인수의 in를 논의할 때, 이들은 사양에서 ref 인수로 일반화됩니다. 인수가 lvalue인 경우, ref-safe-context는 lvalue의 것이고, 그렇지 않으면 함수 멤버입니다. in는 현재 규칙의 의미상 중요한 경우에만 다시 여기에서 언급됩니다.

반환 전용 안전 컨텍스트

디자인에서는 새로운 안전 컨텍스트인 반환 전용도입해야 합니다. 이는 반환할 수 있지만 문을 통해서만 반환될 수 호출자 컨텍스트 비슷합니다.

반환 전용의 세부 사항은 함수 멤버보다는 크지만, 호출자 컨텍스트보다는 작은 컨텍스트입니다. return 문에 제공된 식은 적어도 반환 전용이어야 합니다. 따라서 대부분의 기존 규칙은 적용되지 않습니다. 예를 들어, ref안전 컨텍스트의 식을 매개 변수에 할당하려고 하면, 이는 ref 매개 변수의 안전 컨텍스트호출자 컨텍스트보다 작기 때문에 실패하게 됩니다. 이 새로운 이스케이프 컨텍스트의 필요성은 아래 에서 논의될 것입니다.

기본적으로 반환 전용세 가지 위치가 있습니다.

  • ref 또는 in 매개 변수는 ref-safe-context와 함께 반환 전용이 있습니다. 이것은 ref struct바보 같은 순환 할당 문제를 방지하기 위해 부분적으로 수행됩니다. 모델을 단순화하고 호환성 변경을 최소화하기 위해 균일하게 수행됩니다.
  • out을 위한 ref struct 매개 변수에는 반환 전용안전 컨텍스트가 있습니다. 이렇게 하면 반환과 out이 동일하게 표현될 수 있도록 합니다. out는 암시적으로 scoped이므로 참조-안전-컨텍스트가 여전히 안전-컨텍스트보다 작아서, 이는 순환 할당 문제를 일으키지 않습니다.
  • this 생성자에 대한 struct 매개 변수에는 반환 전용안전 컨텍스트 있습니다. 모델이 out 매개 변수로 설정되었기 때문에 이 문제가 발생합니다.

메서드나 람다에서 값을 명시적으로 반환하는 모든 표현식이나 문장은 최소한 안전 컨텍스트를 가져야 하며, 해당되는 경우 참조 안전 컨텍스트반환 전용컨텍스트를 가져야 합니다. 여기에는 return 문, 표현식 본문 멤버 및 람다 식이 포함됩니다.

마찬가지로, out 할당에는 반환 전용안전 컨텍스트가 적어도 있어야 합니다. 그러나 이것은 특별한 경우는 아니지만 기존 할당 규칙에서 따릅니다.

참고: 형식이 ref struct 형식이 아닌 식은 항상 안전 컨텍스트호출자 컨텍스트를 갖고 있습니다.

메서드 호출 규칙

메서드 호출에 대한 ref safe 컨텍스트 규칙은 여러 가지 방법으로 업데이트됩니다. 첫 번째는 scoped 인수에 미치는 영향을 인식하는 것입니다. 주어진 인수 expr가 매개변수 p에 전달될 때:

  1. p이(가) scoped ref인 경우, 인수를 고려할 때 exprref-safe-context에 기여하지 않습니다.
  2. 일 경우, 인수를 고려할 때 는 안전한 컨텍스트에서 에 기여하지 않습니다.
  3. 경우 ref-safe-context 기여하거나 안전 컨텍스트를 자세한 내용은

"기여하지 않음" 언어는 메서드 반환의 ref-safe-context 또는 safe-context 값을 계산할 때 인수가 단순히 고려되지 않음을 의미합니다. 이는 scoped 주석이 이를 방지하므로 값이 해당 수명에 기여할 수 없기 때문입니다.

이제 메서드 호출 규칙을 간소화할 수 있습니다. 수신기는 이제 struct의 경우 단순히 scoped ref T이 되므로 특별한 경우일 필요가 없습니다. 값 규칙은 ref 필드 반환을 고려하도록 변경해야 합니다.

e1.M(e2, ...)이 ref-to-ref-struct를 반환하지 않는다면, 메서드 호출 M()의 결과값에는 다음 중 가장 좁은 위치에서 가져온 의 안전 컨텍스트가 있습니다.

  1. 호출자 컨텍스트
  2. 반환 값이 ref struct일 때 모든 인수 식에 의해 제공된 안전 컨텍스트
  3. 반환이 ref struct 경우 모든 인수에서 제공하는 ref

M()이 ref-to-ref-struct를 반환하는 경우, 안전 컨텍스트는 ref-to-ref-struct인 모든 인수의 안전 컨텍스트와 동일합니다. 서로 다른 안전 컨텍스트을 가진 여러 인수가 있는 경우에는 메서드 인수가일치해야 하기 때문에 오류가 발생합니다.

ref 호출 규칙을 다음과 같이 간소화할 수 있습니다.

이 ref-to-ref-struct를 반환하지 않는 경우, 메서드 호출 의 결과 값은 다음 컨텍스트들 중 가장 좁은 ref-safe-context 입니다.

  1. 호출자 컨텍스트
  2. 모든 인수 식에 의해 제공되는 안전 컨텍스트
  3. 모든 인수에서 제공하는 ref

M()가 ref-to-ref-struct를 반환하는 경우, ref-to-ref-struct인 모든 인수에서 제공하는 ref-safe-context 중 가장 좁은 ref-safe-contextref-safe-context입니다.

이제 이 규칙을 사용하여 원하는 메서드의 두 가지 변형을 정의할 수 있습니다.

Span<int> CreateWithoutCapture(scoped ref int value)
{
    // Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *function-member* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> CreateAndCapture(ref int value)
{
    // Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *caller-context* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
    // Okay: the safe-context of `span` is *caller-context* hence this is legal.
    return span;

    // Okay: the local `refLocal` has a ref-safe-context of *function-member* and a 
    // safe-context of *caller-context*. In the call below it is passed to a 
    // parameter that is `scoped ref` which means it does not contribute 
    // ref-safe-context. It only contributes its safe-context hence the returned
    // rvalue ends up as safe-context of *caller-context*
    Span<int> local = default;
    ref Span<int> refLocal = ref local;
    return ComplexScopedRefExample(ref refLocal);

    // Error: similar analysis as above but the safe-context of `stackLocal` is 
    // *function-member* hence this is illegal
    Span<int> stackLocal = stackalloc int[42];
    return ComplexScopedRefExample(ref stackLocal);
}

개체 이니셜라이저에 대한 규칙

개체 이니셜라이저 식의 안전 컨텍스트은 다음 중 가장 좁습니다.

  1. 생성자 호출의 안전 컨텍스트.
  2. 안전-컨텍스트 및 멤버 이니셜라이저 인덱서가 수신기로 이스케이프할 수 있는 경우의 인수의 ref-안전-컨텍스트.
  3. 비 읽기 전용 setter의 멤버 이니셜라이저에 할당되는 RHS의 safe-context 또는 참조 할당의 경우 ref-safe-context .

이를 모델링하는 또 다른 방법은 수신기에 할당할 수 있는 멤버 이니셜라이저에 대한 인수를 생성자에 대한 인수로 간주하는 것입니다. 멤버 이니셜라이저가 실제로 생성자 호출이기 때문입니다.

Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
    Field = stackSpan;
}

// Can be modeled as 
var x = new S(ref heapSpan, stackSpan);

이 모델링은 MAMM이 멤버 이니셜라이저에 대해 특별히 고려해야 보여 주므로 중요합니다. 이 특정 사례는 더 좁은 안전 컨텍스트를 가진 값을 더 높은 컨텍스트에 할당하는 것을 허용하기 때문에 불법이어야 합니다.

메서드 인수가 일치해야 합니다.

ref 필드가 있으면 메서드 인수와 일치하는 규칙이 업데이트되어야 하며, ref 매개 변수를 메서드의 ref struct 인수 필드로 저장할 수 있기 때문입니다. 이전에는 규칙이 필드로 저장되는 또 다른 ref struct만 고려하면 되었습니다. 이로 인한 영향은 호환성 고려 사항에서 논의됩니다. 새 규칙은 ...

어떤 메서드 호출 e.M(a1, a2, ... aN)에 대해서도

  1. 가장 좁은 의 안전한 컨텍스트을 계산합니다.
    • 호출자 컨텍스트
    • 모든 인수의 안전한 맥락
    • 모든 ref 인수의 ref-safe-context 중 해당 매개변수에 ref-safe-context호출자 컨텍스트 인 것
  2. ref 형식의 모든 ref struct 인수는 안전 컨텍스트를 가진 값으로 할당할 수 있어야 합니다. refinout를 포함하도록 일반화되지 않는 경우입니다.

어떤 메서드 호출 e.M(a1, a2, ... aN)에 대해서도

  1. 가장 좁은 의 안전한 컨텍스트을 계산합니다.
    • 호출자 컨텍스트
    • 모든 인수의 안전한 맥락
    • 모든 ref 인수의 해당 매개 변수가 아닌 scoped
  2. out 형식의 모든 ref struct 인수는 안전 컨텍스트를 가진 값으로 할당할 수 있어야 합니다.

scoped의 존재는 개발자가 반환되지 않는 매개 변수를 scoped로 표시함으로써 이 규칙이 초래하는 마찰을 줄일 수 있게 해줍니다. 이렇게 하면 위의 두 경우 모두 (1)에서 그들의 주장이 제거되고, 호출자에게 더 큰 유연성을 제공합니다.

이 변경의 영향은아래에서 더 깊이 설명합니다. 전반적으로 이를 통해 개발자는 이스케이프되지 않는 참조와 유사한 값에 scoped주석을 추가하여 호출 사이트를 보다 유연하게 만들 수 있습니다.

매개 변수 범위 분산

매개 변수의 한정자 및 특성(아래 참조)은 개체 재정의, 인터페이스 구현 및 변환 규칙에도 영향을 줍니다. 재정의, 인터페이스 구현 또는 delegate 변환을 위한 서명은 다음과 같을 수 있습니다.

  • scoped 또는 ref 매개 변수에 in 추가
  • scopedref struct 매개 변수에 추가
  • [UnscopedRef] 매개 변수에서 out 제거
  • [UnscopedRef] 유형의 ref 매개 변수에서 ref struct을 제거합니다.

scoped 또는 [UnscopedRef] 관련된 다른 차이점은 불일치로 간주됩니다.

컴파일러는 다음 조건이 충족될 때 재정의, 인터페이스 구현 및 대리자 변환 간에 안전하지 않은 범위 불일치 관련하여 진단을 보고합니다.

  • 메서드는 ref struct 반환하거나 ref 또는 ref readonly반환하거나 메서드에 ref 형식의 out 또는 ref struct 매개 변수가 있습니다.
  • 메서드에는 하나 이상의 추가 ref, in또는 out 매개 변수 또는 ref struct 형식의 매개 변수가 있습니다.

위의 규칙은 this 인스턴스 메서드를 재정의, 인터페이스 구현 또는 대리자 변환에 사용할 수 없으므로 ref struct 매개 변수를 무시합니다.

일치하지 않는 서명이 모두 C#11 ref 안전 컨텍스트 규칙을 사용하는 경우, 진단 결과가 오류로 보고됩니다. 그렇지 않은 경우, 진단 결과가 경고로 나타납니다.

범위 불일치 경고는 scoped이(가) 사용 불가능한 C#7.2 참조 안전성 컨텍스트 규칙으로 컴파일된 모듈에서 보고될 수 있습니다. 이러한 경우에 일치하지 않는 다른 서명을 수정할 수 없는 경우 경고를 표시하지 않는 것이 필요할 수 있습니다.

scoped 한정자 및 [UnscopedRef] 특성은 메서드 서명에도 다음과 같은 영향을 줍니다.

  • scoped 한정자 및 [UnscopedRef] 특성은 숨기기에 영향을 미치지 않습니다.
  • 오버로드는 단지 scoped 또는 [UnscopedRef]로만 구분될 수 없습니다.

ref 필드와 scoped 섹션이 길어 제안된 중대한 변경 사항에 대한 간략한 요약으로 마무리하고자 했습니다.

  • ref-safe-context호출자 컨텍스트에 있는 값은 ref 또는 ref 필드로 반환할 수 있습니다.
  • out 매개 변수는 안전 컨텍스트를 가지며, 함수 멤버입니다.

자세한 참고 사항:

  • ref 필드는 ref struct 내에서만 선언할 수 있습니다.
  • ref 필드는 static, volatile 또는 const 선언할 수 없습니다.
  • ref 필드에는 ref struct 형식이 있을 수 없습니다.
  • 참조 어셈블리 생성 프로세스는 ref 내에 ref struct 필드가 있는지를 유지해야 합니다.
  • readonly ref structref 필드를 readonly ref로 선언해야 합니다.
  • 참조별 값의 경우 scoped 한정자가 in, out또는 ref 전에 나타나야 합니다.
  • 범위 안전 규칙 문서는 이 문서에 설명된 대로 업데이트됩니다.
  • 새 ref safe 컨텍스트 규칙은 둘 중 하나일 때 적용됩니다.
    • 핵심 라이브러리에는 ref 필드에 대한 지원을 나타내는 기능 플래그가 포함되어 있습니다.
    • langversion 값이 11 이상입니다.

통사론

13.6.2 지역 변수 선언: 'scoped'?추가되었습니다.

local_variable_declaration
    : 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
    ;

local_variable_mode_modifier
    : 'ref' 'readonly'?
    ;

13.9.4 for: 'scoped'?에서 local_variable_declaration 추가되었습니다.

13.9.5 foreach 문장: 'scoped'?추가되었습니다.

foreach_statement
    : 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
      embedded_statement
    ;

12.6.2 인수 목록: 'scoped'? 선언 변수에 out를 추가했습니다.

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' ('scoped'? local_variable_type)? identifier
    ;

12.7 분해 식:

[TBD]

15.6.2 메서드 매개 변수: 'scoped'?parameter_modifier에 추가되었습니다.

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

parameter_modifier
    | 'this' 'scoped'? parameter_mode_modifier?
    | 'scoped' parameter_mode_modifier?
    | parameter_mode_modifier
    ;

parameter_mode_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

20.2 대리자 선언: 간접적으로 추가되었습니다.

12.19 익명 함수 식: 'scoped'?추가됨.

explicit_anonymous_function_parameter
    : 'scoped'? anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

일몰 시 제한되는 유형

컴파일러에는 문서화되지 않은 "제한된 형식" 집합의 개념이 있습니다. C# 1.0에서는 동작을 표현하는 범용 방법이 없었기 때문에 이러한 형식에 특별한 상태가 부여되었습니다. 특히 형식에 실행 스택에 대한 참조가 포함될 수 있다는 사실입니다. 대신 컴파일러는 이들에 대해 특별한 지식을 가지고 있어 항상 안전한 방식으로만 사용하도록 제한합니다. 반환이 허용되지 않으며, 배열 요소로 사용할 수 없고, 제네릭에서도 사용할 수 없습니다.

ref 필드를 사용할 수 있고 ref struct 지원하기 위해 확장되면 ref structref 필드의 조합을 사용하여 C#에서 이러한 형식을 올바르게 정의할 수 있습니다. 따라서 컴파일러가 런타임이 ref 필드를 지원한다는 것을 감지하면 더 이상 제한된 형식의 개념이 없습니다. 대신 코드에 정의된 형식을 사용합니다.

이를 지원하기 위해 참조 안전 컨텍스트 규칙은 다음과 같이 업데이트됩니다.

  • __makeref은(는) 서명이 static TypedReference __makeref<T>(ref T value)인 메서드로 처리됩니다.
  • __refvaluestatic ref T __refvalue<T>(TypedReference tr)서명을 가진 메서드로 처리됩니다. 식 __refvalue(tr, int) 두 번째 인수를 형식 매개 변수로 효과적으로 사용합니다.
  • __arglist은(는) 매개변수로 함수 멤버ref-safe-contextsafe-context를 갖습니다.
  • __arglist(...) 표현은 ref-safe-context안전 컨텍스트를 가진 함수 멤버입니다.

런타임이 준수되면 TypedReference, RuntimeArgumentHandle, 및 ArgIteratorref struct로 정의됩니다. 추가 TypedReference 가능한 모든 형식에 대한 refref struct 필드가 있는 것으로 간주되어야 합니다(모든 값을 저장할 수 있습니다). 위의 규칙과 결합하면 스택에 대한 참조가 수명 범위를 벗어나지 않도록 보장할 수 있습니다.

참고: 엄밀히 말해, 이는 언어의 일부라기보다는 컴파일러 구현의 세부 사항입니다. 그러나 ref 필드와의 관계를 감안할 때 단순성을 위해 언어 제안에 포함됩니다.

범위가 지정되지 않은 기능 제공

가장 주목할 만한 마찰 지점 중 하나는 ref인스턴스 멤버에서 struct 필드를 반환할 수 없다는 것입니다. 즉, 개발자는 메서드/속성을 반환하는 ref 만들 수 없으며 필드를 직접 노출해야 합니다. 이로 인해 ref에서는 가장 원했던 struct 반환의 유용성이 떨어지게 됩니다.

struct S
{
    int _field;

    // Error: this, and hence _field, can't return by ref
    public ref int Prop => ref _field;
}

이 기본값에 대한 근거는 합리적이지만, 참조로 struct 탈출하는 것이 this 본질적으로 잘못된 것은 아닙니다. 이는 단순히 참조 안전 컨텍스트 규칙에 의해 선택된 기본값일 뿐입니다.

이 문제를 해결하기 위해 언어는 scoped을 지원함으로써 UnscopedRefAttribute 수명 주석의 반대를 제공할 것입니다. 이것은 모든 ref에 적용될 수 있으며 ref-safe-context를 기본값보다 한 단계 더 넓게 변경합니다. 예를 들어:

UnscopedRef를 ~에 적용 원래 ref-safe-context ref-safe-context
인스턴스 멤버 함수-멤버 반품 전용
in / ref 매개 변수 반품 전용 호출자-컨텍스트
out 매개 변수 함수-멤버 반품 전용

[UnscopedRef]의 인스턴스 메서드에 struct를 적용하면 암시적 this 매개 변수가 수정됩니다. 즉, this는 동일한 형식의 주석이 없는 ref로 작용합니다.

struct S
{
    int field; 

    // Error: `field` has the ref-safe-context of `this` which is *function-member* because 
    // it is a `scoped ref`
    ref int Prop1 => ref field;

    // Okay: `field` has the ref-safe-context of `this` which is *caller-context* because 
    // it is a `ref`
    [UnscopedRef] ref int Prop1 => ref field;
}

주석을 out 매개 변수에 배치하여 C# 10 동작으로 복원할 수도 있습니다.

ref int SneakyOut([UnscopedRef] out int i)
{
    i = 42;
    return ref i;
}

ref safe 컨텍스트 규칙을 위해 이러한 [UnscopedRef] out 단순히 ref간주됩니다. in는 수명에 대한 측면에서 ref로 간주되는 것과 유사합니다.

[UnscopedRef] 주석은 init내의 struct 멤버 및 생성자에 대해 허용되지 않습니다. 이러한 멤버는 ref 멤버를 변경 가능한 것으로 보는 점에서 readonly 의미 체계와 관련하여 이미 특별한 존재입니다. 즉, 해당 멤버에게 ref 수행하는 것은 간단한 ref로 나타나며, ref readonly가 아닙니다. 생성자와 init의 경계 내에서 이것은 허용됩니다. [UnscopedRef]을 허용하면 이러한 ref이 생성자 외부로 잘못 이스케이프될 수 있으며, readonly 의미 체계가 실행된 후에도 변경이 허용될 수 있습니다.

특성 형식에는 다음 정의가 있습니다.

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(
        AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
        AllowMultiple = false,
        Inherited = false)]
    public sealed class UnscopedRefAttribute : Attribute
    {
    }
}

자세한 참고 사항:

  • [UnscopedRef] 주석이 추가된 인스턴스 메서드 또는 속성에는 ref-safe-contextthis호출자 컨텍스트로 설정되어 있습니다.
  • [UnscopedRef] 주석이 추가된 멤버는 인터페이스를 구현할 수 없습니다.
  • [UnscopedRef]을 사용하는 것은 오류입니다.
    • struct 선언되지 않은 멤버
    • staticinit 멤버, struct 멤버 또는 생성자
    • scoped 표시된 매개 변수
    • 값으로 전달되는 매개 변수
    • 암시적으로 범위가 지정되지 않은 참조로 전달되는 매개 변수

ScopedRefAttribute

scoped 주석은 형식 System.Runtime.CompilerServices.ScopedRefAttribute 특성을 통해 메타데이터로 내보내됩니다. 특성은 네임스페이스 정규화된 이름으로 일치하므로 정의가 특정 어셈블리에 표시되지 않아도 됩니다.

ScopedRefAttribute 형식은 컴파일러 전용이며 원본에서는 허용되지 않습니다. 컴파일에 아직 포함되지 않은 경우 형식 선언이 컴파일러에 의해 합성됩니다.

형식에는 다음 정의가 있습니다.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    internal sealed class ScopedRefAttribute : Attribute
    {
    }
}

컴파일러는 scoped 구문을 사용하여 매개 변수에서 이 특성을 내보낸다. 구문으로 인해 값이 기본 상태와 다른 경우에만 내보내됩니다. 예를 들어 scoped out는 특성이 출력되지 않도록 합니다.

RefSafetyRulesAttribute

C#7.2와 C#11 간의 참조 안전 컨텍스트 규칙에는 몇 가지 차이점이 있습니다. 이러한 차이로 인해 C#11을 사용하여 C#10 또는 이전 버전으로 컴파일된 참조에 대해 다시 컴파일할 때 호환성을 깨뜨릴 수 있는 변경이 발생할 수 있습니다.

  1. 범위가 지정되지 않은 ref/in/out 매개 변수는 C#7.2가 아닌 C#11에서 refref struct 필드로 메서드 호출을 이스케이프할 수 있습니다.
  2. out 매개 변수는 C#11에서 암시적으로 범위가 지정되고 C#7.2에서는 범위가 지정되지 않습니다.
  3. ref 형식에 대한 /inref struct 매개 변수는 C#11에서 암시적으로 범위가 지정되고 C#7.2에서는 범위가 지정되지 않습니다.

C#11을 다시 컴파일할 때 호환성이 손상되는 변경 가능성을 줄이기 위해 메서드 선언분석하는 데 사용된 규칙과 일치할 있는 메서드 호출 참조 안전 컨텍스트 규칙을 사용하도록 C#11 컴파일러를 업데이트합니다. 기본적으로 이전 컴파일러로 컴파일된 메서드에 대한 호출을 분석할 때 C#11 컴파일러는 C#7.2 ref 안전 컨텍스트 규칙을 사용합니다.

이를 사용하도록 설정하기 위해 모듈이 [module: RefSafetyRules(11)] 이상으로 컴파일되거나 -langversion:11 필드의 기능 플래그가 포함된 corlib로 컴파일될 때 컴파일러는 새 ref 특성을 내보낸다.

특성에 대한 인수는 모듈을 컴파일할 때 사용되는 ref safe 컨텍스트 규칙의 언어 버전을 나타냅니다. 컴파일러에 전달된 실제 언어 버전에 관계없이 버전은 현재 11로 고정되어 있습니다.

향후 버전의 컴파일러가 ref safe 컨텍스트 규칙을 업데이트하고 각기 다른 버전으로 특성을 내보낼 것으로 기대됩니다.

컴파일러가 이외의 를 포함하는 모듈을 로드할 때, 해당 모듈에 선언된 메서드에 대한 호출이 있으면 컴파일러는 인식할 수 없는 버전에 대해 경고를 보고합니다.

C#11 컴파일러 가 메서드 호출을 분석할 때:

  • 모듈에 메서드 선언이 [module: RefSafetyRules(version)]를 포함하고 version과 상관없이 포함된 경우, 메서드 호출은 C#11 규칙에 따라 분석됩니다.
  • 메서드 선언이 포함된 모듈이 원본에서 온 경우 -langversion:11 또는 ref 필드에 대한 기능 플래그가 포함된 corlib로 컴파일된 경우 메서드 호출은 C#11 규칙으로 분석됩니다.
  • 메서드 선언이 포함된 모듈이 System.Runtime { ver: 7.0 }참조하는 경우 메서드 호출은 C#11 규칙을 사용하여 분석됩니다. 이 규칙은 C#11/.NET 7의 이전 미리 보기로 컴파일된 모듈에 대한 임시 완화이며 나중에 제거됩니다.
  • 그렇지 않으면 C#7.2 규칙을 사용하여 메서드 호출을 분석합니다.

C#11 이전 컴파일러는 모든 RefSafetyRulesAttribute 무시하고 C#7.2 규칙만 사용하여 메서드 호출을 분석합니다.

RefSafetyRulesAttribute 네임스페이스 정규화된 이름으로 일치하므로 정의가 특정 어셈블리에 표시되지 않아도 됩니다.

RefSafetyRulesAttribute 형식은 컴파일러 전용이며 원본에서는 허용되지 않습니다. 컴파일에 아직 포함되지 않은 경우 형식 선언이 컴파일러에 의해 합성됩니다.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
    internal sealed class RefSafetyRulesAttribute : Attribute
    {
        public RefSafetyRulesAttribute(int version) { Version = version; }
        public readonly int Version;
    }
}

안전한 고정 크기 버퍼

C# 11에서는 안전한 고정 크기 버퍼가 배달되지 않았습니다. 이 기능은 이후 버전의 C#에서 구현될 수 있습니다.

언어는 안전한 코드에서 선언할 수 있고 요소 형식을 관리하거나 관리하지 않을 수 있도록 고정된 크기의 배열에 대한 제한을 완화합니다. 이렇게 하면 다음과 같은 형식이 유효합니다.

internal struct CharBuffer
{
    internal char Data[128];
}

이러한 선언은 unsafe의 대응하는 부분처럼 포함하고 있는 형식에서 N 요소의 시퀀스를 정의합니다. 이러한 멤버는 인덱서로 액세스할 수 있으며 Span<T>ReadOnlySpan<T> 인스턴스로 변환할 수도 있습니다.

fixed 형식의 T 버퍼로 인덱싱하는 경우 컨테이너의 readonly 상태를 고려해야 합니다. 컨테이너가 readonly이면 인덱서는 ref readonly T을 반환하고, 그렇지 않으면 ref T를 반환합니다.

인덱서 없이 fixed 버퍼에 액세스하는 경우 자연 형식은 없지만 Span<T> 형식으로 변환할 수 있습니다. 컨테이너가 버퍼가 암시적으로 변환할 수 , 그렇지 않으면 암시적으로 또는 변환할 수 있습니다( 변환은 더 나은간주됨).

결과 Span<T> 인스턴스의 길이는 fixed 버퍼에 선언된 크기와 같습니다. 반환된 값의 안전 컨텍스트은 백업 데이터가 필드로 액세스될 때와 마찬가지로 컨테이너의 안전 컨텍스트과 동일합니다.

형식이 fixed인 요소에 대한 각 T 선언마다, 언어는 반환 형식이 get인 해당 ref T 전용 인덱서 메서드를 생성합니다. 인덱서는 구현이 선언 형식의 필드를 반환하기 때문에 [UnscopedRef] 특성으로 주석이 달립니다. 멤버의 접근성은 fixed 필드의 접근성과 일치합니다.

예를 들어 CharBuffer.Data 인덱서의 서명은 다음과 같습니다.

[UnscopedRef] internal ref char DataIndexer(int index) => ...;

제공된 인덱스가 fixed 배열의 선언된 범위를 벗어나는 경우, IndexOutOfRangeException 예외가 던져집니다. 상수 값이 제공되는 경우 해당 요소에 대한 직접 참조로 대체됩니다. 상수가 선언된 범위를 벗어나지 않는 한 컴파일 시간 오류가 발생합니다.

fixed 버퍼에 값으로 getset 작업을 수행하는 명명된 접근자가 생성됩니다. 즉, 이는 fixed 버퍼가 ref 접근자와 byval getset 연산을 통해 기존 배열 의미 체계와 더욱 유사하게 됨을 의미합니다. 즉, 컴파일러는 배열을 사용할 때와 동일한 유연성을 가지고 fixed 버퍼를 사용하는 코드를 생성할 수 있습니다. 이렇게 하면 await 버퍼에 대한 fixed 같은 작업을 더 쉽게 내보낸다.

또한 fixed 버퍼를 다른 언어에서 더 쉽게 사용할 수 있다는 이점이 추가되었습니다. 명명된 인덱서는 .NET의 1.0 릴리스 이후 존재했던 기능입니다. 명명된 인덱서를 직접 지원하지 않는 언어도 일반적으로 이를 사용하거나 소비할 수 있습니다(C#이 실제 예시입니다).

버퍼에 대한 백업 스토리지는 [InlineArray] 특성을 사용하여 생성됩니다. 이는 문제 12320 설명된 메커니즘으로, 특히 동일한 형식의 필드 시퀀스를 효율적으로 선언하는 경우를 허용합니다. 이 특정 문제는 아직 활발히 논의 중이며 이 기능의 구현은 논의가 진행되는 동안 따를 것이라는 기대가 있습니다.

refnew 표현식 내에서 with 값을 가진 초기화자

12.8.17.3 개체 이니셜라이저섹션에서 문법을 다음으로 업데이트합니다.

initializer_value
    : 'ref' expression // added
    | expression
    | object_or_collection_initializer
    ;

with 표현섹션에서 문법을 다음과 같이 업데이트합니다.

member_initializer
    : identifier '=' 'ref' expression // added
    | identifier '=' expression
    ;

할당의 왼쪽 피연산자는 ref 필드에 연결되는 식이어야 합니다.
오른쪽 피연산자는 왼쪽 피연산자와 동일한 형식의 값을 지정하는 lvalue를 생성하는 식이어야 합니다.

참조 로컬 재할당에 유사한 규칙을 추가합니다.
왼쪽 피연산자는 쓰기 가능한 참조(즉, ref readonly 필드 이외의 항목을 지정함)인 경우 오른쪽 피연산자는 쓰기 가능한 lvalue여야 합니다.

생성자 호출의 이스케이프 규칙은 유지됩니다.

생성자를 호출하는 new 식은 생성되는 형식을 반환하는 것으로 간주되는 메서드 호출과 동일한 규칙을 준수합니다.

즉, 위에서 업데이트된 메서드 호출 규칙입니다.

메서드 호출 e1.M(e2, ...)에서 발생하는 rvalue는 다음 컨텍스트 중 가장 작은 컨텍스트에 있는 안전 컨텍스트를 가집니다.

  1. 호출자 컨텍스트
  2. 모든 인수 식에 의해 제공되는 안전 컨텍스트
  3. 반환이 ref struct일 때 모든 인수가 기여한 ref

이니셜라이저가 있는 new 표현식의 경우, 이니셜라이저 표현식들은 인수로 간주되어 그들의 안전 컨텍스트에 기여합니다. 또한 ref 이니셜라이저 표현식은 ref 인수로도 간주되어 그들의 참조 안전 컨텍스트에 기여하며, 재귀적으로 처리됩니다.

안전하지 않은 컨텍스트의 변경 내용

포인터 형식(섹션 23.3)은 관리되는 형식을 참조 형식으로 허용하도록 확장됩니다. 이러한 포인터 유형은 관리되는 형식 뒤에 * 토큰을 붙여서 작성됩니다. 경고를 생성합니다.

address-of 연산자(섹션 23.6.5)는 관리되는 형식의 변수를 피연산자로 허용하도록 완화됩니다.

fixed 문(섹션 23.7)은 관리되는 형식 변수의 주소이거나 관리되는 형식 T요소가 있는 array_type 식인 T 허용하도록 완화됩니다.

스택 할당 이니셜라이저(섹션 12.8.22)는 비슷하게 완화됩니다.

고려 사항

이 기능을 평가할 때 개발 스택의 다른 부분에서 고려해야 할 고려 사항이 있습니다.

호환성 고려 사항

이 제안의 도전 과제는 이 디자인이 기존 범위 안전 규칙또는 §9.7.2와의 호환성에 미치는 영향입니다. 이러한 규칙은 ref structref 필드를 가질 수 있는 개념을 완전히 지원하지만, stackalloc를 제외한 다른 API에서는 스택을 참조하는 ref 상태를 캡처하는 것이 허용되지 않습니다. ref safe 컨텍스트 규칙에는 양식의 생성자가 존재하지 않는다는 하드 가정또는 §16.4.12.8 있습니다. 즉, 안전 규칙은 ref 필드로 이스케이프할 수 있는 ref 매개 변수를 고려하지 않으므로 다음과 같은 코드를 허용합니다.

Span<int> CreateSpanOfInt()
{
    // This is legal according to the 7.2 span rules because they do not account
    // for a constructor in the form Span(ref T value) existing. 
    int local = 42;
    return new Span<int>(ref local);
}

실제로 ref 매개 변수가 메서드 호출에서 이스케이프되는 세 가지 방법이 있습니다.

  1. 값에 의한 반환
  2. ref 반환 기준
  3. refref struct 필드가 ref / out 매개 변수로 반환되거나 전달됨으로써

기존 규칙은 (1) 및 (2)만 고려합니다. (3)를 고려하지 않으므로 ref 필드가 고려되지 않으므로 지역 주민을 반환하는 것과 같은 간격이 고려되지 않습니다. 이 디자인은 (3)을 고려하도록 규칙을 변경해야 합니다. 이는 기존 API의 호환성에 작은 영향을 줍니다. 특히 다음과 같은 속성이 있는 API에 영향을 줍니다.

  • 서명에 ref struct이(가) 포함되어 있습니다.
    • ref struct 반환 형식, ref 또는 out 매개 변수인 경우
    • 수신기를 제외한 추가 in 또는 ref 매개 변수가 있습니다.

C# 10에서 이러한 API의 호출자는 API에 대한 ref 상태 입력을 ref 필드로 캡처할 수 있다고 생각할 필요가 없었습니다. 따라서 ref 상태가 ref 필드로 이스케이프할 수 있는 가능성 때문에 C# 10에서 안전하게 존재하던 여러 패턴이 C# 11에서는 안전하지 않게 됩니다. 예를 들어:

Span<int> CreateSpan(ref int parameter)
{
    // The implementation of this method is irrelevant when considering the lifetime of the 
    // returned Span<T>. The ref safe context rules only look at the method signature, not the 
    // implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
    // to escape by ref in this method
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 but would be illegal with ref fields
    return CreateSpan(ref parameter);

    // Legal in C# 10 but would be illegal with ref fields
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 but would be illegal with ref fields
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

이 호환성 중단의 영향은 매우 작을 것으로 예상됩니다. 영향을 받는 API 셰이프는 ref 필드가 없으면 거의 의미가 없으므로 고객이 이 중 많은 부분을 만들 가능성은 거의 없습니다. 이 API 형태를 찾기 위한 도구 실행 실험은 기존 리포지토리에서 해당 주장을 뒷받침합니다. 이 셰이프의 개수가 많은 리포지토리는 dotnet/runtime 리포지토리뿐이며, 이는 리포지토리가 ref 내장 형식을 통해 ByReference<T> 필드를 만들 수 있기 때문입니다.

그럼에도 불구하고 디자인은 일반적인 패턴이 아니라 유효한 패턴을 표현하기 때문에 기존 API를 고려해야 합니다. 따라서 디자인은 개발자에게 C# 10으로 업그레이드할 때 기존 수명 규칙을 복원할 수 있는 도구를 제공해야 합니다. 특히 ref 매개 변수가 ref 또는 ref 필드에서 벗어날 수 없도록 개발자가 이를 주석으로 표시할 수 있는 메커니즘을 반드시 제공해야 합니다. 이를 통해 고객은 C# 10 호출 사이트 규칙이 동일한 C# 11에서 API를 정의할 수 있습니다.

참조 어셈블리

이 제안에 따라 설명된 기능을 사용하는 컴파일에 대한 참조 어셈블리는 안전한 참조 컨텍스트 정보를 전달하는 요소를 유지 관리해야 합니다. 즉, 모든 수명 애노테이션 속성은 원래 위치에 보존되어야 합니다. 해당 어셈블리를 바꾸거나 생략하려고 하면 참조 어셈블리가 잘못 될 수 있습니다.

ref 필드를 표현하는 것은 좀 더 정교합니다. ref 필드는 다른 필드와 마찬가지로 참조 어셈블리에 표시되는 것이 이상적입니다. 그러나 ref 필드는 메타데이터 형식의 변경을 나타내며, 이로 인해 이 메타데이터 변경을 이해하기 위해 업데이트되지 않은 도구 체인에 문제가 발생할 수 있습니다. 구체적인 예는 C++/CLI로, ref 필드를 사용하는 경우 오류가 발생할 수 있습니다. 따라서 핵심 라이브러리의 참조 어셈블리에서 ref 필드를 생략할 수 있으면 유리합니다.

ref 필드 자체는 참조 안전 컨텍스트 규칙에 영향을 주지 않습니다. 기존 Span<T> 정의를 ref 필드로 전환하는 것이 사용량에 아무런 영향을 주지 않는 구체적인 예를 고려해 보십시오. 따라서 ref 자체는 안전하게 생략할 수 있습니다. 그러나 ref 필드는 보존해야 하는 소비에 다른 영향을 미치는 요소를 포함합니다.

  • ref struct 필드가 있는 ref는 절대 unmanaged로 간주되지 않습니다.
  • ref 필드의 형식은 무한 제네릭 확장 규칙에 영향을 줍니다. 따라서 ref 필드의 형식에 보존해야 하는 형식 매개 변수가 포함된 경우

다음 규칙에 따라 ref struct에 대한 유효한 참조 어셈블리의 변환입니다.

// Impl assembly 
ref struct S<T>
{
    ref T _field;
}

// Ref assembly 
ref struct S<T>
{
    object _o; // force managed 
    T _f; // maintain generic expansion protections
}

주석

수명은 형식을 사용하여 가장 자연스럽게 표현됩니다. 지정된 프로그램의 수명은 수명 형식이 확인되면 안전합니다. C#의 구문은 수명을 값에 암시적으로 추가하지만 여기에 기본 규칙을 설명하는 기본 형식 시스템이 있습니다. 이러한 규칙의 관점에서 디자인 변경의 의미를 논의하는 것이 더 쉬울 수 있으므로 토론을 위해 여기에 포함됩니다.

이것은 100% 완전한 문서가 아님을 주의하세요. 여기서는 모든 단일 동작을 문서화하는 것이 목표가 아닙니다. 대신, 이를 통해 모델과 그 잠재적인 변경 사항을 논의할 수 있는 일반적인 이해와 공통적인 용어를 확립하려는 것입니다.

일반적으로 수명 유형에 대해 직접 이야기할 필요는 없습니다. 예외는 특정 "인스턴스화" 사이트에 따라 수명이 달라질 수 있는 위치입니다. 이것은 일종의 다형성이며 이러한 다양한 수명을 제네릭 매개 변수로 표현된 "제네릭 수명"이라고 합니다. C#은 수명 제네릭을 표현하는 구문을 제공하지 않기 때문에, C#에서 명시적 제네릭 매개변수를 포함하는 확장된 하위 언어로의 암시적인 "변환"을 정의합니다.

아래 예제에서는 명명된 수명을 사용합니다. 구문 $aa이라는 수명을 나타냅니다. 그 스스로는 의미가 없지만 where $a : $b 구문을 통해 다른 삶들과 관계를 맺을 수 있는 삶입니다. $a이(가) $b으로 변환 가능함을 확인합니다. 이것을 $a이 적어도 $b만큼의 수명 기간임을 확립하는 것으로 생각하면 도움이 될 수 있습니다.

아래에는 편의성과 간결성을 위해 미리 정의된 몇 가지 수명이 있습니다.

  • $heap: 힙에 존재하는 모든 값의 수명입니다. 모든 컨텍스트 및 메서드 서명에서 사용할 수 있습니다.
  • $local: 메서드 스택에 있는 모든 값의 수명입니다. 함수 멤버에 대한 효과적인 이름 자리 표시자입니다. 메서드에서 암시적으로 정의되며 출력 위치를 제외한 메서드 서명에 표시할 수 있습니다.
  • $ro: 의 반환용 이름 자리 표시자만
  • $cm: 호출자 컨텍스트 대한 이름 자리 표시자

수명 사이에는 몇 가지 미리 정의된 관계가 있습니다.

  • where $heap : $a 모든 수명에 대한 $a
  • where $cm : $ro
  • 미리 정의된 모든 수명에 대한 where $x : $local. 사용자 정의 수명은 명시적으로 정의되지 않는 한 로컬과 아무런 관계가 없습니다.

형식에 정의된 수명 변수는 고정 또는 공변일 수 있습니다. 이러한 구문은 제네릭 매개 변수와 동일한 구문을 사용하여 표현됩니다.

// $this is covariant
// $a is invariant
ref struct S<out $this, $a> 

형식 정의에서 수명 매개 변수는 미리 정의된 없지만 정의될 때 연결된 몇 가지 규칙이 있습니다.

  • 첫 번째 수명 매개 변수여야 합니다.
  • 공변성이어야 합니다. out $this.
  • ref 필드의 수명은 $this의 수명으로 변환될 수 있어야 합니다.
  • 모든 비-ref 필드의 $this 수명은 $heap 또는 $this이어야 합니다.

ref의 수명은 ref에 수명 인수를 제공하여 나타냅니다. 예를 들어, 힙을 참조하는 ref은/는 ref<$heap>로 나타냅니다.

모델에서 생성자를 정의할 때 new 이름이 메서드에 사용됩니다. 반환된 값에 대한 매개 변수 목록과 생성자 인수가 있어야 합니다. 생성자 입력과 생성된 값 간의 관계를 표현하는 데 필요합니다. 모델은 Span<$a><$ro> 대신 Span<$a> new<$ro>을 사용합니다. 생성자에서 수명을 포함한 this의 형식은 정의된 반환 값으로 결정됩니다.

수명에 대한 기본 규칙은 다음과 같이 정의됩니다.

  • 모든 라이프타임은 형식 인수 앞에 오는 제네릭 인수로 문법적으로 표현됩니다. 이는 $heap$local제외하고 미리 정의된 수명에 해당합니다.
  • T이 아닌 모든 ref struct 형식은 암시적으로 수명이 T<$heap>입니다. 암시적이므로 모든 샘플에서 int<$heap> 작성할 필요가 없습니다.
  • ref으로 정의된 ref<$l0> T<$l1, $l2, ... $ln> 필드의 경우:
    • $l1 부터 $ln 이하의 모든 수명은 불변이어야 합니다.
    • $l0의 수명이 $this로 변환 가능해야 합니다.
  • refref<$a> T<$b, ...>로 정의된 경우, $b$a로 변환할 수 있어야 합니다.
  • 변수의 ref 다음과 같이 정의된 수명이 있습니다.
    • ref의 로컬, 매개변수, 필드 또는 반환의 경우, ref<$a> T 형식의 수명은 $a입니다.
    • $heap는 모든 참조 형식과 그 필드에 해당합니다.
    • 다른 모든 항목에 대한 $local
  • 기본 형식 변환이 합법적인 경우 할당 또는 반환이 적법합니다.
  • 캐스트 주석을 사용하여 표현식의 수명을 명시적으로 지정할 수 있습니다.
    • (T<$a> expr) 값 수명이 명시적으로 $aT<...>을 위해 설정됩니다.
    • 값 수명이 ref<$a> (T<$b>)expr이고 $b에 대한 수명은 T<...>이며 참조 수명은 $a입니다.

수명 규칙의 목적을 위해 ref은 변환을 위한 식의 유형의 일부로 간주됩니다. ref<$a> T<...>가 공변적이고 ref<$a, T<...>>가 불변적일 때 $aT로 변환하여 논리적으로 표현됩니다.

다음으로 C# 구문을 기본 모델에 매핑할 수 있는 규칙을 정의해 보겠습니다.

간단히 하기 위해 명시적 수명 매개 변수가 없는 형식은 out $this 정의되어 형식의 모든 필드에 적용되는 것처럼 처리됩니다. ref 필드가 있는 형식은 명시적 수명 매개 변수를 정의해야 합니다.

이러한 규칙은 모든 형식에 대해 Tscoped T에 할당할 수 있다는 기존의 불변식을 지원하기 위해 존재합니다. T<$a, ...>T<$local, ...>로 변환할 수 있는 모든 수명에 대해 $local에 할당될 수 있도록 매핑됩니다. 힙에서 가져온 Span<T>을 스택에 있는 항목에 할당할 수 있는 것과 같은 다른 기능들을 지원합니다. ref가 아닌 값에 대해 필드의 수명이 다른 유형은 제외되지만, 이는 오늘날 C#의 현실입니다. 이를 변경하려면, 계획을 세워야 할 C# 규칙의 중대한 변경이 필요합니다.

인스턴스 메서드 내의 this 형식에 대한 S<out $this, ...> 형식은 다음과 같이 암시적으로 정의됩니다.

  • 일반 인스턴스 메서드의 경우: ref<$local> S<$cm, ...>
  • [UnscopedRef]주석이 추가된 인스턴스 메서드의 경우: ref<$ro> S<$cm, ...>

명시적 this 매개 변수가 없을 경우 여기서 암시적 규칙이 강제로 발생합니다. 복잡한 샘플 및 토론의 경우 static 메서드로 작성하고 this 명시적 매개 변수로 만드는 것이 좋습니다.

ref struct S<out $this>
{
    // Implicit this can make discussion confusing 
    void M<$ro, $cm>(ref<$ro> S<$cm> s) {  }

    // Rewrite as explicit this to simplify discussion
    static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}

C# 메서드 구문은 다음과 같은 방법으로 모델에 매핑됩니다.

  • ref 매개 변수의 참조 수명은 $ro
  • ref struct 형식의 매개 변수는 $cm의 수명을 가진다.
  • ref 반환의 ref 수명은 $ro
  • ref struct 유형의 반환 값은 $ro의 수명을 가집니다.
  • 매개 변수 scoped 또는 ref이 참조 수명을 $local로 변경합니다.

여기서 모델을 보여 주는 간단한 예제를 살펴보겠습니다.

ref int M1(ref int i) => ...

// Maps to the following. 

ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
    // okay: has ref lifetime $ro which is equal to $ro
    return ref i;

    // okay: has ref lifetime $heap which convertible $ro
    int[] array = new int[42];
    return ref array[0];

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

이제 ref struct사용하여 동일한 예제를 살펴보겠습니다.

ref struct S
{
    ref int Field;

    S(ref int f)
    {
        Field = ref f;
    }
}

S M2(ref int i, S span1, scoped S span2) => ...

// Maps to 

ref struct S<out $this>
{
    // Implicitly 
    ref<$this> int Field;

    S<$ro> new<$ro>(ref<$ro> int f)
    {
        Field = ref f;
    }
}

S<$ro> M2<$ro>(
    ref<$ro> int i,
    S<$ro> span1)
    S<$local> span2)
{
    // okay: types match exactly
    return span1;

    // error: has lifetime $local which has no conversion to $ro
    return span2;

    // okay: type S<$heap> has a conversion to S<$ro> because $heap has a
    // conversion to $ro and the first lifetime parameter of S<> is covariant
    return default(S<$heap>)

    // okay: the ref lifetime of ref $i is $ro so this is just an 
    // identity conversion
    S<$ro> local = new S<$ro>(ref $i);
    return local;

    int[] array = new int[42];
    // okay: S<$heap> is convertible to S<$ro>
    return new S<$heap>(ref<$heap> array[0]);

    // okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These 
    // are convertible.
    return new S<$ro>(ref<$heap> array[0]);

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

다음으로 순환적인 자체 할당 문제를 어떻게 해결하는지 살펴보겠습니다.

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        s.refField = ref s.field;
    }
}

// Maps to 

ref struct S<out $this>
{
    int field;
    ref<$this> int refField;

    static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
    {
        // error: the types work out here to ref<$cm> int = ref<$ro> int and that is 
        // illegal as $ro has no conversion to $cm (the relationship is the other direction)
        s.refField = ref<$ro> s.field;
    }
}

다음으로 이 방법이 잘못된 캡처 매개 변수 문제에 어떻게 도움이 되는지 살펴보겠습니다.

ref struct S
{
    ref int refField;

    void Use(ref int parameter)
    {
        // error: this needs to be an error else every call to this.Use(ref local) would fail 
        // because compiler would assume the `ref` was captured by ref.
        this.refField = ref parameter;
    }
}

// Maps to 

ref struct S<out $this>
{
    ref<$this> int refField;
    
    // Using static form of this method signature so the type of this is explicit. 
    static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
    {
        // error: the types here are:
        //  - refField is ref<$cm> int
        //  - ref parameter is ref<$ro> int
        // That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and 
        // hence this reassignment is illegal
        @this.refField = ref<$ro> parameter;
    }
}

열려 있는 문제

호환성 문제가 발생하지 않도록 디자인을 변경하십시오.

이 디자인은 기존 ref-safe-context 규칙과의 호환성을 차단하는 몇 가지를 제안합니다. 중단이 최소한의 영향을 미치는 것으로 여겨지지만 주요 변경 사항이 없는 디자인에 상당한 고려가 주어졌습니다.

그러나 호환 유지 디자인은 이 디자인보다 훨씬 더 복잡했습니다. 호환성을 유지하기 위해, ref 필드는 refref 필드로 반환하는 기능을 위해 각기 다른 수명이 필요합니다. 기본적으로 모든 매개 변수에 대해 ref-field-safe-context 추적을 메서드에 제공해야 합니다. 이는 모든 식에 대해 계산되어야 하며, 현재 추적되는 ref-safe-context 거의 모든 위치에서 모든 값에서 추적되어야 합니다.

이 외에도 이 값은 ref-safe-context관계가 있습니다. 예를 들어, 값을 반환할 수 있는 경우 ref 필드로는 가능하지만 ref으로는 직접 반환할 수 없는 것은 불합리합니다. ref 필드는 이미 ref에 의해 쉽게 반환될 수 있기 때문입니다(비록 포함된 값을 반환할 수 없는 경우에도 ref에 의해 ref struct에서ref 상태를 반환할 수 있습니다). 따라서 규칙은 이러한 값이 서로 관련하여 합리적인지 확인하기 위해 지속적인 조정이 필요합니다.

또한 언어는 ref 필드, ref 및 값으로 세 가지 방법으로 반환될 수 있는 ref 매개 변수를 나타내는 구문이 필요하다는 것을 의미합니다. 기본값은 ref에 의해 반환 가능합니다. 앞으로 특히 ref struct가 관련된 경우 더 자연스러운 반환은 ref 필드나 ref에 의해 예상됩니다. 즉, 새 API는 기본적으로 올바르게 하기 위해 추가적인 문법 주석이 필요합니다. 이것은 바람직하지 않습니다.

하지만 이러한 호환성이 변경되면 다음 속성이 있는 메서드에 영향을 줍니다.

  • Span<T> 또는 ref struct
    • ref struct 반환 형식, ref 또는 out 매개 변수인 경우
    • 추가 in 또는 ref 매개 변수가 있습니다(수신기 제외).

영향을 이해하려면 API를 범주로 분리하는 것이 유용합니다.

  1. 소비자가 refref 필드로 캡처되는 것을 고려하도록 하고 싶습니다. 대표적인 예는 Span(ref T value) 생성자입니다.
  2. 소비자들이 refref 필드로 캡처되는 것을 고려하지 않도록 합니다. 이러한 항목은 두 범주로 구분됩니다.
    1. 안전하지 않은 API. 이들은 UnsafeMemoryMarshal 형식 내의 API이며, 그 중 MemoryMarshal.CreateSpan 가장 두드러집니다. 이러한 API는 ref를 안전하게 캡처하지 않지만, 불안전한 API로도 잘 알려져 있습니다.
    2. 안전한 API. 효율성을 위해 ref 매개 변수를 사용하는 API이지만 실제로는 캡처되지 않습니다. 예는 작고 하나가 AsnDecoder.ReadEnumeratedBytes입니다.

이 변경은 주로 (1)에 이익을 줍니다. 이 API들이 앞으로 ref을 받아들여 ref struct을 반환하는 대부분의 API를 구성할 것으로 예상됩니다. 수명 규칙이 변경되어 기존 호출 의미 체계를 중단시키므로, 이는 (2.1)과 (2.2)에 부정적인 영향을 미칩니다.

범주(2.1)의 API는 주로 Microsoft 또는 ref 필드(세계 태너)의 혜택을 가장 많이 받는 개발자가 작성합니다. C# 11로 업그레이드할 때 ref 필드가 제공된다면, 기존 의미 체계를 유지하기 위한 몇 가지 주석 형태로 이루어지는 호환성 세금에 이 개발자 클래스가 동의할 가능성이 있다는 것은 합리적인 추측입니다.

범주(2.2)의 API가 가장 큰 문제입니다. 이러한 API가 얼마나 많은지 알 수 없으며 타사 코드에서 이러한 API가 더 많거나 덜 빈번할지는 불분명합니다. 그들 중 매우 적은 수가 있을 것으로 기대하며, 특히 우리가 out호환성을 깰 경우에는 더욱 그렇다. 지금까지의 탐색은 public 표면적에서 매우 적은 수의 존재를 발견했다. 의미 체계 분석이 필요하므로 검색하기 어려운 패턴입니다. 이 변경을 수행하기 전에 소수의 알려진 사례에 영향을 미치는 이와 관련된 가정을 확인하기 위해 도구 기반 접근 방식이 필요합니다.

범주(2)의 두 경우 모두 수정 사항이 바로 진행됩니다. 캡처 가능으로 간주하지 않으려는 ref 매개 변수는 scopedref을 추가해야 합니다. (2.1)에서는 개발자가 Unsafe 또는 MemoryMarshal을 강제로 사용하게 될 가능성이 높지만, 이는 안전하지 않은 스타일 API에서는 예상되는 일입니다.

이상적으로는 API가 눈에 띄지 않게 문제 있는 동작으로 빠질 때 경고를 제공함으로써 예기치 못한 비호환성 문제의 영향을 줄일 수 있습니다. ref을 받아들이고 ref struct을 반환하지만 실제로 ref에서 ref struct를 캡처하지 않는 메서드입니다. 이 경우 컴파일러는 개발자에게 이러한 ref 대신 scoped ref 주석을 추가해야 한다는 것을 알리는 진단을 내릴 수 있습니다.

의사 결정 이 설계는 달성할 수 있지만 결과 기능은 호환성 중단을 위해 결정된 시점까지 사용하기가 더 어렵습니다.

의사 결정 컴파일러는 메서드가 조건을 충족하지만 ref 매개 변수를 ref 필드로 캡처하지 않을 때 경고를 제공합니다. 고객이 업그레이드 시 발생할 수 있는 잠재적인 문제에 대해 적절하게 경고받아야 합니다.

키워드 및 특성

이 디자인은 특성을 사용하여 새 수명 규칙에 주석을 달도록 되어 있습니다. 컨텍스트 키워드를 사용하여 쉽게 수행할 수도 있습니다. 예를 들어 [DoesNotEscape]은(는) scoped로 매핑될 수 있습니다. 그러나 컨텍스트 키워드조차도 일반적으로 포함을 위해 매우 높은 막대를 충족해야 합니다. 그것들은 언어에서 중요한 위치를 차지하며 언어의 더 눈에 띄는 부분입니다. 이 기능은 중요하지만 소수의 C# 개발자에게 서비스를 제공할 것입니다.

겉으로 보기에는 키워드를 사용하지 않는 것이 좋지만 고려해야 할 두 가지 중요한 사항이 있습니다.

  1. 주석은 프로그램 의미 체계에 영향을 줍니다. 특성이 프로그램 의미 체계에 영향을 미치는 것은 C#이 넘기 꺼리는 경계이며, 이 기능이 그 단계를 정당화하는지 여부는 불분명합니다.
  2. 이 기능을 사용할 가능성이 가장 큰 개발자는 함수 포인터를 사용하는 개발자 집합과 강력하게 교차합니다. 이 기능은 소수의 개발자가 사용하더라도 새로운 구문 도입이 정당화되었으며, 그 결정은 여전히 타당하다고 평가됩니다.

이를 종합하면 구문을 고려해야 합니다.

구문의 대략적인 스케치는 다음과 같습니다.

  • [RefDoesNotEscape]이(가) scoped ref에 매핑된다.
  • [DoesNotEscape]이(가) scoped에 매핑된다.
  • [RefDoesEscape]이(가) unscoped에 매핑된다.

의사 결정scoped 구문 사용 및 scoped ref; unscoped특성을 사용합니다.

고정 버퍼 로컬 허용

이 디자인은 모든 형식을 지원할 수 있는 안전한 fixed 버퍼를 허용합니다. 여기서 가능한 확장 중 하나는 이러한 fixed 버퍼를 지역 변수로 선언할 수 있도록 하는 것입니다. 이렇게 하면 여러 기존 stackalloc 작업을 fixed 버퍼로 바꿀 수 있습니다. 또한 stackalloc 관리되지 않는 요소 형식으로 제한되지만 fixed 버퍼는 그렇지 않으므로 스택 스타일 할당이 있을 수 있는 시나리오 집합을 확장합니다.

class FixedBufferLocals
{
    void Example()
    {
        Span<int> span = stackalloc int[42];
        int buffer[42];
    }
}

이는 함께 유지되지만 로컬의 구문을 약간 확장해야 합니다. 이것이 추가 복잡성의 가치가 있는지 여부는 불분명합니다. 가능한 우리는 지금 아니오를 결정하고 충분한 필요성이 입증되면 나중에 다시 가져올 수 있습니다.

도움이 되는 위치의 예: https://github.com/dotnet/runtime/pull/34149

결정은 잠시 보류

modreqs를 사용할지 말지

새 수명 특성으로 표시된 메서드를 modreq으로 변환할지 말지를 결정해야 합니다. 이 방법을 사용하는 경우 주석과 modreq 간에 1:1 매핑이 적용됩니다.

modreq를 추가하는 근거는 속성이 참조 안전 컨텍스트 규칙의 의미를 변경하기 위한 것입니다. 이러한 의미 체계를 이해하는 언어만 해당 메서드를 호출해야 합니다. OHI 시나리오에 적용할 경우 수명이 모든 파생 메서드가 반드시 구현해야 하는 계약으로 간주됩니다. 주석이 modreq 없이 존재하면 수명 주석이 상충하는 virtual 메서드 체인이 로드되는 상황이 발생할 수 있습니다. 이는 virtual 체인의 한 부분만 컴파일되고 다른 부분은 컴파일되지 않은 경우에 발생할 수 있습니다.

초기 ref 안전 컨텍스트 작업은 modreq를 사용하지 않고 언어와 프레임워크에 의존하여 이해하려고 했습니다. 동시에 ref 안전 컨텍스트 규칙에 기여하는 모든 요소는 메서드 서명의 강력한 부분인 ref, in, ref struct등입니다. 따라서 메서드의 기존 규칙을 변경하면 서명에 대한 이진 변경이 이미 발생합니다. 새 수명 주석에 동일한 영향을 주려면 modreq 적용이 필요합니다.

문제는 이것이 과잉인지 여부입니다. 서명을 보다 유연하게 만드는 것이, 예를 들어 매개 변수에 [DoesNotEscape]을 추가함으로써, 이진 호환성 변화라는 부정적인 영향을 초래합니다. 그러한 절충안은 시간이 흐르면서 BCL과 같은 프레임워크가 이러한 서명을 완화할 수 없게 될 것임을 의미합니다. 언어가 in 매개 변수에서 수행하는 몇 가지 접근 방식을 취하고 가상 위치에만 modreq 적용하여 어느 정도 완화할 수 있습니다.

의사 결정 메타데이터에 modreq 사용하지 마세요. outref의 차이점은 modreq가 있는 것이 아니라 이제 참조 컨텍스트 안전 값이 다릅니다. 여기에 modreq 사용하여 규칙을 절반만 적용하는 것은 실질적인 이점이 없습니다.

다차원 고정 버퍼 허용

다차원 스타일 배열을 포함하도록 fixed 버퍼의 디자인을 확장해야 하나요? 기본적으로 다음과 같은 선언을 허용합니다.

struct Dimensions
{
    int array[42, 13];
}

의사 결정 지금은 허용하지 않음

범위 위반

런타임 리포지토리에는 ref 매개 변수를 ref 필드로 캡처하는 여러 비공용 API가 있습니다. 결과 값의 수명이 추적되지 않으므로 안전하지 않습니다. 예를 들어 Span<T>(ref T value, int length) 생성자입니다.

이들 API의 대부분은 C# 11로 업데이트함으로써 반환에 대한 적절한 수명 추적을 선택할 가능성이 높습니다. 그러나 일부에서는 전체 의도가 안전하지 않기 때문에 반환 값을 추적하지 않는 현재 의미 체계를 유지하려고 합니다. 가장 주목할 만한 예는 MemoryMarshal.CreateSpanMemoryMarshal.CreateReadOnlySpan. 매개 변수를 scoped표시하여 이 작업을 수행합니다.

즉, 런타임에서는 매개 변수에서 scoped을 안전하지 않은 방식으로 제거하기 위해 정해진 패턴이 필요합니다.

  1. Unsafe.AsRef<T>(in T value)scoped in T value로 변경하여 기존 용도를 확장할 수 있습니다. 이렇게 하면 매개 변수에서 inscoped 모두 제거할 수 있습니다. 그런 다음 보편적인 "ref 안전을 제거하는" 메서드가 됩니다.
  2. scoped을 제거하는 데 전적으로 사용되는 새로운 방법을 소개합니다: ref T Unsafe.AsUnscoped<T>(scoped in T value). 이렇게 하면 in도 제거됩니다. 그렇지 않다면 호출자는 기존 솔루션이 아마도 충분히 될 수 있는 지점에서 "ref 안전 제거"를 위한 여러 메서드 호출을 여전히 조합해야 하기 때문입니다.

기본적으로 이 범위가 지정되지 않은 경우

기본적으로 이 디자인에는 위치가 두 군데 있으며, 그 위치는 scoped으로 설정되어 있습니다.

  • this은/는 scoped ref입니다
  • out은/는 scoped ref입니다

out 결정은 ref 필드의 호환성 부담을 크게 줄이는 동시에 더 자연스러운 기본값입니다. 개발자는 out를 외부로만 흐르는 데이터로 실제로 생각할 수 있으며, ref인 경우에는 양방향으로 데이터를 고려해야 하는 규칙을 염두에 두어야 합니다. 이로 인해 상당한 개발자 혼란이 발생합니다.

this 결정은 structref필드를 반환할 수 없음을 의미하기 때문에 바람직하지 않습니다. 이 시나리오는 높은 성능 개발자에게 중요한 시나리오이며, 이 시나리오에서는 기본적으로 [UnscopedRef] 특성이 추가되었습니다.

키워드는 기준이 높아서 단일 시나리오에 추가하는 것은 의심스럽습니다. 그에 따라 this을 기본적으로 단순히 ref으로 만들고 scoped ref가 되지 않도록 하여 이 키워드를 전혀 피할 수 있는지에 대해 생각이 주어졌습니다. 모든 멤버가 thisscoped ref로 필요로 하는 경우, 메서드에 scoped을 표시함으로써 수행할 수 있습니다(메서드를 오늘날 readonly로 표시하여 readonly ref를 만들 수 있습니다).

보통의 struct 상황에서는 멤버가 ref 반환을 가질 때만 호환성 문제가 발생하므로, 이는 대부분 긍정적인 변화입니다. 이러한 메서드는 매우 적으며, 도구가 이를 발견하여 신속하게 scoped 멤버로 변환할 수 있습니다.

ref struct의 경우, 이 변경은 훨씬 더 큰 호환성 문제를 야기합니다. 다음을 고려합니다.

ref struct Sneaky
{
    int Field;
    ref int RefField;

    public void SelfAssign()
    {
        // This pattern of ref reassign to fields on this inside instance methods would now
        // completely legal.
        RefField = ref Field;
    }

    static Sneaky UseExample()
    {
        Sneaky local = default;

        // Error: this is illegal, and must be illegal, by our existing rules as the 
        // ref-safe-context of local is now an input into method arguments must match. 
        local.SelfAssign();

        // This would be dangerous as local now has a dangerous `ref` but the above 
        // prevents us from getting here.
        return local;
    }
}

근본적으로 변경 가능한ref struct 로컬에 대한 모든 인스턴스 메서드 호출은 로컬이 scoped로 추가로 표시되지 않는 한 허용되지 않습니다. 규칙은 필드가 this다른 필드에 다시 할당된 경우를 고려해야 합니다. readonly ref structreadonly 특성이 변수 재할당을 방지하므로 이런 문제가 없습니다. 그러나 이는 거의 모든 기존 변경 가능한 ref struct영향을 주기 때문에 중요한 백컴퓨트 호환성이 손상되는 변경이 될 것입니다.

하지만 readonly ref struct 필드를 ref로 확장하면 ref struct에는 여전히 문제가 있습니다. ref 필드 값으로 캡처를 이동하면 동일한 근본적인 문제가 발생할 수 있습니다.

readonly ref struct ReadOnlySneaky
{
    readonly int Field;
    readonly ref ReadOnlySpan<int> Span;

    public void SelfAssign()
    {
        // Instance method captures a ref to itself
        Span = new ReadOnlySpan<int>(ref Field, 1);
    }
}

일부 고려가 thisstruct 또는 멤버 유형에 따라 다른 기본값을 갖도록 한다는 아이디어에 주어졌다. 예를 들어:

  • this as ref: struct, readonly ref struct 또는 readonly member
  • thisscoped ref로: ref struct 또는 readonly ref struct에서 ref 필드를 ref struct

이렇게 하면 호환성 문제를 최소화하고 유연성을 극대화할 수 있지만, 고객에게 혼란을 줄 수 있습니다. 또한 안전한 fixed 버퍼와 같은 향후 기능은 변경 가능한 ref struct이 이 디자인만으로는 작동하지 않는 필드에 대해 ref 반환을 필요로 하므로, 이는 scoped ref 범주에 속하게 되어 문제를 완전히 해결하지 못합니다.

의사 결정 유지 thisscoped ref로 합니다. 즉, 이전의 비정상 예제에서는 컴파일러 오류가 발생합니다.

ref 필드를 ref 구조체로 변경

이 기능은 ref 필드가 ref struct을 참조할 수 있도록 허용하기 때문에 새로운 참조 안전 컨텍스트 규칙 집합을 엽니다. 이러한 일반적인 특성을 가진 ByReference<T> 때문에 지금까지 런타임에서 그런 구문을 가질 수 없었습니다. 결과적으로 모든 규칙은 이것이 불가능하다는 가정 하에 작성됩니다. ref 필드 기능은 주로 새 규칙을 만드는 것이 아니라 시스템의 기존 규칙을 명문화하는 것입니다. ref 필드를 ref struct로 허용하기 위해서는 고려해야 할 몇 가지 새로운 시나리오가 있기 때문에 새로운 규칙을 명문화해야 합니다.

첫 번째는 readonly ref 이제 ref 상태를 저장할 수 있다는 것입니다. 예를 들어:

readonly ref struct Container
{
    readonly ref Span<int> Span;

    void Store(Span<int> span)
    {
        Span = span;
    }
}

즉, 메서드 인수는 규칙과 일치해야 한다는 것을 염두에 두고, readonly ref TT에 대한 ref 필드가 잠재적으로 있는 경우, ref struct이 메서드의 잠재적 출력으로 고려되어야 합니다.

두 번째 문제는 언어가 새로운 유형의 안전 컨텍스트를 고려해야 한다는 것입니다. ref-field-safe-context. 전이적으로 ref struct 필드를 포함하는 모든 ref에는 ref 필드의 값을 나타내는 또 다른 이스케이프 범위가 존재합니다. 여러 ref 필드의 경우 단일 값으로 집합적으로 추적할 수 있습니다. 매개 변수에 대한 기본값은 호출자 컨텍스트입니다.

ref struct Nested
{
    ref Span<int> Span;
}

Span<int> M(ref Nested nested) => nested.Span;

이 값은 컨테이너의 안전 컨텍스트와 관련이 없습니다. 즉, 컨테이너 컨텍스트가 작아져도 필드 값의 ref에는 영향을 미치지 않습니다. 또한 ref-field-safe-context는 절대로 컨테이너의 안전 컨텍스트보다 작을 수 없습니다.

ref struct Nested
{
    ref Span<int> Span;
}

void M(ref Nested nested)
{
    scoped ref Nested refLocal = ref nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is illegal
    refLocal.Span = stackalloc int[42];

    scoped Nested valLocal = nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is still illegal
    valLocal.Span = stackalloc int[42];
}

ref-field-safe-context는 사실상 항상 존재했습니다. 지금까지 ref 필드는 정상적인 struct만을 가리킬 수 있었기 때문에 호출자-문맥으로 쉽게 축약되었습니다. 이 새로운 ref를 고려하여 기존 규칙이 ref struct 필드를 지원할 수 있도록 업데이트되어야 합니다.

셋째, 우리가 값에 대한 ref-field-context를 위반하지 않도록 ref 재할당 규칙을 업데이트해야 합니다. 기본적으로 x.e1 = ref e2 경우, e1의 형식이 ref struct일 때, ref-field-safe-context가 같아야 합니다.

이러한 문제는 매우 해결할 수 있습니다. 컴파일러 팀은 이러한 규칙의 몇 가지 버전을 스케치했으며, 이는 대부분 기존 분석에서 제외됩니다. 문제는 정확성과 유용성을 증명하는 데 도움이되는 이러한 규칙에 대한 소비 코드가 없다는 것입니다. 이로 인해 잘못된 기본값을 선택하고 런타임을 유용성 코너로 되돌릴 것이라는 두려움 때문에 지원을 추가하는 것을 매우 주저하게 됩니다. .NET 8이 allow T: ref structSpan<Span<T>>이 방향으로 밀어붙일 가능성이 높기 때문에 이러한 우려는 특히 강력합니다. 규칙이 소비 코드와 함께 수행되는 경우 더 잘 작성됩니다.

의사 결정 지연을 통해 이러한 시나리오에 대한 규칙을 구동하는 데 도움이 되는 시나리오가 있는 .NET 8까지 ref 필드가 ref struct 수 있습니다. .NET 9 기준으로 구현되지 않았습니다.

C# 11.0을 만드는 것은 무엇인가요?

이 문서에 설명된 기능은 단일 패스로 구현할 필요가 없습니다. 대신 다음 버킷의 여러 언어 릴리스에서 단계적으로 구현할 수 있습니다.

  1. ref 필드 및 scoped
  2. [UnscopedRef]
  3. ref 필드에서 ref struct으로
  4. 일몰 시 제한되는 유형
  5. 고정 크기 버퍼

릴리스에서 구현되는 항목은 범위 지정 연습에 불과합니다.

결정은 (1) 및 (2)만 C# 11.0에 포함되었습니다. 나머지는 이후 버전의 C#에서 고려됩니다.

향후 고려 사항

고급 생애 어노테이션

이 제안서의 수명 주석들은 개발자가 값의 기본 이스케이프/이스케이프 하지 않음 동작을 변경할 수 있는 기능을 제공한다는 점에서 제한적입니다. 이렇게 하면 모델에 강력한 유연성이 추가되지만 표현할 수 있는 관계 집합을 근본적으로 변경하지는 않습니다. 본질적으로 C# 모델은 여전히 효과적으로 이진적입니다: 값을 반환할 수 있거나 없을 수 있습니다.

이를 통해 제한된 수명 관계를 이해할 수 있습니다. 예를 들어 메서드에서 반환할 수 없는 값의 수명은 메서드에서 반환할 수 있는 수명보다 작습니다. 하지만 메서드에서 반환할 수 있는 값 간의 수명 관계를 설명할 수 있는 방법은 없습니다. 특히 두 값이 모두 메서드에서 반환될 수 있다고 정해진 후에는 한 값이 다른 값보다 더 긴 수명을 가진다고 말할 수 있는 방법이 없습니다. 우리의 일생 진화의 다음 단계는 그러한 관계를 설명할 수 있게 하는 것입니다.

Rust와 같은 다른 메서드를 사용하면 이러한 유형의 관계를 표현할 수 있으므로 더 복잡한 scoped 스타일 작업을 구현할 수 있습니다. 이러한 기능이 포함된 경우 언어도 마찬가지로 도움이 될 수 있습니다. 현재 이 작업을 수행하는 동기 부여 압력은 없지만, 미래에 있다면 우리의 scoped 모델을 확장하여 상당히 쉽게 포함할 수 있습니다.

모든 scoped 구문에 제네릭 스타일 인수를 추가하여 명명된 수명을 할당할 수 있습니다. 예를 들어 scoped<'a>는 수명이 'a인 값입니다. 그런 다음 where 같은 제약 조건을 사용하여 이러한 수명 간의 관계를 설명할 수 있습니다.

void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
  where 'b >= 'a
{
    s.Span = span;
}

이 메서드는 'a'b 두 수명과 그 관계를 정의하며, 특히 'b'a보다 크다는 점을 명시합니다. 값을 호출 사이트에서 메서드로 안전하게 전달하는 방법에 대해, 현재의 덜 구체적인 규칙보다 더 세분화된 규칙을 가질 수 있도록 합니다.

문제

다음 문제는 모두 이 제안과 관련이 있습니다.

제안

다음 제안은 이 제안과 관련이 있습니다.

기존 샘플

Utf8JsonReader

이 특정 코드 조각은 Span<T>가 스택에 할당되어 ref struct의 인스턴스 메서드로 전달되는 문제를 겪기 때문에, 'unsafe' 블록이 필요합니다. 이 매개 변수가 캡처되지 않더라도 언어는 이 매개 변수를 가정해야 하므로 불필요하게 여기서 마찰을 일으킵니다.

utf8JsonWriter

이 코드 조각은 데이터의 요소를 이스케이프하여 매개 변수를 변경하려고 합니다. 효율성을 위해 이스케이프된 데이터를 스택 할당할 수 있습니다. 매개 변수가 이스케이프되지 않더라도 컴파일러는 매개 변수이기 때문에 외부에 있는 메서드를 감싸는 안전 컨텍스트를 지정합니다. 스택 할당을 사용하려면, 데이터를 이스케이프한 후 매개 변수에 다시 할당하기 위해 구현에서 반드시 unsafe을 사용해야 합니다.

재미있는 샘플

ReadOnlySpan<T>

public readonly ref struct ReadOnlySpan<T>
{
    readonly ref readonly T _value;
    readonly int _length;

    public ReadOnlySpan(in T value)
    {
        _value = ref value;
        _length = 1;
    }
}

절약적인 목록

struct FrugalList<T>
{
    private T _item0;
    private T _item1;
    private T _item2;

    public int Count = 3;

    public FrugalList(){}

    public ref T this[int index]
    {
        [UnscopedRef] get
        {
            switch (index)
            {
                case 0: return ref _item0;
                case 1: return ref _item1;
                case 2: return ref _item2;
                default: throw null;
            }
        }
    }
}

예제 및 참고 사항

다음은 규칙이 작동하는 방식과 이유를 보여주는 예제 집합입니다. 위험한 동작을 보여 주는 몇 가지 예와 규칙이 이러한 동작을 방지하는 방법을 보여 주는 몇 가지 예가 포함되어 있습니다. 제안을 조정할 때 이러한 사항을 염두에 두어야 합니다.

Ref 재할당 및 호출 지점

재할당메서드 호출이 함께 작동하는 방법을 보여 줍니다.

ref struct RS
{
    ref int _refField;

    public ref int Prop => ref _refField;

    public RS(int[] array)
    {
        _refField = ref array[0];
    }

    public RS(ref int i)
    {
        _refField = ref i;
    }

    public RS CreateRS() => ...;

    public ref int M1(RS rs)
    {
        // The call site arguments for Prop contribute here:
        //   - `rs` contributes no ref-safe-context as the corresponding parameter, 
        //      which is `this`, is `scoped ref`
        //   - `rs` contribute safe-context of *caller-context*
        // 
        // This is an lvalue invocation and the arguments contribute only safe-context 
        // values of *caller-context*. That means `local1` has ref-safe-context of 
        // *caller-context*
        ref int local1 = ref rs.Prop;

        // Okay: this is legal because `local` has ref-safe-context of *caller-context*
        return ref local1;

        // The arguments contribute here:
        //   - `this` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `this` contributes safe-context of *caller-context*
        //
        // This is an rvalue invocation and following those rules the safe-context of 
        // `local2` will be *caller-context*
        RS local2 = CreateRS();

        // Okay: this follows the same analysis as `ref rs.Prop` above
        return ref local2.Prop;

        // The arguments contribute here:
        //   - `local3` contributes ref-safe-context of *function-member*
        //   - `local3` contributes safe-context of *caller-context*
        // 
        // This is an rvalue invocation which returns a `ref struct` and following those 
        // rules the safe-context of `local4` will be *function-member*
        int local3 = 42;
        var local4 = new RS(ref local3);

        // Error: 
        // The arguments contribute here:
        //   - `local4` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `local4` contributes safe-context of *function-member*
        // 
        // This is an lvalue invocation and following those rules the ref-safe-context 
        // of the return is *function-member*
        return ref local4.Prop;
    }
}

Ref 재할당 및 안전하지 않은 이스케이프

ref 재할당 규칙 다음 줄의 이유는 언뜻 보기에 명확하지 않을 수 있습니다.

e1과 동일한 e2를 가져야 합니다.

이는 ref 위치에서 가리키는 값의 수명이 고정되기 때문입니다. 간접 참조로 인해 우리는 수명이 더 짧아지더라도 이 경우 어떠한 종류의 가변성도 허용할 수 없습니다. 축소가 허용되면 다음과 같은 안전하지 않은 코드가 열립니다.

void Example(ref Span<int> p)
{
    Span<int> local = stackalloc int[42];
    ref Span<int> refLocal = ref local;

    // Error:
    // The safe-context of refLocal is narrower than p. For a non-ref reassignment 
    // this would be allowed as its safe to assign wider lifetimes to narrower ones.
    // In the case of ref reassignment though this rule prevents it as the 
    // safe-context values are different.
    refLocal = ref p;

    // If it were allowed this would be legal as the safe-context of refLocal
    // is *caller-context* and that is satisfied by stackalloc. At the same time
    // it would be assigning through p and escaping the stackalloc to the calling
    // method
    // 
    // This is equivalent of saying p = stackalloc int[13]!!! 
    refLocal = stackalloc int[13];
}

ref에서 ref struct로 가는 경우, 모든 값이 동일한 안전 컨텍스트를 가지므로 이 규칙은 쉽게 충족됩니다. 이 규칙은 값이 ref struct때에만 실제로 적용됩니다.

이러한 ref의 동작은 ref 필드가 ref struct할 수 있는 미래에서도 중요합니다.

범위가 지정된 지역

지역 변수에서 scoped을 사용하는 것은 서로 다른 안전 컨텍스트를 가진 값을 조건에 따라 지역 변수에 할당하는 코드 패턴에 특히 유용합니다. 즉, 코드는 더 이상 로컬 = stackalloc byte[0] 정의하기 위해 같은 초기화 트릭에 의존할 필요가 없지만 이제는 scoped사용할 수 있습니다.

// Old way 
// Span<byte> span = stackalloc byte[0];
// New way 
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
    span = stackalloc byte[len];
}
else
{
    span = new byte[len];
}

이 패턴은 낮은 수준의 코드에서 자주 발생합니다. ref structSpan<T>인 경우 위의 트릭을 사용할 수 있습니다. 그러나 다른 ref struct 형식에는 적용되지 않으며, 수명을 제대로 지정하지 못하는 경우를 해결하기 위해 unsafe 의존해야 하는 하위 수준 코드가 발생할 수 있습니다.

제한된 매개 변수 값

낮은 수준의 코드에서 반복되는 마찰의 한 가지 원인은 매개 변수에 대한 기본 이스케이프가 관대하기 때문입니다. 그들은 안전 컨텍스트 에 있으며 호출자 컨텍스트에 있습니다. 이는 .NET의 코딩 패턴과 전체적으로 일치하기 때문에 합리적인 기본값입니다. 하위 수준 코드에서는 ref struct 더 많이 사용하지만 이 기본값은 참조 안전 컨텍스트 규칙의 다른 부분과 마찰을 일으킬 수 있습니다.

메서드 인수가 규칙과 일치해야 하기 때문에 주요 마찰 지점이 발생합니다. 이 규칙은 주로 하나 이상의 매개 변수가 ref structref struct 인스턴스 메서드에 관련될 때 적용됩니다. 이는 ref struct 형식이 일반적으로 메서드에서 Span<T> 매개 변수를 활용하는 하위 수준 코드의 일반적인 패턴입니다. 예를 들어 ref struct을 사용하여 버퍼를 전달하는 작성기 스타일 Span<T>에서는 발생합니다.

이 규칙은 다음과 같은 시나리오를 방지하기 위해 존재합니다.

ref struct RS
{
    Span<int> _field;
    void Set(Span<int> p)
    {
        _field = p;
    }

    static void DangerousCode(ref RS p)
    {
        Span<int> span = stackalloc int[] { 42 };

        // Error: if allowed this would let the method return a reference to 
        // the stack
        p.Set(span);
    }
}

본질적으로 이 규칙은 모든 메서드의 입력이 허용된 최대 안전 컨텍스트로 이스케이프한다고 가정해야 하기 때문에 존재합니다. 수신기를 포함하여 ref 또는 out 매개 변수가 있는 경우 위의 ref 것과 같이 입력이 해당 RS.Set 값의 필드로 이스케이프할 수 있습니다.

실제로는 ref struct을 출력에서 캡처할 의도가 없는 매개 변수로 전달하는 많은 메서드가 있습니다. 현재 메서드 내에서 사용되는 값일 뿐입니다. 예를 들어:

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Error: The safe-context of `span` is function-member 
        // while `reader` is outside function-member hence this fails
        // by the above rule.
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

이 낮은 수준의 코드를 해결하기 위해 unsafe수명에 대해 컴파일러에 거짓말을하는 ref struct 트릭에 의존합니다. ref struct의 가치는 고성능 코드를 계속 작성하면서 unsafe을 방지하기 위한 수단으로 설계되었기 때문에, 이로 인해 가치를 크게 감소시킵니다.

규칙에 따라 업데이트된 메서드 인수가 일치해야 하므로, 매개 변수를 메서드의 반환에서 고려되지 않도록 하는 효과적인 도구입니다. 사용되지만 반환되지 않는 ref struct 매개 변수는 scoped 레이블을 지정하여 통화 사이트를 보다 유연하게 만들 수 있습니다.

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(scoped ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Okay: the compiler never considers `span` as capturable here hence it doesn't
        // contribute to the method arguments must match rule
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

까다로운 ref 할당이 읽기 전용으로 변형되지 않도록 방지

ref를 생성자 또는 readonly 멤버의 init 필드에 가져가면 그 형식은 ref이며 ref readonly가 아닙니다. 이는 다음과 같은 코드를 허용하는 오랜 동작입니다.

struct S
{
    readonly int i; 

    public S(string s)
    {
        M(ref i);
    }

    static void M(ref int i) { }
}

그렇지만 동일한 형식의 ref 필드에 ref을 저장할 수 있다면 잠재적인 문제가 발생할 수 있습니다. 인스턴스 멤버로부터 readonly struct를 직접 수정할 수 있습니다.

readonly ref struct S
{ 
    readonly int i; 
    readonly ref int r; 
    public S()
    {
        i = 0;
        // Error: `i` has a narrower scope than `r`
        r = ref i;
    }

    public void Oops()
    {
        r++;
    }
}

그럼에도 불구하고 이 제안은 참조 안전 컨텍스트 규칙을 위반하기 때문에 이를 방지합니다. 다음을 고려합니다.

  • this함수 멤버이고, 안전 컨텍스트호출자 컨텍스트입니다. this 멤버의 struct의 두 가지 표준입니다.
  • i함수 멤버입니다. 이는 필드 수명 규칙에서 비롯된 결과입니다. 특히 규칙 4.

r = ref i 줄은 이 시점에서 참조 재할당 규칙에 의해 유효하지 않습니다.

이러한 규칙은 이 동작을 방지하기 위한 것이 아니라 부작용으로 의도된 것입니다. 이러한 시나리오에 미치는 영향을 평가하려면 향후 규칙 업데이트에 유의해야 합니다.

어리석은 순환 할당

이 디자인이 어려움을 겪은 한 측면은 메서드에서 ref을 얼마나 자유롭게 반환할 수 있는가 하는 점입니다. 대부분의 개발자는 모든 ref이 일반 값처럼 자유롭게 반환되기를 직관적으로 기대할 가능성이 높습니다. 그러나 참조 안전을 계산할 때 컴파일러에서 고려해야 하는 병리학적 시나리오를 허용합니다. 다음을 고려합니다.

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        // Error: s.field can only escape the current method through a return statement
        s.refField = ref s.field;
    }
}

이는 개발자가 사용하기를 기대하는 코드 패턴이 아닙니다. 그러나 ref가 값과 동일한 수명으로 반환될 수 있다면 규칙에 따라 그것은 합법적입니다. 컴파일러는 메서드 호출을 평가할 때 모든 법적 사례를 고려해야 하며, 이로 인해 이러한 API를 효과적으로 사용할 수 없게 됩니다.

void M(ref S s)
{
    ...
}

void Usage()
{
    // safe-context to caller-context
    S local = default; 

    // Error: compiler is forced to assume the worst and concludes a self assignment
    // is possible here and must issue an error.
    M(ref local);
}

이러한 API를 사용 가능하게 하기 위해, 컴파일러는 ref 매개 변수의 ref 수명이 관련된 매개 변수 값의 참조 수명보다 작도록 보장합니다. 이는 부터 ref까지 ref struct이고, out을 위해 참조 안전 컨텍스트을 갖는 이유입니다. 따라서 수명 차이로 인해 주기적 할당을 방지할 수 있습니다.

호출자 컨텍스트 값을 위해 모든 ref-safe-context 승격하므로 주기적 할당을 허용하고 호출 체인을 바이럴 사용을 강제합니다.

S F()
{
    S local = new();
    // Error: self assignment possible inside `S.M`.
    S.M(ref local);
    return local;
}

ref struct S
{
    int field;
    ref int refField;

    public static void M([UnscopedRef] ref S s)
    {
        // Allowed: s has both safe-context and ref-safe-context of caller-context
        s.refField = ref s.field;
    }
}

마찬가지로 [UnscopedRef] out은 매개변수에 안전 컨텍스트ref-safe-context가 모두 포함되어 있으며, 반환 전용이므로 순환 할당을 허용합니다.

호출자 컨텍스트에서 으로 승격하는 것은 형식이 이고 가 아닐 때 유용합니다. (참고로, 규칙을 단순하게 유지하여 ref와 비ref 구조체를 구분하지 않으려고 합니다.)

int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2

static S F([UnscopedRef] ref int x)
{
    S local = new();
    local.M(ref x);
    return local;
}

ref struct S
{
    public ref int RefField;

    public void M([UnscopedRef] ref int data)
    {
        RefField = ref data;
    }
}

고급 주석의 관점에서 [UnscopedRef] 디자인은 다음을 만듭니다.

ref struct S { }

// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)

// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
  where 'b >= 'a

readonly는 ref 필드를 통해 깊이 있게 전달될 수 없습니다.

아래 코드 샘플을 고려합니다.

ref struct S
{
    ref int Field;

    readonly void Method()
    {
        // Legal or illegal?
        Field = 42;
    }
}

진공 상태에서 ref 인스턴스의 readonly 필드에 대한 규칙을 디자인할 때 위의 규칙이 합법적이거나 불법이 되도록 규칙을 올바르게 설계할 수 있습니다. 기본적으로 readonlyref 필드를 통해 깊어질 수 있으며, 또는 ref에만 적용될 수 있습니다. ref만 적용하면 참조 재할당이 방지되지만, 참조된 값을 변경하는 기본 할당은 허용됩니다.

이 디자인은 고립된 상태에 있는 것이 아니라 이미 효과적으로 ref 필드가 있는 유형에 대한 규칙을 디자인하고 있습니다. 그 중 가장 눈에 띄는 Span<T>은 이미 이곳에서 깊지 않은 readonly에 대한 강한 종속성을 가지고 있다. 기본 시나리오는 ref 인스턴스를 통해 readonly 필드에 할당하는 기능입니다.

readonly ref struct SpanOfOne
{
    readonly ref int Field;

    public ref int this[int index]
    {
        get
        {
            if (index != 1)
                throw new Exception();
            return ref Field;
        }
    }
}

즉, 우리는 readonly의 얕은 해석을 선택해야 합니다.

모델링 생성자

미묘한 디자인 질문 중 하나는 생성자 본문이 참조 안전을 위해 어떻게 모델링되는지입니다. 기본적으로 다음 생성자는 어떻게 분석합니까?

ref struct S
{
    ref int field;

    public S(ref int f)
    {
        field = ref f;
    }
}

다음과 같은 두 가지 방법이 있습니다.

  1. static 메서드로 모델링하기, 여기서 this호출자 컨텍스트 내의 안전 컨텍스트인 로컬이다.
  2. staticthis 매개 변수인 out 메서드로 모델링합니다.

또한 생성자는 다음 불변식을 충족해야 합니다.

  1. ref 매개 변수를 ref 필드로 캡처할 수 있는지 확인합니다.
  2. ref 매개 변수를 통해 this의 필드인 ref이 이스케이프되지 않도록 합니다. 이는 까다로운 참조 할당을 위반할 것입니다.

의도는 생성자에 대한 특별한 규칙을 도입하지 않고 우리의 불변 조건을 충족하는 양식을 선택하는 것입니다. 주어진 조건에서 생성자를 위해 가장 적합한 모델은 this을(를) out 매개 변수로 고려하는 것입니다. out 특성만 반환하여 특별한 경우 처리를 하지 않고도 위의 모든 불변 조건을 만족할 수 있게 합니다.

public static void ctor(out S @this, ref int f)
{
    // The ref-safe-context of `ref f` is *return-only* which is also the 
    // safe-context of `this.field` hence this assignment is allowed
    @this.field = ref f;
}

메서드 인수가 일치해야 합니다.

메서드 인수가 규칙과 일치해야 하는 것은 개발자에게 일반적인 혼동의 원인입니다. 규칙의 이면에 있는 추론에 익숙하지 않으면 이해하기 어려운 다양한 특수 사례가 있는 규칙입니다. 규칙의 이유를 더 잘 이해하기 위해 ref-safe-contextsafe-context를 단순화하여 단순히 컨텍스트로 만듭니다.

메서드는 매개 변수로 전달된 상태를 자유롭게 반환할 수 있습니다. 범위가 지정되지 않은 모든 도달 가능한 상태는 기본적으로 반환될 수 있습니다(예를 들어, ref을 통해 반환). return 문을 통해 직접 반환하거나 ref 값에 할당하여 간접적으로 반환할 수 있습니다.

직접 반환은 참조 안전성에 큰 문제를 제기하지 않습니다. 컴파일러는 메서드의 반환 가능한 모든 입력을 살펴본 후, 반환 값을 해당 입력의 최소 컨텍스트로 효과적으로 제한합니다. 그런 다음 반환 값은 정상적인 처리를 거치게 됩니다.

간접 반환은 모든 ref가 메서드의 입력이자 출력이기 때문에 큰 문제를 일으킵니다. 이러한 출력에는 이미 알려진 컨텍스트있습니다. 컴파일러는 새로운 요소를 유추할 수 없으며 현재 수준에서 고려해야만 합니다. 즉, 컴파일러는 호출된 메서드에서 할당할 수 있는 각 ref을 확인하고, 컨텍스트 를 평가한 다음, 메서드에 반환할 수 있는 입력의 컨텍스트가 해당 보다 더 작은 ref가 없는지 확인해야 합니다. 이러한 경우 메서드 호출이 ref 안전을 위반할 수 있으므로 불법이어야 합니다.

메서드 인수는 컴파일러가 이 안전 검사를 어설션하는 프로세스와 일치해야 합니다.

개발자가 고려하는 것이 더 쉬운 다른 평가 방법은 다음 연습을 수행하는 것입니다.

  1. 메서드 정의를 살펴보면 상태를 간접적으로 반환할 수 있는 모든 위치(a)를 식별합니다. 변경 가능한 ref 매개 변수가 ref struct b를 가리킵니다. 참조 할당 가능 ref 필드 c를 사용하여 변경할 수 있는 ref 매개 변수입니다. ref 객체를 가리키는 할당 가능한 ref 매개 변수 또는 ref struct 필드(재귀적으로 고려하십시오)
  2. 호출 위치 a를 확인합니다. 위에서 식별된 위치와 일치하는 컨텍스트를 식별합니다. 반환 가능한 메서드에 대한 모든 입력의 컨텍스트를 식별합니다(scoped 매개 변수와 정렬하지 않음).

2.b의 값이 2.a보다 작은 경우 메서드 호출이 잘못되어야 합니다. 규칙을 설명하는 몇 가지 예제를 살펴보겠습니다.

ref struct R { }

class Program
{
    static void F0(ref R a, scoped ref R b) => throw null;

    static void F1(ref R x, scoped R y)
    {
        F0(ref x, ref y);
    }
}

F0 호출을 보면 (1) 및 (2)를 통과 할 수 있습니다. 간접 반환 가능성이 있는 매개 변수는 직접 할당될 수 있다는 점에서 ab입니다. 이러한 매개 변수에 부합하는 인수는 다음과 같습니다.

  • 호출자 컨텍스트의 컨텍스트 를 가진 로 매핑되는
  • by에 매핑되고, 컨텍스트에 있는 함수 멤버

메서드에 반환 가능한 입력 집합은 다음과 같습니다.

  • 호출자 컨텍스트의 x
  • 호출자 컨텍스트의 ref x
  • y이스케이프 범위를 사용하여

ref y 값은 scoped ref 매핑되므로 입력으로 간주되지 않으므로 반환할 수 없습니다. 그러나 출력( 인수) 중 하나보다 더 작은 y(인수x)가 있는 입력이 하나 이상 있는 경우 메서드 호출이 잘못되었습니다.

다른 변형은 다음과 같습니다.

ref struct R { }

class Program
{
    static void F0(ref R a, ref int b) => throw null;

    static void F1(ref R x)
    {
        int y = 42;
        F0(ref x, ref y);
    }
}

다시 간접 반환의 가능성이 있는 매개 변수는 ab이며, 둘 다 직접 할당할 수 있습니다. 그러나 bref struct 가리키지 않으므로 ref 상태를 저장하는 데 사용할 수 없으므로 제외할 수 있습니다. 따라서 다음과 같은 내용이 있습니다.

  • 호출자 컨텍스트의 컨텍스트 를 가진 로 매핑되는

메서드에 반환 가능한 입력 집합은 다음과 같습니다.

  • x 컨텍스트호출자 컨텍스트
  • ref x 컨텍스트호출자 컨텍스트
  • ref y컨텍스트함수 멤버

출력( 인수) 중 하나보다 더 작은 ref y(인수x)가 있는 입력이 하나 이상 있는 경우 메서드 호출이 잘못되었습니다.

이것은 메서드 인수가 규칙과 일치해야 한다는 논리를 포괄하려는 시도입니다. 그것은 더 나아가서 scoped을 고려 사항에서 입력으로 제거하는 방법으로 여기고, readonly을 사용하여 ref를 출력으로서 제거하는 방법으로 삼습니다 (readonly ref에 할당할 수 없으므로 출력 원본이 될 수 없습니다). 이러한 특수 사례는 규칙에 복잡성을 더하지만 개발자의 이익을 위해 수행됩니다. 컴파일러는 멤버를 호출할 때 개발자에게 최대한의 유연성을 제공하기 위해 결과에 기여할 수 없는 것으로 알고 있는 모든 입력 및 출력을 제거하려고 합니다. 복잡성 해결과 마찬가지로, 규칙을 더 복잡하게 만드는 것이 소비자에게 더 많은 유연성을 제공할 수 있다면 그 노력을 할 가치가 있습니다.

선언 식의 안전한 컨텍스트 유추된 예시

선언 식의 유추 안전 컨텍스트 관련됩니다.

ref struct RS
{
    public RS(ref int x) { } // assumed to be able to capture 'x'

    static void M0(RS input, out RS output) => output = input;

    static void M1()
    {
        var i = 0;
        var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M2(RS rs1)
    {
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M3(RS rs1)
    {
        M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
    }
}

scoped 수정자로부터 생성된 로컬 컨텍스트는 변수를 위해 사용될 수 있는 가장 좁은 컨텍스트입니다. 더 좁아진다면 그 식은 그보다 더 좁은 컨텍스트에서만 선언된 변수를 참조하게 된다는 뜻입니다.