참조 구조체 인터페이스

메모

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

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

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

챔피언 이슈: https://github.com/dotnet/csharplang/issues/7608

요약

이 제안은 인터페이스를 구현하고 제네릭 형식 인수로 참여할 수 있도록 ref struct 기능을 확장합니다.

동기

ref struct 인터페이스를 구현할 수 없다는 것은 .NET의 매우 기본적인 추상화 기술에 참여할 수 없음을 의미합니다. Span<T>순차 목록의 모든 특성이 있지만 IReadOnlyList<T>, IEnumerable<T>등을 수행하는 메서드에 참여할 수 없습니다. 대신 구현이 거의 동일한 Span<T> 특정 메서드를 코딩해야 합니다. ref struct가 인터페이스를 구현할 수 있도록 허용하면, 다른 유형과 마찬가지로 인터페이스 상에서 작업이 추상화될 수 있습니다.

상세 디자인

ref 구조체 인터페이스

언어를 사용하면 ref struct 형식이 인터페이스를 구현할 수 있습니다. 구문 및 규칙은 struct 형식의 제한 사항을 고려하는 몇 가지 예외가 있는 일반 ref struct 경우와 동일합니다.

인터페이스를 구현하는 기능은 boxing ref struct 인스턴스에 대한 기존 제한 사항에 영향을 주지 않습니다. 즉, ref struct가 특정 인터페이스를 구현하더라도, 그것을 박싱 동작으로 보기 때문에 해당 인터페이스로 직접 캐스팅할 수 없습니다.

ref struct File : IDisposable
{
    private SafeHandle _handle;
    public void Dispose()
    {
        _handle.Dispose();
    }
}

File f = ...;
// Error: cannot box `ref struct` type `File`
IDisposable d = f;

인터페이스를 구현하는 기능은 ref struct 제네릭 인수에 참여하는 기능과 결합된 경우에만 유용합니다(나중에설명).

인터페이스가 ref struct의 전체 표현력을 다루고 그로 인해 발생할 수 있는 수명 문제를 해결할 수 있도록, 이 언어에서는 인터페이스 메서드와 속성에 [UnscopedRef]을 사용할 수 있습니다. 이는 struct 통해 추상화되는 인터페이스가 struct 직접 사용하는 것과 동일한 유연성을 가질 수 있도록 하기 때문에 필요합니다. 다음 예제를 고려하세요.

interface I1
{
    [UnscopedRef]
    ref int P1 { get; }
    ref int P2 { get; }
}

struct S1
{
    [UnscopedRef]
    internal ref int P1 { get {...} }

    internal ref int P2 { get {...} }
}

ref int M<T>(T t, S1 s)
    where T : I1, allows ref struct
{
    // Error: may return ref to t
    return ref t.P1;

    // Error: may return ref to t
    return ref s.P1;

    // Okay
    return ref t.P2;

    // Okay
    return ref s.P2;
}

struct / ref struct 멤버가 [UnscopedRef] 특성을 사용하여 인터페이스 멤버를 구현하는 경우 구현 멤버도 [UnscopedRef] 데코레이팅될 수 있지만 필수는 아닙니다. 그러나 [UnscopedRef] 있는 멤버는 특성(세부 정보)이 없는 멤버를 구현하는 데 사용할 수 없습니다.

interface I1
{
    [UnscopedRef]
    ref int P1 { get; }
    ref int P2 { get; }
}

struct S1
{
    internal ref int P1 { get {...} }
    internal ref int P2 { get {...} }
}

struct S2
{
    [UnscopedRef]
    internal ref int P1 { get {...} }
    internal ref int P2 { get {...} }
}

struct S3 : I1
{
    internal ref int P1 { get {...} }
    // Error: P2 is marked with [UnscopedRef] and cannot implement I1.P2 as is not marked 
    // with [UnscopedRef]
    [UnscopedRef]
    internal ref int P2 { get {...} }
}

class C1 : I1
{
    internal ref int P1 { get {...} }
    internal ref int P2 { get {...} }
}

기본 인터페이스 메서드는 ref struct 멤버를 박싱하는 기본 구현으로 인해 보호가 없어서 this 문제가 발생합니다.

interface I1
{
    void M()
    {
        // Danger: both of these box if I1 is implemented by a ref struct
        I1 local1 = this;
        object local2 = this;
    }
}

// Error: I1.M cannot implement interface member I1.M() for ref struct S
ref struct S : I1 { }

이를 처리하기 위해 ref struct 기본 구현이 있더라도 인터페이스의 모든 멤버를 구현해야 합니다. 또한 ref struct 형식에서 기본 인터페이스 멤버가 호출되는 경우 예외를 throw하도록 런타임이 업데이트됩니다.

런타임에 예외를 방지하기 위해 컴파일러는 ref 구조체를 허용하는 형식 매개 변수에서 가상이 아닌 인스턴스 메서드(또는 속성)를 호출하는 오류를 보고합니다. 예제는 다음과 같습니다.

public interface I1
{
    sealed void M3() {}
}

class C
{
    static void Test2<T>(T x) where T : I1, allows ref struct
    {
#line 100
        x.M3(); // (100,9): error: A non-virtual instance interface member cannot be accessed on a type parameter that allows ref struct.
    }
}

현재 논의 중인 디자인 문제로, ref 구조체를 허용하는 형식의 매개 변수에서 추상적이지 않은 가상 인스턴스 메서드(또는 속성)를 호출할 때 경고을 보고하는 방법에 대한 질문도 있습니다.

자세한 참고 사항:

  • ref struct 인터페이스를 구현할 수 있습니다.
  • ref struct 기본 인터페이스 멤버에 참여할 수 없습니다.
  • ref struct는 박싱 작업이기 때문에 구현된 인터페이스로 캐스팅할 수 없습니다.

ref 구조체의 제네릭 매개 변수

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : restrictive_type_parameter_constraints
    | allows_type_parameter_constraints_clause
    | restrictive_type_parameter_constraints ',' allows_type_parameter_constraints_clause

restrictive_type_parameter_constraints
    : primary_constraint
    | secondary_constraints
    | constructor_constraint
    | primary_constraint ',' secondary_constraints
    | primary_constraint ',' constructor_constraint
    | secondary_constraints ',' constructor_constraint
    | primary_constraint ',' secondary_constraints ',' constructor_constraint
    ;

primary_constraint
    : class_type
    | 'class'
    | 'struct'
    | 'unmanaged'
    ;

secondary_constraints
    : interface_type
    | type_parameter
    | secondary_constraints ',' interface_type
    | secondary_constraints ',' type_parameter
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

allows_type_parameter_constraints_clause
    : 'allows' allows_type_parameter_constraints

allows_type_parameter_constraints
    : allows_type_parameter_constraint
    | allows_type_parameter_constraints ',' allows_type_parameter_constraint

allows_type_parameter_constraint
    : ref_struct_clause

ref_struct_clause
    : 'ref' 'struct'

이 언어를 사용하면 제네릭 매개 변수가 ref struct 절 내에서 allows ref struct 구문을 사용하여 where을/를 인수로 선택적으로 지원하도록 할 수 있습니다.

T Identity<T>(T p)
    where T : allows ref struct
    => p;

// Okay
Span<int> local = Identity(new Span<int>(new int[10]));

제네릭 매개 변수의 기능을 지정한다는 where 절의 다른 항목과 비슷합니다. 차이점은 다른 구문 항목은 제네릭 매개 변수를 충족할 수 있는 형식 집합을 제한하는 반면 allows ref struct 형식 집합을 확장한다는 점입니다. 이는 ref struct 제네릭 매개 변수를 충족할 수 없는 암시적 제약 조건을 제거하기 때문에 사실상 제약 조건 방지입니다. 따라서 이를 명확하게 하기 위해 allows새 구문 접두사로 제공됩니다.

allows ref struct 바인딩된 형식 매개 변수에는 ref struct 형식의 모든 동작이 있습니다.

  1. 인스턴스를 상자에 담을 수 없습니다.
  2. 인스턴스는 일반 ref struct와 유사한 수명 규칙에 참여합니다.
  3. 형식 매개 변수는 static 필드, 배열 요소 등에서 사용할 수 없습니다.
  4. 인스턴스를 scoped 표시할 수 있습니다.

작동 중인 이러한 규칙의 예:

interface I1 { }
I1 M1<T>(T p)
    where T : I1, allows ref struct
{
    // Error: cannot box potential ref struct
    return p;
}

T M2<T>(T p)
    where T : allows ref struct
{
    Span<int> span = stackalloc int[42];

    // The safe-to-escape of the return is current method because one of the inputs is
    // current method
    T t = M3<int, T>(span);

    // Error: the safe-to-escape is current method.
    return t;

    // Okay
    return default;
    return p;
}

R M3<T, R>(Span<T> span)
    where R : allows ref struct
{
    return default;
}

제약 조건 방지는 형식 매개 변수 형식 제약 조건에서 "상속"되지 않습니다. 예를 들어 아래 코드의 S ref 구조체로 대체할 수 없습니다.

class C<T, S>
    where T : allows ref struct
    where S : T
{}

자세한 참고 사항:

  • where T : allows ref struct 제네릭 매개 변수는 사용할 수 없습니다.
    • where T : U은(는) U로 알려진 참조 형식입니다
    • where T : class 제약 조건이 있다.
    • 해당 매개 변수도 where T: allows ref struct 않는 한 제네릭 인수로 사용할 수 없습니다.
  • allows ref struct where 절의 마지막 제약 조건이어야 합니다.
  • T 있는 형식 매개 변수 allows ref structref struct 형식과 모두 동일한 제한 사항이 있습니다.

메타데이터의 표현

ref 구조체를 허용하는 형식 매개 변수는 byref와 유사한 제네릭 문서설명된 대로 메타데이터로 인코딩됩니다. 특히 CorGenericParamAttr.gpAllowByRefLike(0x0020) 또는 System.Reflection.GenericParameterAttributes.AllowByRefLike(0x0020) 플래그 값을 사용합니다. 런타임에서 이 기능을 지원하는지 여부는 System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics 필드의 존재를 확인하여 확인할 수 있습니다. 여러 API가 https://github.com/dotnet/runtime/pull/98070에 추가되었습니다.

using 명세서

using 문은 리소스가 ref 구조체인 경우 IDisposable 인터페이스의 구현을 인식하고 사용합니다.

ref struct S2 : System.IDisposable
{
    void System.IDisposable.Dispose()
    {
    }
}

class C
{
    static void Main()
    {
        using (new S2())
        {
        } // S2.System.IDisposable.Dispose is called
    }
}

기본 설정은 패턴을 구현하는 Dispose 메서드에 지정되며, 패턴을 찾을 수 없는 경우에만 IDisposable 구현이 사용됩니다.

using 문은 리소스가 IDisposableallows ref struct이 포함된 유효 인터페이스 집합의 형식 매개 변수일 때, IDisposable 인터페이스의 구현을 인식하고 사용합니다.

class C
{
    static void Test<T>(T t) where T : System.IDisposable, allows ref struct
    {
        using (t)
        {
        }
    }
}

인터페이스(그리고 패턴을 찾을 수 있는 유일한 위치)가 ref 구조체가 아니기 때문에 Dispose 형식 매개 변수에서는 패턴 allows ref struct 메서드가 인식되지 않습니다.

interface IMyDisposable
{
    void Dispose();
}
class C
{
    static void Test<T>(T t, IMyDisposable s) where T : IMyDisposable, allows ref struct
    {
        using (t) // Error, the pattern is not recognized
        {
        }

        using (s) // Error, the pattern is not recognized
        {
        }
    }
}

await using 명세서

현재 언어는 참조 구조체를 await using 문의 리소스로 사용할 수 없습니다. allows ref struct형식 매개 변수에 동일한 제한 사항이 적용됩니다.

비동기 메서드에서 ref 구조체의 사용에 대한 일반적인 제한을 해제하는 제안이 있습니다 - https://github.com/dotnet/csharplang/pull/7994. 만약/언제 그 제한이 해제된다면, 이 섹션의 나머지 부분에서는 await using 문에 대한 일반적인 제한 사항이 해제된 후의 동작에 대해 설명합니다.

await using 문은 리소스가 ref 구조체인 경우 IAsyncDisposable 인터페이스의 구현을 인식하고 사용합니다.

ref struct S2 : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync()
    {
    }
}

class C
{
    static async Task Main()
    {
        await using (new S2())
        {
        } // S2.IAsyncDisposable.DisposeAsync
    }
}

기본 설정은 패턴을 구현하는 DisposeAsync 메서드에 지정되며, 패턴을 찾을 수 없는 경우에만 IAsyncDisposable 구현이 사용됩니다.

패턴 DisposeAsync 메서드는 오늘날 제약 조건 없이 형식 매개 변수에서 인식되는 것처럼, allows ref struct 제약 조건이 있는 형식 매개 변수에서도 인식될 것입니다.

interface IMyAsyncDisposable
{
    ValueTask DisposeAsync();
}

class C
{
    static async Task Test<T>() where T : IMyAsyncDisposable, new(), allows ref struct
    {
        await using (new T())
        {
        } // IMyAsyncDisposable.DisposeAsync
    }
}

using 문은 리소스가 IAsyncDisposable형식 매개 변수인 경우, allows ref struct 패턴 메서드를 찾는 프로세스가 실패할 때, 그리고 DisposeAsync가 형식 매개 변수의 유효 인터페이스 집합에 있을 때 IAsyncDisposable 인터페이스의 구현을 인식하고 사용합니다.

interface IMyAsyncDisposable1
{
    ValueTask DisposeAsync();
}

interface IMyAsyncDisposable2
{
    ValueTask DisposeAsync();
}

class C
{
    static async Task Test<T>() where T : IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable, new(), allows ref struct
    {
        await using (new T())
        {
            System.Console.Write(123);
        } // IAsyncDisposable.DisposeAsync
    }
}

foreach 명세서

다음을 통합하기 위해 https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement 섹션을 적절하게 업데이트해야 합니다.

foreach 문은 컬렉션이 ref 구조체인 경우 IEnumerable<T>/IEnumerable 인터페이스의 구현을 인식하고 사용합니다.

ref struct S : IEnumerable<int>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() {...}
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {...}
}

class C
{
    static void Main()
    {
        foreach (var i in new S()) // IEnumerable<int>.GetEnumerator
        {
        }
    }
}

패턴 GetEnumerator 메서드는 오늘날 제약 조건 없이 형식 매개 변수에서 인식되는 것처럼, allows ref struct 제약 조건이 있는 형식 매개 변수에서도 인식될 것입니다.

interface IMyEnumerable<T>
{
    IEnumerator<T> GetEnumerator();
}

class C
{
    static void Test<T>(T t) where T : IMyEnumerable<int>, allows ref struct
    {
        foreach (var i in t) // IMyEnumerable<int>.GetEnumerator
        {
        }
    }
}

foreach 문은 컬렉션이 IEnumerable<T>형식 매개 변수이고, / 패턴 메서드를 찾는 프로세스가 실패하고, IEnumerableallows ref structGetEnumerator 형식 매개 변수의 유효 인터페이스 집합에 있는 경우 IEnumerable<T>/IEnumerable 인터페이스의 구현을 인식하고 사용합니다.

interface IMyEnumerable1<T>
{
    IEnumerator<int> GetEnumerator();
}

interface IMyEnumerable2<T>
{
    IEnumerator<int> GetEnumerator();
}

class C
{
    static void Test<T>(T t) where T : IMyEnumerable1<int>, IMyEnumerable2<int>, IEnumerable<int>, allows ref struct
    {
        foreach (var i in t) // IEnumerable<int>.GetEnumerator
        {
        }
    }
}

enumerator 패턴은 지금까지 제약 조건 없이 인식되었던 형식 매개 변수에 대해 인식되었던 것처럼, allows ref struct 형식 매개 변수에 대해 동일하게 인식됩니다.

interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct 
{
    TEnumerator GetEnumerator();
}

class C
{
    static void Test1<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
        where TEnumerator : IEnumerator, IDisposable, allows ref struct 
    {
        foreach (var i in t) // IEnumerator.MoveNext/Current
        {
        }
    }

    static void Test2<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
        where TEnumerator : IEnumerator<int>, allows ref struct 
    {
        foreach (var i in t) // IEnumerator<int>.MoveNext/Current
        {
        }
    }

    static void Test3<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
        where TEnumerator : IMyEnumerator<int>, allows ref struct 
    {
        foreach (var i in t) // IMyEnumerator<int>.MoveNext/Current
        {
        }
    }
}

interface IMyEnumerator<T> : System.IDisposable
{
    T Current {get;}
    bool MoveNext();
}

foreach 문은 열거자가 ref 구조체인 경우 IDisposable 인터페이스의 구현을 인식하고 사용합니다.

struct S1
{
    public S2 GetEnumerator()
    {
        return new S2();
    }
}

ref struct S2 : System.IDisposable
{
    public int Current {...}
    public bool MoveNext() {...}
    void System.IDisposable.Dispose() {...}
}

class C
{
    static void Main()
    {
        foreach (var i in new S1())
        {
        } // S2.System.IDisposable.Dispose()
    }
}

기본 설정은 패턴을 구현하는 Dispose 메서드에 지정되며, 패턴을 찾을 수 없는 경우에만 IDisposable 구현이 사용됩니다.

foreach 문은 열거자가 형식 매개 변수이며, 그 형식 매개 변수의 유효 인터페이스 집합에 IDisposableallows ref struct가 포함되어 있는 경우에 IDisposable 인터페이스의 구현을 인식하고 사용합니다.

interface ICustomEnumerator
{
    int Current {get;}
    bool MoveNext();
}

interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct 
{
    TEnumerator GetEnumerator();
}

class C
{
    static void Test<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>
        where TEnumerator : ICustomEnumerator, System.IDisposable, allows ref struct 
    {
        foreach (var i in t)
        {
        } // System.IDisposable.Dispose()
    }
}

인터페이스(그리고 패턴을 찾을 수 있는 유일한 위치)가 ref 구조체가 아니기 때문에 Dispose 형식 매개 변수에서는 패턴 allows ref struct 메서드가 인식되지 않습니다. 또한 런타임은 allows ref struct 형식 매개변수가 런타임 시 IDisposable 인터페이스를 구현하는지를 확인할 방법을 제공하지 않으므로, allows ref struct가 유효 인터페이스 집합에 포함되지 않는 한 IDisposable 형식 매개변수 열거자는 허용되지 않습니다.

interface ICustomEnumerator
{
    int Current {get;}
    bool MoveNext();
}

interface IMyDisposable
{
    void Dispose();
}

interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct 
{
    TEnumerator GetEnumerator();
}

class C
{
    static void Test<TEnumerable, TEnumerator>(TEnumerable t)
        where TEnumerable : IGetEnumerator<TEnumerator>
        where TEnumerator : ICustomEnumerator, IMyDisposable, allows ref struct 
    {
        // error CS9507: foreach statement cannot operate on enumerators of type 'TEnumerator'
        //               because it is a type parameter that allows ref struct and
        //               it is not known at compile time to implement IDisposable.
        foreach (var i in t)
        {
        }
    }
}

await foreach 명세서

다음을 통합하기 위해 https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement 섹션을 적절하게 업데이트해야 합니다.

await foreach 문은 컬렉션이 ref 구조체인 경우 IAsyncEnumerable<T> 인터페이스의 구현을 인식하고 사용합니다.

ref struct S : IAsyncEnumerable<int>
{
    IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator(CancellationToken token) {...}
}

class C
{
    static async Task Main()
    {
        await foreach (var i in new S()) // S.IAsyncEnumerable<int>.GetAsyncEnumerator
        {
        }
    }
}

패턴 GetAsyncEnumerator 메서드는 오늘날 제약 조건 없이 형식 매개 변수에서 인식되는 것처럼, allows ref struct 제약 조건이 있는 형식 매개 변수에서도 인식될 것입니다.

interface IMyAsyncEnumerable<T>
{
    IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}

class C
{
    static async Task Test<T>() where T : IMyAsyncEnumerable<int>, allows ref struct
    {
        await foreach (var i in default(T)) // IMyAsyncEnumerable<int>.GetAsyncEnumerator
        {
        }
    }
}

await foreach 문은 컬렉션이 IAsyncEnumerable<T>형식 매개 변수일 때, allows ref struct 패턴 메서드를 찾는 과정이 실패하고, GetAsyncEnumerator가 형식 매개 변수의 유효 인터페이스 집합에 포함되어 있는 경우, IAsyncEnumerable<T> 인터페이스의 구현을 인식하고 사용할 것입니다.

interface IMyAsyncEnumerable1<T>
{
    IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}

interface IMyAsyncEnumerable2<T>
{
    IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}

class C
{
    static async Task Test<T>() where T : IMyAsyncEnumerable1<int>, IMyAsyncEnumerable2<int>, IAsyncEnumerable<int>, allows ref struct
    {
        await foreach (var i in default(T)) // IAsyncEnumerable<int>.GetAsyncEnumerator
        {
            System.Console.Write(i);
        }
    }
}

await foreach 문은 ref 구조체 열거자 및 allows ref struct형식 매개 변수 열거자를 계속 허용하지 않습니다. 그 이유는 await MoveNextAsync() 호출에서 열거자를 유지해야 하기 때문입니다.

익명 함수 또는 메서드 그룹에 대한 대리자 형식

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md#delegate-types 섹션은 다음과 같이 명시합니다.

컴파일러는 나중에 더 많은 서명이 System.Action<>System.Func<> 형식에 바인딩하도록 허용할 수 있습니다(ref struct 형식이 형식 인수를 허용하는 경우).

형식 매개 변수에 대한 Action<> 제약 조건이 있는 Func<>allows ref struct 형식은 대리자의 서명에서 ref 구조체 형식과 관련된 더 많은 시나리오에서 사용됩니다.

대상 런타임에서 allows ref struct 제약 조건을 지원하는 경우 제네릭 익명 대리자 형식에는 해당 형식 매개 변수에 대한 allows ref struct 제약 조건이 포함됩니다. 이렇게 하면 ref 구조체 형식 및 allows ref struct 제약 조건이 있는 다른 형식 매개 변수를 사용하여 해당 형식 매개 변수를 대체할 수 있습니다.

인라인 배열

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/inline-arrays.md#detailed-design 섹션은 다음과 같이 명시합니다.

언어는 인라인 배열 형식의 요소에 액세스하기 위한 형식 및 참조 안전한 방법을 제공합니다. 액세스는 특정 범위를 기준으로 설정됩니다. 이 제한은 형식 인수로 사용할 수 있는 요소 형식을 사용하여 인라인 배열 형식을 지원합니다.

ref 구조체의 범위를 지원하도록 범위 형식이 변경되면 ref 구조체의 인라인 배열에 대한 제한을 해제해야 합니다.

건전성

특히 ref struct 제약 조건 방지 개념과 일반적으로 제약 조건 방지 개념의 건전성을 확인하려고 합니다. 이렇게 하려면 C# 형식 시스템에 제공된 기존 소리 증명을 활용하려고 합니다. 이 작업은 C#에 비슷하지만 생성 시 더 일반적인 새 언어를 정의하여 더 쉽게 수행할 수 있습니다. 해당 모델의 안전을 확인한 다음, 이 언어로 사운드 번역을 지정합니다. 이 새 언어는 제약 조건을 중심으로 하므로 이 언어를 "constraint-C#"이라고 합니다.

보존해야 하는 주요 ref 구조체 안전 불변식은 ref 구조체 형식의 변수가 힙에 나타나지 않아야 한다는 것입니다. 제약 조건을 통해 이 제한을 인코딩할 수 있습니다. 제약 조건이 대체를 허용하기 때문에, 우리는 기술적으로 역 제약 조건을 정의합니다: heap. heap 제약 조건은 형식이 힙에 표시되도록 지정합니다. "constraint-C#"에서 ref-structs를 제외한 모든 형식은 heap 제약 조건을 충족합니다. 또한 C#의 모든 기존 형식 매개 변수는 "constraint-C#"의 heap 제약 조건이 있는 형식 매개 변수로 낮아지게 됩니다.

이제 기존 C#이 안전하다고 가정하고 C# ref-struct 규칙을 "constraint-C#"으로 전송할 수 있습니다.

  1. 클래스의 필드는 ref-struct 형식을 가질 수 없습니다.
  2. 정적 필드에는 ref-struct 형식이 있을 수 없습니다.
  3. ref-struct 형식의 변수는 ref가 아닌 구조체로 변환할 수 없습니다.
  4. ref-struct 형식의 변수는 형식 인수로 대체할 수 없습니다.
  5. ref-struct 형식의 변수는 인터페이스를 구현할 수 없습니다.

새 규칙은 heap 제약 조건에 적용됩니다.

  1. 클래스 필드에는 heap 제약 조건을 충족하는 형식이 있어야 합니다.
  2. 정적 필드에는 heap 제약 조건을 충족하는 형식이 있어야 합니다.
  3. heap 제약 조건이 있는 형식에는 ID 변환만 있습니다.
  4. ref-struct 형식의 변수는 heap 제약 조건 없이 형식 매개 변수로만 대체될 수 있습니다.
  5. Ref-struct 형식은 기본 인터페이스 멤버가 없는 인터페이스만 구현할 수 있습니다.

규칙(4) 및 (5)는 약간 변경됩니다. 규칙(4)은 heap 제약 없이 형식 매개 변수의 개념을 가지고 있으므로 정확히 전송할 필요가 없습니다. 규칙(5)은 복잡합니다. 인터페이스 구현이 항상 문제가 되는 것은 아니지만, 기본 인터페이스 메서드는 인터페이스 유형의 수신기를 전제로 하며 이는 값 유형이 아니므로 규칙 (3)을 위반합니다. 따라서 기본 인터페이스 멤버는 허용되지 않습니다.

이러한 규칙을 사용하면 "constraint-C#"은 ref-struct 안전하며 형식 대체를 지원하며 인터페이스 구현을 지원합니다. 다음 단계는 이 제안에 정의된 언어를 "allow-C#"이라고 부르고, 이를 "constraint-C#"으로 번역하는 것입니다. 다행히도 이것은 사소한 일입니다. 낮추기는 간단한 구문 변환입니다. "allow-C#"의 where T : allows ref struct 구문은 "constraint-C#"에서 제약 조건 없음과 동일하며 "allow 절"이 없는 것은 heap 제약 조건과 동일합니다. 추상 의미 체계와 타이핑이 동일하므로 "allow-C#"도 타당합니다.

C#의 모든 형식화된 용어도 "constraint-C#"에 입력되는지 여부를 고려할 수 있는 마지막 속성이 있습니다. 즉, C#의 모든 용어 t에 대해, 이를 "constraint-C#"로 낮춘 후에 해당 용어 t'이 올바른 형식인지 여부를 알고 싶습니다. 이것은 건전성 제약 조건이 아닙니다 - 우리의 대상 언어에서 용어를 잘못 입력하는 것은 안전하지 않은 것을 결코 허용하지 않을 것입니다 - 오히려 이전 버전과의 호환성에 관한 것입니다. "constraint-C#" 입력을 사용하여 "allow-C#"의 유효성을 검사하기로 결정한 경우 기존 C# 코드를 불법으로 만들지 않는지 확인하려고 합니다.

모든 C# 용어는 유효한 "constraint-C#" 용어로 시작되므로 새로운 "constraint-C#" 제한을 각각 검사하여 보존의 유효성을 검사할 수 있습니다. 먼저 heap 제약 조건을 추가합니다. C#의 모든 형식 매개 변수는 heap 제약 조건을 획득하므로 모든 기존 용어는 해당 제약 조건을 충족해야 합니다. ref 구조체를 제외한 모든 구체적인 형식에 적용됩니다. 이는 ref 구조체가 현재 형식 인수로 표시되지 않을 수 있으므로 적합합니다. 모든 형식 매개 변수가 heap 제약 조건을 획득하므로, 이들 형식 매개 변수에 대해서도 마찬가지입니다. 또한 heap 제약 조건은 다른 모든 제약 조건과 유효한 조합이므로 문제가 발생하지 않습니다. 규칙(1-5)은 기존 C# 규칙에 직접 해당하거나 이완이므로 문제가 발생하지 않습니다. 따라서 C#의 모든 형식화 가능한 용어는 "constraint-C#"에서 입력할 수 있어야 하며 입력 호환성이 손상되는 변경은 도입해서는 안 됩니다.

열려 있는 문제

제약 조건 방지 구문

의사 결정: where T: allows ref struct 사용

이 제안은 ref struct를 포함시키기 위해 기존 where 구문을 확장하여 allows ref struct 반제약을 드러내기로 선택했습니다. 이 두 가지 모두 기능을 간결하게 설명하며 나중에 포인터와 같은 다른 제약 조건 방지를 포함하도록 확장할 수도 있습니다. 논의할 가치가 있는 다른 솔루션이 있습니다.

첫 번째는 단순히 where 절 내에서 사용할 다른 구문을 선택하는 것입니다. 기타 제안된 옵션은 다음과 같습니다.

  • ~ref struct: ~는 뒤에 오는 구문이 반제약 조건이라는 것을 나타내는 표시로 작용합니다.
  • include ref struct: includes 대신 allows 사용
void M<T>(T p)
    where T : IDisposable, ~ref struct
{
    p.Dispose();
}

두 번째는 새 절을 완전히 사용하여 다음 항목이 허용되는 형식 집합을 확장하는 것임을 분명히 하는 것입니다. 이 것의 지지자들은 where 내에서 구문을 사용하면 읽을 때 혼란이 발생할 수 있다고 생각합니다. 초기 제안에서는 다음 구문을 사용했습니다. allow T: ref struct.

void M<T>(T p)
    where T : IDisposable
    allow T : ref struct
{
    p.Dispose();
}

where T: allows ref struct 구문은 LDM 토론에서 약간 더 강한 선호도를 가졌습니다.

공변성과 반공변성

의사 결정: 새로운 문제 없음

allows ref struct 매우 유용한 형식 매개 변수가 되려면 제네릭 분산과 호환되어야 합니다. 특히, 매개 변수가 공변적 또는 반공변적이며 동시에 allows ref struct인지가 적법해야 합니다. delegate, interface, Func<T>등과 같은 .NET에서 가장 인기있는 Action<T>IEnumerable<T> 형식에서 사용할 수 없다는 것이 부족합니다.

토론 후 이것은 문제가되지 않습니다 결론을 내렸다. allows ref struct 제약 조건은 struct 제네릭 인수로 사용할 수 있는 또 다른 방법입니다. 일반적인 struct 인수가 API의 분산을 제거하는 것처럼 ref struct도 제거합니다.

대리자 멤버에 자동 적용

의사 결정: 자동 적용 안 함

많은 일반 delegate 멤버의 경우 언어는 순전히 상방 변경이기 때문에 allows ref struct 자동으로 적용할 수 있습니다. Func<> / Action<> 스타일 대리자 및 대부분의 인터페이스 정의의 경우 ref struct허용하도록 확장해야 하는 단점이 없습니다. 언어는 이 제약 조건 방지를 자동으로 적용하는 것이 안전한 규칙을 간략하게 설명할 수 있습니다. 이렇게 하면 수동 프로세스가 제거되고 이 기능의 채택 속도가 빨라질 수 있습니다.

하지만 이 allows ref struct 자동 적용은 몇 가지 문제를 제기합니다. 첫 번째는 다중 대상 시나리오에 있습니다. 코드는 한 대상 프레임워크에서 컴파일되지만 다른 프레임워크에서는 실패하며 API가 다르게 동작해야 하는 이유에 대한 구문 표시기가 없습니다.

// Works in net9.0 but fails in all other TF
Func<Span<char>> func;

이로 인해 고객 혼란이 발생할 수 있으며 Func<T> 원본에서 net9.0 변경 내용을 살펴보면 고객에게 변경된 내용에 대한 단서를 제공하지 않을 수 있습니다.

다른 문제는 매우 미세한 코드 변경이 원거리 스푸키 액션 문제를 일으킬 수 있다는 것입니다. 다음 코드를 고려합니다.

interface I1<T>
{
}

이 인터페이스는 allows ref struct자동 애플리케이션에 적합합니다. 개발자가 나중에 찾아와서 기본 인터페이스 메서드를 추가하면 갑자기 그렇지 않으며 이미 I1<Span<char>>같은 호출을 만든 소비자가 중단됩니다. 이것은 추적하기 어려운 매우 미묘한 변화입니다.

바이너리 호환성을 깨뜨리는 변경

기존 API에 allows ref struct 추가하는 것은 원본 호환성이 손상되는 변경이 아닙니다. API에 대해 허용된 형식 집합만 확장합니다. 이진 호환성이 손상되는 변경인지 여부를 추적해야 합니다. 제네릭 매개 변수의 특성을 업데이트하는 것이 이진 호환성이 손상되는 변경을 구성하는지 불분명합니다.

DIM 호출에 경고

컴파일러는 런타임 예외에 대한 기회를 만들 때 다음과 같은 M 호출에 대해 경고해야 하나요?

interface I1
{
    // Virtual method with default implementation
    void M() { }
}

// Invocation of a virtual instance method with default implementation in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
    where T : allows ref struct, I1
{
    p.M(); // Warn?
}

그러나 이는 시끄럽고 대부분의 시나리오에서 별로 도움이 되지 않을 수 있습니다. C#에는 모든 가상 API를 구현하기 위해 ref 구조체가 필요합니다. 따라서 다른 플레이어가 동일한 규칙을 따르는 경우 예외가 발생할 수 있는 유일한 상황은 팩트 후에 메서드가 추가되는 경우입니다. 소비 코드 작성자는 이러한 모든 세부 정보를 알지 못하는 경우가 많으며 코드에서 사용할 ref 구조체를 제어하지 않는 경우가 많습니다. 따라서 작성자가 실제로 수행할 수 있는 유일한 작업은 경고를 표시하지 않는 것입니다.

고려 사항

런타임 지원

이 기능을 사용하려면 런타임/라이브러리 팀의 몇 가지 지원이 필요합니다.

  • 기본 인터페이스 메서드가 ref struct 적용되지 않도록 방지
  • System.Reflection.Metadata 값을 인코딩하기 위한 gpAcceptByRefLike API
  • 제네릭 매개 변수가 ref struct으로 지원됨

이 지원의 대부분은 이미 마련되어 있습니다. 제네릭 매개 변수 지원으로 일반적인 ref struct이(가) 여기에서 설명된 대로 이미 구현되었습니다. DIM 구현이 이미 ref struct를 고려했을 수 있습니다. 그러나 이러한 각 항목을 추적해야 합니다.

API 버전 관리

ref 구조체에 대한 반제약 조건을 허용합니다.

allows ref struct 제약 조건 방지는 구현이 없는 많은 수의 제네릭 정의에 안전하게 적용할 수 있습니다. 즉, 대부분의 대리자, 인터페이스 및 abstract 메서드는 매개 변수에 allows ref struct 안전하게 적용할 수 있습니다. 이는 구현이 없는 API 정의일 뿐이므로 허용되는 형식 집합을 확장하면 ref struct 허용되지 않는 형식 인수로 사용되는 경우에만 오류가 발생합니다.

API 소유자는 "컴파일하는 경우 안전"이라는 간단한 규칙을 사용할 수 있습니다. 컴파일러는 다른 allows ref struct 사용 시와 마찬가지로 ref struct의 안전하지 않은 사용에 대해 오류를 발생시킬 것입니다.

동시에 API 작성자가 고려해야 하는 버전 관리 고려 사항이 있습니다. 기본적으로 API 소유자는 소유 형식/멤버가 나중에 allows ref struct호환되지 않도록 변경될 수 있는 형식 매개 변수에 allows ref struct 추가하지 않아야 합니다. 예를 들어:

  • 나중에 abstract 메서드로 변경할 수 있는 virtual 메서드
  • 나중에 구현을 추가할 수 있는 abstract 형식

이러한 경우 API 작성자는 형식/멤버 진화가 allows ref struct 규칙을 위반하는 방식으로 T 사용하지 않을 것이 확실한 경우가 아니면 ref struct 추가하는 데 주의해야 합니다.

allows ref struct 안티 제약을 제거하는 것은 항상 소스 및 바이너리 호환성을 손상시키는 변경입니다.

기본 인터페이스 메서드

API 작성자는 DIMS를 추가하면 다시 컴파일될 때까지 ref struct 구현자가 중단된다는 점에 유의해야 합니다. 이는 인터페이스에 DIM을 추가하면 기존 구현이 다시 컴파일될 때까지 중단되는 기존 DIM 동작 유사합니다. 즉, API 작성자는 DIM을 추가할 때 ref struct 구현 가능성을 고려해야 합니다.

이 상황을 만드는 데 필요한 세 가지 코드 구성 요소가 있습니다.

interface I1
{
    // 1. The addition of a DIM method to an _existing_ interface
    void M() { }
}

// 2. A ref struct implementing the interface but not explicitly defining the DIM 
// method
ref struct S : I1 { }

// 3. The invocation of the DIM method in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
    where T : allows ref struct, I1
{
    p.M();
}

이러한 세 가지 구성 요소는 모두 이 특정 문제를 만드는 데 필요합니다. (1) 및 (2) 이상은 다른 어셈블리에 있어야 합니다. 동일한 어셈블리에 있는 경우 컴파일 오류가 발생합니다.

UnscopedRef

[UnscopedRef] 멤버에서 interface 추가하거나 제거하는 것은 소스 호환성이 손상되는 변경이며 런타임 문제가 발생할 수 있습니다. 인터페이스 멤버를 정의할 때 특성을 적용해야 하며 나중에 추가하거나 제거하지 않아야 합니다.

범위<범위<T>>

이 기능 조합은 Span<Span<T>>같은 구문을 허용하지 않습니다. 이는 Span<T>정의를 확인하여 좀 더 명확해졌습니다.

readonly ref struct Span<T>
{
    public readonly ref T _data;
    public readonly int _length;

    public Span(T[] array) { ... }

    public static implicit operator Span<T>(T[]? array) { }
 
    public static implicit operator Span<T>(ArraySegment<T> segment) { }
}

이 형식 정의가 allows ref struct 포함하는 경우 정의의 모든 T 인스턴스는 잠재적으로 ref struct 형식인 것처럼 처리되어야 합니다. 즉, 문제의 두 가지 클래스를 제공합니다.

첫 번째는 Span(T[] array)과 같은 API와 암시적 연산자에 관한 것으로, Tref struct이 될 수 없습니다: 이는 배열 요소 또는 allows ref struct이 될 수 없는 제네릭 매개 변수로 사용됩니다. Span<T>을 사용하는 T와 호환할 수 없는 공용 API가 ref struct에 몇 개 있습니다. 이러한 API는 삭제할 수 없으므로 언어로 합리화해야 하는 공용 API입니다. 앞으로의 가장 가능성이 큰 경로는 컴파일러가 Span<T>을 특별한 경우로 처리하고, T의 인수가 잠재적으로 또는 또는 ref struct인 경우, 이러한 API 중 하나에 바인딩된 오류 코드를 발행하는 것입니다.

두 번째는 refref struct 필드를 언어가 지원하지 않는다는 것입니다. 해당 기능을 허용하기 위한 디자인 제안 있습니다. 언어로 허용되는지 또는 Span<T>관련 시나리오의 전체 집합을 처리할 만큼 표현력이 있는지는 불분명합니다.

이 두 가지 문제는 이 제안의 범위를 벗어습니다.

UnscopedRef 구현 논리

인터페이스 구현에 대한 [UnscopedRef] 규칙의 근거는 this 매개 변수를 메서드에 대한 암시적 인수가 아닌 명시적 인수로 시각화할 때 가장 쉽게 이해할 수 있습니다. 예를 들어, struct이 Python에서 처리하는 방식과 유사하게 암시적 매개변수로 시각화되는 다음의 this를 고려하십시오.

struct S
{
    public void M(scoped ref S this) { }
}

인터페이스 멤버에서 [UnscopedRef]은(는) 호출 지점에서 수명과 관련하여 thisscoped이(가) 부족함을 지정합니다. 구현 멤버에서 [UnscopedRef] 생략하도록 허용하면 ref T 매개 변수를 scoped ref T매개 변수로 구현할 수 있습니다. 언어는 이미 다음을 허용합니다.

interface I1
{
    void M(ref Span<char> span);
}

struct S : I1
{
    public void M(scoped ref Span<char> span) { }
}

관련 항목: